0%

协程

Java语言里解决并发问题靠的是多线程,但线程是个重量级对象,不能频繁的创建、销毁,而且线程切换的成本也很高,Java SDK中的线程池及工具类也比较复杂。

其他语言有一种轻量级线程-协程(Coroutine)的并发问题解决方案。

Java OpenSDK中Loom项目的目标是支持协程。

从操作系统的角度看,线程是在内核态中调度的,而协程是在用户态调度的,相对于线程来说,协程切换的成本更低。协程的栈相比线程栈也小得多。从时间维度到空间维度,协程都比线程轻量。

Golang中的协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import (
"fmt"
"time"
)
func hello(msg string) {
fmt.Println("Hello" + msg)
}
func main() {
//在新的协程中执行hello方法
go hello("World")
fmt.Println("Run in main")
//等待100毫秒让协程执行结束
time.Sleep(100 * time.Millisecond)
}

从示例代码可见,要让hello()方法在一个新的协程中执行,只需要go hello(“World”)这一行代码就搞定了。

利用协程能够很好的实现Thread-Per-Message中介绍的Thread-Per-Message模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import (
"log"
"net"
)
func main() {
//监听本地9090端口
socket, err := net.Listen("tcp", "127.0.0.1:9090")
if err != nil {
log.Panicln(err)
}
defer socket.Close()
for {
//处理连接请求
conn, err := socket.Accept()
if err != nil {
log.Panicln(err)
}
//处理已经成功建立连接的请求
go handleRequest(conn)
}
}
//处理已经成功建立连接的请求
func handleRequest(conn net.Conn) {
defer conn.Close()
for {
buf := make([]byte, 1024)
//读取请求数据
size, err := conn.Read(buf)
if err != nil {
return
}
//回写相应数据
conn.Write(buf[:size])
}
}

示例代码用Golang实现echo程序的服务端,使用Thread-Per-Message模式,为每个成功建立连接的socket分配一个协程。

利用协程实现同步

在Java里使用多线程并发的处理I/O,基本上用的都是异步非阻塞模型,这种模型的异步主要是靠注册回调函数实现的,不能使用同步处理。同步意味着等待,线程等待本质上是一种严重的浪费。但是对于协程来说,等待的成本不高,基于协程实现同步非阻塞是一个可行的方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 创建socket
local sock = ngx.socket.tcp()
-- 设置socket超时时间
sock:settimeouts(connect_timeout, send_timeout, read_timeout)

-- 连接到目标地址
local ok, err = sock:connect(host, port)
if not ok then
-- 省略异常处理
end

-- 发送请求
local bytes, err = sock:send(request_data)
if not bytes then
-- 省略异常处理
end

-- 读取响应
local line, err = sock:receive()
if err then
-- 省略异常处理
end

-- 关闭socket
sock:close()
--处理读取到的数据line
handle(line)

OpenResty里实现cosocket是一种同步非阻塞方案,借助cosocket可以用线性的思维模式来编写非阻塞的程序(线性的思维模式反映到编程世界里就是同步,异步的思维模式不宜理解)。
示例代码是用cosocket实现的socket程序的客户端,建立连接、发送请求、读取响应所有的操作都是同步的,由于cosocket本身是非阻塞的,所以这些操作虽然是同步的,但是并不会阻塞。

结构化并发编程

golang中的go语句让协程用起来十分简单。Golang中的go语句只是一种快速创建协程的方法而已。但是goto语句会让程序变得混乱-代码的书写顺序和执行顺序不一致,干扰对代码的理解。

艾薇格·迪克斯切(Edsger Dijkstra)首先发现goto语句是毒药,同时提出了结构化程序设计:在结构化程序设计中,可以使用三种基本控制结构来代替goto-顺序结构、选择结构、循环结构

这三种基本的控制结构奠定了高级语言的基础,它们的入口和出口只有一个,意味着它们是可组合的,而且组合起来一定是线程的,整体来看,代码的书写顺序和执行顺序也是一致的。

开启新的线程(或协程)异步执行这种做法比较粗糙,违背了结构化程序设计。当开启一个新的线程时,程序会并行的出现两个分支,主线程是一个分支,子线程一个分支,这两个分支很多情况下都是天各一方、永不相见。而结构化的程序,可以有分支,但是最终一定要汇聚,不能有多个出口,因为只有这样它们组合起来才是线性的。

总结

计算机里很多面向开发人员的技术,大多数都是在解决一个问题:易用性。协程作为一项并发编程技术,本质上也不过是解决并发工具的易用性问题而已。对于易用性,最重要的是要适应大家的思维模式-用大众的、普通的思维模式写易读的代码,而不是炫技。