再次 Borrow 陈天老师的 Rust 学习路经图,欢迎进入 Rust 并发和异步的学(爬)习(坡)之旅。

Rust 并发编程初探

并发和并行

许多人分不清并发和并行的概念,所以学习在 Rust 异步编程之前,首要弄清楚清楚并发(Concurrence)并行(parallel) 的差异。

咱们常常在相关操作体系的书里边听到:

  • 并发是指两个或多个作业在同一时间距离内发生。
  • 并行性是指体系具有一同进行运算或操作的特性。
  1. 解说一:并发是指两个或多个作业在同一时间距离发生,而并行是指两个或许多个作业在同一时间发生。
  2. 解说二:并发是在同一实体上的多个作业,并行是在不同实体上的多个作业。
  3. 解说三:并发是在一台处理器上“一同”处理多个使命,并行是在多台处理器上一同处理多个使命。如分布式集群。

Golang 创始人之一的 Rob Pike,对此有很精辟很直观的解说:

Concurrency is aboutdealing withlots of things at once. Parallelism is aboutdoinglots of things at once.

并发是一种一同处理许多作业的才干,并行是一种一同履行许多作业的手法。

咱们把要做的作业放在多个线程中,或许多个异步使命中处理,这是并发的才干。在多核多 CPU 的机器上一同运转这些线程或许异步使命,是并行的手法。能够说,并发是为并行赋能。当咱们具有了并发的才干,并行便是水到渠成的作业。

Erlang之父Joe Armstrong,用一张图片解说了并发与并行的差异:

Rust 并发编程初探

上图很直观的表现了:

  • 并发(Concurrent) 是多个行列运用同一个咖啡机,然后两个行列轮换着运用,终究每个人都能接到咖啡
  • 并行(Parallel) 是每个行列都具有一个咖啡机,一同有多个人在接咖啡,终究也是每个人都能接到咖啡,效率更高。

当然,咱们还能够对比下串行:只要一个行列且仅运用一台咖啡机,前面哪个人接咖啡时忽然发呆了几分钟,后边的人就只能等他完毕才干持续接。可能有疑问了,从图片来看,并发也存在这个问题啊,前面的人发呆了几分钟不接咖啡怎么办?很简单,另外一个行列的人把他推开就行了,自己队友不能在背面开枪,可是其它队的能够:)

并发(concurrency):指在同一时间只能有一条指令履行,但多个进程指令被快速的轮换履行,使得在微观上具有多个进程一同履行的效果,但在微观上并不是一同履行的,只是把时间分成若干段,使多个进程快速替换的履行处理。

Rust 并发编程初探

并行(parallel):指在同一时间,有多条指令在多个处理器上一同履行。所以无论从微观仍是从微观来看,二者都是一同履行,一同处理。

Rust 并发编程初探

当有多个线程在操作时,假如体系只要一个 CPU,则它底子不可能真正一同进行一个以上的线程,它只能把 CPU 运转时间划分成若干个时间段,再将时间段分配给各个线程履行,在一个时间段的线程代码运转时,其它线程处于挂起状况,这种方法咱们称之为并发(Concurrent)。

当体系有一个以上 CPU 时,则线程的操作有可能非并发。当一个 CPU 履行一个线程时,另一个 CPU 能够履行另一个线程,两个线程互不抢占 CPU 资源,能够一同进行,这种方法咱们称之为并行(Parallel)。

先给出一个定论:并发和并行都是对“多使命”处理的描绘,其间并发是轮番履行(处理),倾向于处理才干,比方并发量,而并行是一同履行(处理),倾向于处理手法,比方使命并发

并发编程模型

咱们知道各个言语的完结不同,所以导致各个言语的并发模型各不相同。当咱们用某种言语编写、编译好一个程序之后,该程序在运转起来之后会占用一个进程。在这个进程内,能够由进程拓荒出一些线程,这个线程是操作体系级别的。而在言语内部,程序员调用该言语创立的线程则是编程言语级别的。而这两者是否是一一对应,则要看该言语的内部完结:

  • OS原生线程:例如 Rust 言语是直接调用操作体系供给的API,所以终究程序内的线程数和该程序占用的操作体系线程数相等
  • 协程(Coroutines) :相似 Go 言语编写的程序内部的 M 个线程最后会以某种映射方法运用 N 个操作体系线程去运转
  • 作业驱动(Event driven):作业驱动常常跟回调( Callback )一同运用,这种模型功能适当的好,但最大的问题便是存在回调阴间的风险。
  • actor模型:根据音讯传递,对分解成的小块进行并发计算。是Erlang言语杀手锏。
  • async/await模型:该模型功能高,还能支撑底层编程,一同又像线程和协程那样无需过多的改变编程模型,但有得必有失,async模型的问题便是内部完结机制过于杂乱。

总之,Rust 经过权衡取舍后,终究选择了一同供给多线程async/await 两种并发编程模型:

  • 多线程在规范库中得到了完结,直接调用底层操作体系API,完结和运用简单。适合于量小的并发需求。
  • async/await 完结起来较为杂乱,但 Rust 经过言语特性 + 规范库 + 三方库的方法完结和封装,让开发者能够不用关怀底层完结逻辑,适用于量大的并发和异步IO。

Rust 中的异步编程

异步编程便是一个并发编程模型,异步编程答应咱们一同并发运转很多的使命,却只是需求几个乃至一个OS线程或CPU核心,现代化的异步编程在运用体会上跟同步编程也几无差异。

目前已经有诸多言语都经过async的方法供给了异步编程,但Rust在完结上有所差异:

  • Future 在 Rust 中是惰性的,只要在被轮询(poll)时才会运转, 因而丢掉一个future会阻止它未来再被运转, 你能够将Future理解为一个在未来某个时间点被调度履行的使命。
  • Async 在 Rust 中运用开支是零, 意味着只要你能看到的代码(自己的代码)才有功能损耗,你看不到的(async内部完结)都没有功能损耗,例如,你能够无需分配任何堆内存、也无需任何动态分发来运用async,这关于热点途径的功能有非常大的好处,正是得益于此,Rust 的异步编程功能才会这么高。
  • Rust 没有内置异步调用所必须的运转时,可是无需担心,Rust社区生态中已经供给了非常优异的运转时完结,例如大明星tokio
  • 运转时一同支撑单线程和多线程,这两者具有各自的优缺点, 稍后会讲

Async 异步与多线程的选型

虽然async多线程都能够完结并发编程,后者乃至还能经过线程池来增强并发才干,可是这两个方法并不互通,从一个方法切换成另一个需求很多的代码重构作业,因而把握二者的差异和适用范围,然后提前选型适当重要。

  • 关于 CPU密布型 使命,例如并行计算,运用多线程编程更有优势。 这是由于这种密布使命往往会让所在的线程长时间满负荷运转,一同你所创立的线程数应该等于CPU核心数,充分利用CPU的并行才干。此刻不需求频频创立和切换进程,由于任何线程切换都会带来功能损耗,所以你能够将线程绑定到CPU核心上来减少线程上下文切换。

  • 而关于 IO密布型 使命,例如 web 服务器、数据库连接等等网络服务,运用异步编程更有优势。由于这些使命绝大部分时间都处于等候状况,假如运用多线程,那线程很多时间会处于空闲状况,再加上线程上下文切换的昂扬价值,会损失很多功能。而运用 async,既能够有效的降低 CPU 和内存的担负,又能够让很多的使命并发的运转,一个使命一旦处于IO或许其他等候(堵塞)状况,就会被立刻切走并履行另一个使命,而这儿的使命切换的功能开支要远远低于运用多线程时的线程上下文切换。

async 底层也是根据线程完结。可是它根据线程封装了一个运转时,能够将多个使命映射到少数线程上。其实便是将很多并发的IO密布作业丢到少数线程中,并经过作业来进行高效通信。

价值便是这样做会增大 Rust 程序的运转时(运转时是那些会被打包到所有程序可履行文件中的 Rust 代码),形成编译出的二进制可履行文件体积明显增大。

用一个简单的例子说明两者的差异:比方咱们想要下载两个文件。咱们能够一个一个的 download(串行方法),但明显这样不是最快的。此刻咱们会很自然地想到运用多线程并行来下载:

多线程编程:

fn download_two_files() {
    // 创立两个新线程履行使命
    let thread_one = thread::spawn(|| download("URL1"));
    let thread_two = thread::spawn(|| download("URL2"));
    // 等候两个线程的完结
    thread_one.join().expect("thread one panic");
    thread_two.join().expect("thread two panic");
}

假如每次你只需求下载一两个文件,这样做没有任何问题。但问题在于,当此你需求一同下载成百上千个文件的时分,一个下载使命就消耗一个线程,线程本身的资源消耗会被急速放大(线程仍是太重了)。此刻你就能够考虑运用 async

async 异步编程:

async fn get_two_sites_async() {
    // 创立两个不同的future
    // 你能够把future理解为未来某个时间会被履行的计划使命、JS中的Promise
    // 当两个future被一同履行后,它们将并发的去下载目标页面
    let future_one = download_async("URL1");
    let future_two = download_async("URL2");
    // 一同运转两个`future`,直至完结
    join!(future_one, future_two);
}

Async 比较多线程模型,在此刻展现出的是在并行量不变的情况下,减少了创立和切换线程的花销。

总结

并发和并行都是对“多使命”处理的描绘,其间并发是轮番处理,而并行是一同处理并发编程代表程序的不同部分彼此独立的履行,而并行编程代表程序不同部分于一同履行。在并发编程模型上,Rust 中由于言语规划理念、安全、功能的多方面考虑,并没有采用 Go 言语大道至简的方法,而是选择了多线程async/await相结合,长处是可控性更强、功能更高,缺点是杂乱度并不低,当然这也是体系级言语的应有选择:运用杂乱度交换可控性和功能。

事实上,async 和多线程并不是二选一,在同一应用中,常常能够一同运用这两者。虽然async和多线程都能够完结并发编程,后者乃至还能经过线程池来增强并发才干,可是这两个方法并不互通,从一个方法切换成另一个需求很多的代码重构作业,因而提前为自己的项目选择适合的并发模型就变得至关重要。

总之,async编程适合 IO 密布,多线程适合 CPU 密布。简单总结下选用规矩:

  • 有很多IO使命需求并发运转时,选async模型
  • 有部分IO使命需求并发运转时,选多线程,假如想要降低线程创立和销毁的开支,能够运用线程池
  • 有很多CPU密布使命需求并行运转时,例如并行计算,选多线程模型,且让线程数等于或许稍大于CPU核心数
  • 无所谓时,一致选多线程

参考

  • course.rs/advance/con…
  • kaisery.github.io/trpl-zh-cn/…
  • github.com/rustlang-cn…
  • hardocs.com/d/rustprime…
  • huangjj27.github.io/async-book/…
  • /post/715697…
  • mp.weixin.qq.com/s/aA89BBUfM…