多线程 | Rust学习笔记

作者:谢敬伟,江湖人称“刀哥”,20年IT老兵,数据通信网络专家,电信网络架构师,目前任Netwarps开发总监。刀哥在操作系统、网络编程、高并发、高吞吐、高可用性等领域有多年的实践经验,并对网络及编程等方面的新技术有浓厚的兴趣。

现代的CPU基本都是多核结构,为了充分利用多核的能力,多线程都是绕不开的话题。无论是同步或是异步编程,与多线程相关的问题一直都是困难并且容易出错的,本质上是因为多线程程序的复杂性,特别是竞争条件的错误,使得错误发生具备一定的随机性,而随着程序的规模越来越大,解决问题的难度也随之越来越高。

其他语言的做法

C/C++将同步互斥,以及线程通信的问题全部交给了程序员。关键的共享资源一般需要通过Mutex/Semaphone/CondVariable之类的同步原语保证安全。简单地说,就是需要加锁。然而怎么加,在哪儿加,怎么释放,都是程序员的自由。不加也能跑,绝大多数时候,也不会出问题。当程序的负载上来之后,不经意间程序崩溃了,然后就是痛苦地寻找问题的过程。

Go提供了通过channel的消息机制来规范化协程之间的通信,但是对于共享资源,做法与C/C++没有什么不同。当然,遇到的问题也是类似。

Rust 做法

Go类似,Rust 也提出了channel机制用于线程之间的通信。因为Rust 所有权的关系,无法同时持有多个可变引用,因此channel被分成了rxtx两部分,使用起来没有Go的那么直观和顺手。事实上,channel的内部实现也是使用原子操作、同步原语对于共享资源的封装。所以,问题的根源依然在于Rust如何操作共享资源。

Rust 通过所有权以及Type系统给出了解决问题的一个不同的思路,共享资源的同步与互斥不再是程序员的选项,Rust代码中同步及互斥相关的并发错误都是编译时错误,强迫程序员在开发时就写出正确的代码,这样远远好过面对在生产环境中顶着压力排查问题的窘境。我们来看一看这一切是如何做到的。

Send,Sync 究竟是什么

Rust语言层面通过 std::marker 提供了 SendSync 两个Trait。一般地说法,Send标记表明类型的所有权可以在线程间传递,Sync标记表明一个实现了Sync 的类型可以安全地在多个线程中拥有其值的引用。这段话很费解,为了更好地理解SendSync,需要看一看这两个约束究竟是怎样被使用的。以下是标准库中std::thread::spawn()的实现:

    pub fn spawn<F, T>(self, f: F) -> io::Result<JoinHandle<T>>
    where
        F: FnOnce() -> T,
        F: Send + 'static,
        T: Send + 'static,
    {
        unsafe { self.spawn_unchecked(f) }
    }

可以看到,创建一个线程,需要提供一个闭包,而这个闭包的约束是 Send ,也就是需要能转移到线程中,闭包返回值T的约束也是 Send(这个不难理解,线程运行后返回值需要转移回去) 。举例说明,以下代码无法通过编译。

    let a = Rc::new(100);
    let h = thread::spawn(move|| {
        let b = *a+1;

    });

    h.join();

编译器指出,std::rc::Rc<i32> cannot be sent between threads safely。原因在于,闭包的实现在内部是由编译器创建一个匿名结构,将捕获的变量存入此结构。以上代码闭包大致被翻译成:

struct {
    a: Rc::new(100),
    ...
}

Rc<T>是不支持 Send 的数据类型,因此该匿名结构,即这个闭包,也不支持 Send ,无法满足std::thread::spawn()关于F的约束。

上面代码改用Arc<T>,则编译通过,因为Arc<T>是一种支持 Send的数据类型。但是Arc<T>不允许共享可变引用,如果想实现多线程之间修改共享资源,则需要使用Mutex<T>来包裹数据。代码会改为这个样子:

    let mut a = Arc::new(Mutex::new(100));
    let h = thread::spawn(move|| {
        let mut shared = a.lock().unwrap();
        *shared = 101;

    });
    h.join();

为什么Mutex<T>可以做到这一点,能否改用RefCell<T>完成相同功能?答案是否定的。我们来看一下这几个数据类型的限定:

unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}

unsafe impl<T: ?Sized> Send for RefCell<T> where T: Send {}
impl<T: ?Sized> !Sync for RefCell<T> {}

unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}

Arc<T>可以Send,当其包裹的T同时支持SendSync。很明显Arc<RefCell<T>>不满足此条件,因为RefCell<T>不支持Sync。而Mutex<T>在其包裹的T支持Send的前提下,满足同时支持SendSync。实际上,Mutex<T>的作用就是将一个支持Send的普通数据结构转化为支持Sync,进而可以通过Arc<T>传入线程中。我们知道,多线程下访问共享资源需要加锁,所以Mutex::lock()正是这样一个操作,lock()之后便获取到内部数据的可变引用。

通过上述分析,我们看到Rust另辟蹊径,利用所有权以及Type系统在编译时刻解决了多线程共享资源的问题,的确是一个巧妙的设计。

异步代码,协程

异步代码同步互斥问题与同步多线程代码没有本质不同。异步运行库一般提供类似于std::thread::spawn()的方式来创建协程/任务,以下是async-std创建一个协程/任务的API

pub fn spawn<F, T>(future: F) -> JoinHandle<T>
where
    F: Future<Output = T> + Send + 'static,
    T: Send + 'static,
{
    Builder::new().spawn(future).expect("cannot spawn task")
}

可以看到,与std::thread::spawn()非常相似,闭包换成了Future,而Future要求Send约束。这意味着参数future必须可以Send。我们知道,async语法通过generaror生成了一个状态机驱动的Future,而generaror与闭包类似,捕获变量,放入一个匿名数据结构。所以这里变量必须也是Send才能满足FutureSend约束条件。试图转移一个Rc<T>进入async block依然会被编译器拒绝。以下代码无法通过编译:

    let a = Rc::new(100);
    let h = task::spawn(async move {
        let b = a;
    });

此外,在异步代码中,原则上应当避免使用同步的操作从而影响异步代码的运行效率。试想一下,如果Future中调用了std::mutex::lock,则当前线程被挂起,Executor将不再有机会执行其他任务。为此,异步运行库一般提供了类似于标准库的各种同步原语。这些同步原语不会挂起线程,而是当无法获取资源时返回Poll::PendingExecutor将当前任务挂起,执行其他任务。

完美了么?死锁问题

Rust虽然用一种优雅的方式解决了多线程同步互斥的问题,但这并不能解决程序的逻辑错误。因此,多线程程序最令人头痛的死锁问题依然会存在于Rust的代码中。所以说,所谓Rust“无惧并发”是有前提的。至少在目前,看不到编译器可以智能到分析并解决人类逻辑错误的水平。当然,届时程序员这个岗位应该也就不存在了...


深圳星链网科科技有限公司(Netwarps),专注于互联网安全存储领域技术的研发与应用,是先进的安全存储基础设施提供商,主要产品有去中心化文件系统(DFS)、区块链基础平台(SNC)、区块链操作系统(BOS)。
微信公众号:Netwarps

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342