单线程并发
本文封面和思路来源于 Ruby China 2021 By 东仙队长的分享: Ruby 高并发编程指北
并发
假设有一个处理器, 因为只有一个核心, 所以同时他只能运行一个进程, 又因为一个进程上同一时间只能运行一个线程(task). 所以你可以认为, 同一时间, 这颗处理器只能处理一个任务.
操作系统会将cpu 使用时间
这一资源进行切分, 这就是时间分片
. 划分好的资源会按照调度算法分配给各个线程去使用. 进程内线程切换大概就是下面这种感觉:
因为现代计算机性能足够强劲, 所以使用者体感上是 task_1、2、3 在同时运行, 这就是并发.
我并不想用「多个任务同时发生」
这种模糊的表达, 在我看来「并发」
之所以被称为「并发」
单纯是翻译后中文表达的问题.
无论如何, 同时确实只有一个任务在进行
并行
并行就要好理解的多, 你有一个多核心处理器, 例如拥有两个核心的老古董E6600. 因为有两个核心, 同时能运行两个进程, 每个进程可以运行一个线程. 因此, 你的电脑就能真正的在同一时间运行两个任务.
大概就是下面这种感觉:
程序设计
之前刷知乎时, 看到这样一种说法:「你的程序首先必须支持并发, 才能支持并行」
因为并发不仅是对程序执行状态的一种描述, 也是一种程序设计的方法论:
设计并发程序的条件就是「程序的执行不依赖先后顺序、不依赖精确时序」
.
而一段不依赖先后顺序、不依赖精确时序的程序, 自然可以拆分成多段, 在多个线程上运行. 这多个线程又可以运行在不同的进程之上, 进而实现并行
.
首先我们来看看最常见的 web 应用场景, 从接收到用户请求到返回, 需要这些步骤:
其中
DB operation
占据了最大的空间, 因为数据库操作确实是最耗时的操作.
多线程并发
我们来假设一种极端的情况, 如果一直只有一个线程在运行, 那么运行过程就会变成这样:
如果是这样的话, 我想我们这台单核的机器没几个用户就 burn out 了.
应对这种情况, 最简单的思路就是多开几个线程, 虽然我们只有一个进程, 但当第一个线程卡在数据库操作时, 可以将控制权转移到其他线程进行操作.
协程
多线程并发, 一切看起来都很美好, 但有两个问题, 一个是计算机能启动的线程数量是有限的, 还有就是线程的切换有着客观的成本.
那么如果我们可以自己在程序中模拟线程
, 切换的不再是线程, 而是执行的程序方法. 岂不是可以用极低的成本去并发?
这个模拟线程, 或者说轻量线程/用户线程
, 就是协程.
有了协程, 我们可以更细粒度的控制程序的执行. 以下是 node.js 协程(async异步)的例子:
async function App(request, response) {
const id = request.query.id // <- read req and parsing
try {
const result = await DB.find({ id }) // <- DB operation
res.json({ // generate json and response
code: 200,
data: result
})
} catch(e) {
// ...
}
}
当程序执行到await
标识的数据库操作时, 就会在当前方法内形成阻塞, 等待数据库的返回结果. 但对于调用 App() 的方法来说, 程序则会继续向下执行(处理其他的请求).
整个过程的简略图大概如下:
关于协程更具体的介绍, 可以看我的这篇文章协程(Coroutine)和纤程(Fiber)
实际上这种单线程+协程
并发模型, 就是 Node.js 的并发模型.
这几年协程的火爆, jdk21、php8 都支持了协程方案. 足以提现这种模式的优势.
说到这里, 有没有什么比单线程异步并发更强的方案呢? 那就是
多线程+协程
, Go 就是这样的方案. Go 实现了一种更复杂的 Goroutine 到实际线程资源的映射, 使其可以以极低的配置达到恐怖的并发性能