文盤Rust -- 生命週期問題引發的 static hashmap 鎖

2023-09-05 15:00:44

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的生命週期是個很有意思的概念,從認識到理解確實有個過程。

原始碼地址

作者:京東科技 賈世聞

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