当前位置: 首页 > 后端技术 > Python

多线程-Rust学习笔记

时间:2023-03-25 19:52:31 Python

作者:谢经纬,人称“刀哥”,20年IT从业者,数据通信网络专家,电信网络架构师,现任Netwarps开发总监。刀哥在操作系统、网络编程、高并发、高吞吐量、高可用等领域有多年的实践经验,对网络和编程方面的新技术有着浓厚的兴趣。现代CPU基本上都是多核结构。为了充分利用多核的能力,多线程是一个绕不开的话题。无论是同步编程还是异步编程,多线程相关的问题一直都是比较棘手和容易出错的。本质上,正是由于多线程程序的复杂性,尤其是竞争条件的错误,导致错误的发生具有一定的随机性。随着程序规模的增大,求解问题的难度也随之增大。其他语言C/C++的做法,把同步互斥和线程通信的问题全部留给了程序员。关键共享资源通常需要通过同步原语(例如Mutex/Semaphone/CondVariable)来保护。简单地说,需要锁。但是,如何添加,添加到哪里,如何发布,都是程序员的自由。不加也可以运行,大部分时候不会出问题。当程序的负载上来的时候,不经意间程序就崩溃了,然后就是痛苦的寻找问题的过程。Go通过channels提供消息机制来规范协程之间的通信,但是对于共享资源,其方式与C/C++并无区别。当然,遇到的问题都是类似的。Rust的方法类似于Go。Rust还提出了线程间通信的通道机制。因为Rust的所有权关系,不可能同时持有多个变量引用,所以channel分为rx和tx两部分,不像Go那么直观易用。实际上,通道的内部实现也使用了原子操作和同步原语来封装共享资源。所以,问题的根源还是在于Rust如何操作共享资源。Rust通过所有权和类型系统提供了一种不同的解决问题的方法。共享资源的同步互斥不再是程序员的选择。Rust代码中与同步和互斥相关的并发错误是编译时错误。程序员在开发过程中写出正确的代码远比在生产环境中顶着压力排查问题的困境要好得多。让我们来看看这一切是如何完成的。什么是发送、同步?Rust语言级别通过std::marker提供了两个Traits,Send和Sync。一般而言,Send标志表示类型的所有权可以在线程之间转移,Sync标志表示实现Sync的类型可以在多个线程中安全地拥有对其值的引用。这段话非常混乱。为了更好地理解Send和Sync,我们需要看一下这两个约束是如何使用的。下面是标准库中std::thread::spawn()的实现:pubfnspawn(self,f:F)->io::Result>whereF:FnOnce()->T,F:Send+'static,T:Send+'static,{unsafe{self.spawn_unchecked(f)}}可以看到创建线程需要提供闭包,约束这个闭包的都是Send,也就是需要传给线程,闭包返回值T的约束也是Send(这个不难理解,线程运行完返回值需要传回去).例如,以下代码无法编译。让a=Rc::new(100);leth=thread::spawn(移动||{letb=*a+1;});h.join();编译器指出std::rc::Rc不能在线程之间安全地发送。原因是闭包的实现在内部让编译器创建了一个匿名结构,捕获的变量存储在其中。上面代码的闭包大致翻译为:struct{a:Rc::new(100),...}而Rc是不支持Send的数据类型,所以匿名结构,即,闭包,不支持Send,无法满足std::thread::spawn()对F的约束。如果上面的代码改用Arc,编译会通过,因为Arc是一个数据支持发送的类型。但是Arc不允许共享可变引用。如果要修改多个线程之间的共享资源,需要用Mutex来包裹数据。代码会变成这样:letmuta=Arc::new(Mutex::new(100));leth=thread::spawn(move||{letmutshared=a.lock().unwrap();*shared=101;});h.join();为什么Mutex可以做到这一点?您可以使用RefCell来完成相同的功能吗?答案是否定的。我们来看看这些数据类型的限制:unsafeimplSendforArc{}unsafeimplSyncforArc{}unsafeimplSendforRefCellwhereT:Send{}impl!SyncforRefCell{}unsafeimplSendforMutex{}unsafeimplSyncforMutex{}Arc可以发送,当包装的T支持发送和同步时。显然Arc>不满足这个条件,因为RefCell不支持Sync。在wrappedT支持Send的前提下,Mutex同时满足Send和Sync。其实Mutex的作用就是把一个支持Send的普通数据结构转换成支持Sync的,然后就可以通过Arc传递给线程了。我们知道在多线程下访问共享资源是需要加锁的,所以Mutex::lock()就是这样一个操作,l??ock()之后获取的是内部数据的变量引用。通过上面的分析,我们可以看出Rust另辟蹊径,利用所有权和Type系统来解决编译时多线程共享资源的问题,这确实是一个巧妙的设计。异步代码,协程异步代码中的同步互斥问题与同步多线程代码没有本质区别。异步运行时库通常提供类似于std::thread::spawn()的方法来创建协程/任务。以下是async-std创建协程/任务的API:pubfnspawn(future:F)->JoinHandlewhereF:Future+Send+'static,T:Send+'static,{Builder::new().spawn(future).expect("cannotspawntask")}可以看出,与std::thread::spawn()非常相似,闭包被Future取代,Future需要Send约束。这意味着参数future必须是Sendable。我们知道async语法通过生成器生成一个状态机驱动的Future,而生成器类似于闭包,捕获变量并放入匿名数据结构中。所以这里的变量也必须是Send才能满足Future的Send约束。将Rc移动到异步块中的尝试仍将被编译器拒绝。以下代码将无法编译:leta=Rc::new(100);leth=task::spawn(asyncmove{letb=a;});同样,在异步代码中,同步操作也会影响异步代码的运行效率。试想一下,如果在Future中调用了std::mutex::lock,那么当前线程就会被挂起,Executor就再也没有机会执行其他任务了。为此,异步运行时库一般会提供类似于标准库的各种同步原语。这些同步原语不会挂起线程,而是在无法获取资源时返回Poll::Pending,Executor会挂起当前任务,去执行其他任务。完美吗?死锁问题虽然Rust以优雅的方式解决了多线程同步互斥的问题,但是并不能解决程序的逻辑错误。因此,多线程程序最头疼的死锁问题,依然会存在于Rust代码中。所以,所谓的Rust“不怕并发”是有前提的。至少目前,我们还没有看到编译器可以智能到足以分析和解决人类逻辑错误的程度。当然,那时候程序员的职位应该不存在了……深圳市星联网络科技有限公司(Netwarps),专注于互联网安全存储领域技术的研发和应用,是一家先进的安全存储基础设施提供商。主要产品有去中心化文件系统(DFS)、区块链基础平台(SNC)、区块链操作系统(BOS)。微信公众号:网华