Categories
程式開發

Kotlin协程实践(1)之进程、线程、协程


本文自Roman Elizarov在KotlinConf 2018关于Kotlin协程演讲和笔者构建网络爬虫服务实践过程中的一些总结而来。

由于篇幅和精力限制,打算分多篇文章逐步写作完成,敬请期待!

知其然才能知其所以然,为了能用好协程,我们不得不先从一些理论开始!

进程、线程、协程

为了了解为什么要提出协程这一概念,让我们先来回顾一下历史。

进程

进程是操作系统里面的概念。操作系统最大的需求是要“同时”运行多个任务(程序),于是用进程来封装和抽象这个需求。程序运行在进程中,进程中的程序会感觉到一个假象,自己独立占用了CPU资源,感觉自己一直被执行。

当然这只是个假象,操作系统通过如抢占式分片时间来进行调度。这种抽象的好处在于操作系统负责了这个复杂性,而程序只负责自己的逻辑代码即可。如果让每个程序来考虑如何分时使用CPU,那么对于程序员来说,是个灾难吧。

线程

进程是一个重型资源,包括独立的内存空间、文件句柄、状态等等等等,这里我们不需要详细描述。做个想象实验,在一个操作系统上创建100进程,在一个JVM中创建100线程,哪个更快?

线程也是一种抽象,暂且我们认为是程序级别的抽象。随着一个进程(程序)中,逻辑越来越复杂,需要“同时”存在多个相互独立的逻辑,比如一个负责UI事件、一个负责绘画、一个负责数据库查询计算等。

和进程一样,从程序员的角度看这个问题,此时需要线程这个抽象概念来解决问题,不然程序会很复杂。为什么不创建多个进程呢?前面我们说过进程是重资产,太昂贵。线程比进程更为轻量,因为线程们共享同一个进程的内存空间等资源。甚至操作系统都可以感知不到线程概念存在。同样由操作系统和程序来共同欺骗线程,让线程感觉自己独立占用了CPU来执行自己的指令。

不去考虑用户态线程还是核心态线程的实现方式,Java提供了一个统一Thread的抽象。其他语言的Runtime也会类似提供支持。仅仅只有Thread还不够编写完美的程序,因为线程共享内存空间,不得不额外提供Lock、信号量等工具,让线程们可以相互配合来完成工作。

协程

协程并不是一个新的概念,不过对于Java程序员而言是比较陌生。

有了进程、线程这个两个抽象后,程序员们过着幸福无比的生活,写出了很多复杂强大的软件。但是随着高并发、高负载、C10K问题越来越困扰程序员们。

一个核心的原因在于Block(阻塞)。

比如线程中的代码,执行一个IO请求,此时线程被Block,也就是IO请求没有返回之前,线程不能做其他任何事情了。

那么Block会引起什么问题?比如你系统中有10工作线程,假设每个线程处理一个用户请求花费1秒钟,这一秒钟包括了IO等待的990毫秒,10毫秒是CPU计算时间。那么请问在这个一秒钟内,如果来了第11个用户请求怎么办?答案自然是排队等待了。也就是说这个系统1秒内最多处理10个请求。当然你也可以创建更多工作线程来提高吞度量。虽然线程是轻量资源,但是是相对进程而言,实际上你在一个JVM里面去创建10000个线程试试,可能直接崩溃。

因为CPU很快,IO却很慢,一个是火箭速度、一个是蚂蚁在爬行。线程这个抽象的问题在于,在系统执行IO时,线程傻傻等待,浪费大把美好时光,从而造成系统吞吐量过低。

所以程序员们需要一个新的抽象来解决这个问题,“不要阻塞”。

为了让线程在遇到IO阻塞时,能不闲着,去做其他事情。此刻就需要把程序逻辑封装在一个叫协程的抽象里面。

如果程序逻辑被装在协程里面,那么上面的例子中,只需要一个线程,系统吞度量就可以提升到100/s.

所以协程的存在是为了避免线程被Block卡住。

小结

本篇简单介绍了站在程序员角度,进程、线程、协程三个抽象概念如何帮助他们完成编程任务,以及三个抽象的优缺点。

下一篇我们会介绍Callback地狱和事件循环问题。