logo
【该文在 2021-02-22T10:32:28.059+08:00 时间 被 一网 用户隐藏】

分享Rust中的无锁编程技术

头像
一网
3阅读0评论3 个月前

作者:poppindouble

当多线程同时对共享资源读写的时候,我们需要用锁去保护这个共享资源。这里有几个概念,一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并行多个线程,每条线程并行执行不同的任务[1],共享资源就是“堆(heap)”中的一段数据,那么们平常说的“锁”究竟是什么呢?而我们平常听到的无锁编程究竟是什么呢?在Rust开源社区中,crossbeam这个库究竟给开发者们提供了什么工具,解决了什么问题呢?这些问题我会慢慢在以后分享。

要解决的核心问题


多线程带来的最大问题在于其不确定性。在并行并发计算中,我们是没有办法控制cpu什么时候执行哪条线程的,我们为了最大限度地享受cpu所给我们带来的性能优化,只需要将所有的工作交给cpu,让cpu自己决定什么时候执行什么指令。不仅仅是cpu,实际上,编译器,操作系统都会有自己的优化策略,都会给我们的程序运行带来不确定性。所以我们在写多线程程序的时候,脑海里面时时刻刻要记住我们最大的三个敌人:

  1. cpu

  2. 编译器

  3. 操作系统

cpu和编译器都会根据自己的优化策略去更改指令的执行顺序,而操作系统是负责线程调度的,什么时候哪条线程获得了cpu的使用权,这是我们无法控制的。

核心问题带来的另一个问题


上面我们只提到了多线程编程指令执行顺序上的不确定性,而忽略了另一个很重要的环节,那就是共享资源的内存管理问题。正是因为线程执行顺序上的不确定性,这使得对于共享内存的管理变得格外复杂。比如在处理无锁数据结构的时候,从一个线程共享的链表里面删除一个节点,我们是否能立马释放其内存?答案是否定的,因为我们不确定是否有别的线程还持有对这块内存的引用。特别是对于Rust或者是C或者是C++这样的系统语言,这些语言没有垃圾回收机制,要求编程者对于共享内存进行手动管理,至于如何管理,这也是我以后要讨论的问题。


锁应该是最简单也是最直接可以解决上述两个痛点的解决方案了。锁给我们对多线程的运行提供了一个非常好的“模型”。这个模型是这样的:

  • 在堆上我们有一个共享数据。

  • 同时有很多线程都在尝试往这块内存写数据,或者是读数据。

  • 锁就像一个门卫,这个门卫可以保证在任意的时间点,最多只有一个线程对这块内存有使用权,这个线程可以往里面写数据或者读数据。

  • 当共享的内存发现自身的引用量达到0的时候释放内存。

那么这个神奇的“锁”究竟是什么呢?在Rust中又是如何实现的呢?

锁其实是一个协议(protocal),在Rust里面我们称之为trait。我们一起来看看这个trait:

pub trait RawLock: Default + Send + Sync {
type Token: Clone;

fn lock(&self) -> Self::Token;

/// # Safety
///
/// \`unlock()\` should be called with the token given by the corresponding \`lock()\`.
unsafe fn unlock(&self, token: Self::Token);

}

这里最主要要看到这几个概念Token lock函数以及unlock函数的签名。我们今天主要讲讲这个token是什么。

token我们可以将其理解为一个证明,在编程语言的语义中,这个证明是证明当前持有这个token的线程可以对这个共享资源进行任何操作。而其他没有这个证明的线程,只能卡在抢锁的代码处。

在Rust的std里面,lock函数返回的是一个LockResult<MutexGuard<'_, T>>,暂时不用管LockResult,MutextGuard实际上就是对我们token的进一步的封装。

那么我们从以上的定义可以得到以下几个信息:

  1. 任何的锁,包括spinlock,ticketlock,mcslock,等等,都必须实现以上的协议,也就是,必须提供token,提供lock函数的实现,提供unlock函数的实现。

  2. 任何的锁,都应该是Send以及Sync的,也就是说,任何的锁都可以安全的在线程中转移(Send),并且其借用(&)也必须是可以安全的在线程中转移(Sync)。

  3. lock函数成功返回的话,代表当前线程获得了一个可以访问共享资源的证明。

  4. unlock函数的签名中,需要提供token的ownership,并且这个函数是unsafe的,虽然函数本身并没有需要deref raw pointer这样的操作,但语言本身是无法保证调用这个函数的线程传入的token是否就是这个线程在之前调用lock成功时所返回的token。所以我们加上unsafe来提醒使用者。

我们现在对“锁”有一个初步的认识了,但是我们还没有提及另一个非常重要的概念,就是guard,也就是上面提及的对token的进一步的封装,在后续文章中,我会再慢慢和大家解释这个“证明”究竟是什么。

[1]wiki: thread

分享主题:
工具/资源经历/经验
加载中…
精选评论
暂无数据

暂无数据