博客 / 詳情

返回

文盤Rust -- 生命週期問題引發的 static hashmap 鎖 | 京東雲技術團隊

2021年上半年,擼了個rust cli開發的框架,基本上把交互模式,子命令提示這些cli該有的常用功能做進去了。項目地址:https://github.com/jiashiwen/interactcli-rs。

春節以前看到axum已經0.4.x了,於是想看看能不能用rust做個服務端的框架。

春節後開始動手,在做的過程中會碰到各種有趣的問題。於是記下來想和社區的小夥伴一起分享。社區裏的小夥伴大部分是DBA和運維同學,如果想進一步瞭解更底層的東西,代碼入手是個好路數。

我個人認為想看懂代碼先要寫好代碼,起碼瞭解開發的基本路數和工程的一般組織模式。但好多同學的主要工作並不是專職開發,所以也就沒有機會下探研發技術。代碼這個事兒光看書是不管用的。瞭解一門語言最好的方式是使用它。

那麼,問題來了非研發人員如何熟悉語言呢?詠春拳裏有句拳諺:”無師無對手,樁與鏡中求“。解釋兩句,就是在沒有師兄弟練習的情況下,對着鏡子和木人樁練習。在這裏我覺得所謂樁有兩層含義,一個是木人樁,就是練習的工具,一個是”站樁“,傳統武術訓練基本功的方法。其實在實際的工作中DBA和運維同學會有很多場景需要編程,比如做一些運維方面的統計工作;分析問題時需要拿到某些數據。如果追求簡單用Python的話可能對於其他語言就沒有涉獵了。如果結合你運維數據庫的原生開發語言,假以時日慢慢就能看懂相關的底層邏輯了。我個人有個觀點,產品研發的原生語言是瞭解產品底層最好的入口。

後面如果在Rust的開發過程中有其他問題,我本人會把問題結合實際也寫到這個系列裏,也希望社區裏對Rust感興趣的小夥伴一起來”盤Rust“。 言歸正傳,説説這次在玩兒Rust時遇到的問題吧。

在 Rust 開發過程中,我們經常需要全局變量作為公共數據的存放位置。通常做法是利用 lazy_static/onecell 和 mux/rwlock 生成一個靜態的 collection。

代碼長這樣

use std::collections::HashMap;
use std::sync::RwLock;

lazy_static::lazy_static! {
    static ref GLOBAL_MAP: RwLock<HashMap<String,String>> = RwLock::new({
        let map = HashMap::new();
        map
    });
}



基本的數據存取這樣實現

use std::collections::HashMap;
use std::sync::RwLock;

lazy_static::lazy_static! {
    static ref GLOBAL_MAP: RwLock<HashMap<String,String>> = RwLock::new({
        let map = HashMap::new();
        map
    });
}

fn main() {
    for i in 0..3 {
        insert_global_map(i.to_string(), i.to_string())
    }
    print_global_map();
    println!("finished!");
}

fn insert_global_map(k: String, v: String) {
    let mut gpw = GLOBAL_MAP.write().unwrap();
    gpw.insert(k, v);
}

fn print_global_map() {
    let gpr = GLOBAL_MAP.read().unwrap();
    for pair in gpr.iter() {
        println!("{:?}", pair);
    }
}



insert\_global\_map函數用來向GLOBAL\_MAP插入數據,print\_global_map()用來讀取數據,上面程序的運行結果如下

("0", "0")
("1", "1")
("2", "2")



下面我們來實現一個比較複雜一點兒的需求,從 GLOBAL_MAP 裏取一個數,如果存在後面進行刪除操作,直覺告訴我們代碼似乎應該這樣寫

use std::collections::HashMap;
use std::sync::RwLock;

lazy_static::lazy_static! {
    static ref GLOBAL_MAP: RwLock<HashMap<String,String>> = RwLock::new({
        let map = HashMap::new();
        map
    });
}

fn main() {
    for i in 0..3 {
        insert_global_map(i.to_string(), i.to_string())
    }
    print_global_map();
    get_and_remove(1.to_string());
    println!("finished!");
}

fn insert_global_map(k: String, v: String) {
    let mut gpw = GLOBAL_MAP.write().unwrap();
    gpw.insert(k, v);
}

fn print_global_map() {
    let gpr = GLOBAL_MAP.read().unwrap();
    for pair in gpr.iter() {
        println!("{:?}", pair);
    }
}

fn get_and_remove(k: String) {
    println!("execute get_and_remove");
    let gpr = GLOBAL_MAP.read().unwrap();
    let v = gpr.get(&*k.clone());
    let mut gpw = GLOBAL_MAP.write().unwrap();
    gpw.remove(&*k.clone());
}




上面這段代碼輸出長這樣

("0", "0")
("1", "1")
("2", "2")
execute get_and_remove




代碼沒有結束,而是hang在了get\_and\_remove函數。 為啥會出現這樣的情況呢?這也許與生命週期有關。gpr和gpw 這兩個返回值分別為 RwLockReadGuard 和 RwLockWriteGuard,查看這兩個

struct 發現確實可能引起死鎖

must_not_suspend = "holding a RwLockWriteGuard across suspend \
                    points can cause deadlocks, delays, \
                    and cause Future's to not implement `Send`"



問題找到了就可以着手解決辦法了,既然是與rust的生命週期有關,那是不是可以把讀和寫分別放在兩個不同的生命週期裏呢,於是對代碼進行改寫

use std::collections::HashMap;
use std::sync::RwLock;

lazy_static::lazy_static! {
    static ref GLOBAL_MAP: RwLock<HashMap<String,String>> = RwLock::new({
        let map = HashMap::new();
        map
    });
}

fn main() {
    for i in 0..3 {
        insert_global_map(i.to_string(), i.to_string())
    }
    print_global_map();
    get_and_remove(1);
    println!("finished!");
}

fn insert_global_map(k: String, v: String) {
    let mut gpw = GLOBAL_MAP.write().unwrap();
    gpw.insert(k, v);
}

fn print_global_map() {
    let gpr = GLOBAL_MAP.read().unwrap();
    for pair in gpr.iter() {
        println!("{:?}", pair);
    }
}

fn get_and_remove_deadlock(k: String) {
    println!("execute get_and_remove");
    let gpr = GLOBAL_MAP.read().unwrap();
    let _v = gpr.get(&*k.clone());
    let mut gpw = GLOBAL_MAP.write().unwrap();
    gpw.remove(&*k.clone());
}

fn get_and_remove(k: i32) {
    let v = {
        let gpr = GLOBAL_MAP.read().unwrap();
        let v = gpr.get(&*k.to_string().clone());
        match v {
            None => Err(anyhow!("")),
            Some(pair) => Ok(pair.to_string().clone()),
        }
    };
    let vstr = v.unwrap();
    println!("get value is {:?}", vstr.clone());
    let mut gpw = GLOBAL_MAP.write().unwrap();
    gpw.remove(&*vstr);
}




正確輸出

("1", "1")
("0", "0")
("2", "2")
get value is "1"
("0", "0")
("2", "2")
finished!



Rust的生命週期是個很有意思的概念,從認識到理解確實有個過程。

源碼地址

作者:京東科技 賈世聞

來源:京東雲開發者社區 轉載請註明來源

user avatar changhao_flag 頭像 jackysummer 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.