Nim 语言中,提供低阶的多线程接口,以及一个高阶的线程池 threadpool 。使用低阶接口,你可以完全用 C 的函数编写多线程,或者使用 Nim 实现的相同性质的 threads 模块(内置系统模块)。高阶接口,实现的非常优雅,封装了操作系统中互斥锁等待信号变量的线程池模式。
这样组合的结果是,在 Nim 中编写多线程非常优雅和可读。
你看了之后,也可能会说 Erlang 语言的并发也不错。我要说的是,Erlang 仅仅是针对通信网络的语言。而 Nim 是针对操作系统和上层网络通信的语言。Nim 拥有 C 语言的计算级别(其编译器优化甚至可以比 C 源码更快),并且可以随时轻易的切换到 C 操作。Nim 对数据计算拥有更有力的速度。
现在,我们使用 Nim 来实现一个典型的多线程事务例子:
多个客户端请求,将账户 x 的金额减少 1,并把账户 y 的金额增加 1 。
首先,这涉及到并发。多个账户,使用多个线程读取 read() 他们的信息,然后需要把多线程锁住,只开一个线程“窗口”,进入事务模式: 每次只有一个线程可以操作减少增加金额。然后把完成的线程重新开放,作出响应 response() 告诉客户端完成任务。
线程模块
Nim 有一个 System 模块,是自动加载的,里面包含了大量的工具,包括 threads, channels(线程通信模块)。这个 System 非常类似 JavaScript 中的 global,比如你在 JavaScript 中使用 parseInt(1.2) 不需要导入模块,也没有命名空间。Nim 的 System 跟此很相似。
实现这个任务需要 threadpool(线程池模块),locks(锁模块)。locks 就是操作系统的互斥量(pthread_mutex_t)和条件变量(pthread_cond_t)的对应实现。此外,Nim 为锁增加了数据安全的检查(发生在编译期,不会影响运行期的效能),每个被锁标记的数据,在访问时都要有一个编译注释:
var
xlock: TLock
x {.guard.} = 100
{.locks: [xlock].}: x = x - 1
如果没有 {.locks: [xlock].},会触发一个编译期错误。这能有效提醒多线程的程序员,这个变量是锁变量,需要更多的“照顾”。
threadpool 提供了非常棒的线程池,默认是 256 个启动线程,这对大部分业务都是足够得了(实际上,更多的线程可能性能反而更低)。
使用线程池,你不需要手动创建、脱离线程。只需要使用 spawn() 函数,就可以从线程池中领取一个线程来运行任务。所以,你更多的是关注任务如何运作,而不再是线程如何领取并按照你的意愿运行。
proc work() {.thread.} =
read()
update()
response()
spawn work() # 领取一个线程,运行 work 函数
这跟事件驱动模式很相似,先把资源加载进一个池中管理,然后通过某些类似钩子的东西,领取一个资源,运行加入的函数。
实现并行
首先,为了模拟一个时间比较长的操作,使用一个 longtime() 来延长计算时间。这个函数什么也不做,只是从0数到200000000。根据估算,大概是3秒种:
proc longtime() =
for i in 0..200_000_000: discard
然后是读取客户端的数据,我们使用一个 read() 来模拟:
proc read() =
echo "--- Read begin"
longtime()
echo ">>> Read finish"
响应客户端,我们使用一个 response() 来模拟:
proc response() =
echo "--- Response begin"
longtime()
echo ">>> Response finish"
然后是最重要的事务函数,这个函数需要锁,我们通过模板宏来封装一个锁语句:
template lock(x: TLock, y: TLock, body: stmt) =
acquire(x)
acquire(y)
{.locks: [x, y].}: body
release(y)
release(x)
然后使用 lock 语句来启动事务:
proc update() =
# 启动一个事务
lock(xlock, ylock):
# 把账户 x 减少 1
echo "--- Decrease begin with x " & $x
longtime()
dec(x, 1)
echo ">>> Decrease finish with x " & $x
# 把账户 y 增加 1
echo "--- Increase begin with y " & $y
longtime()
inc(y, 1)
echo ">>> Increase finish with y " & $y
最后,工作线程的过程非常简单,就是上面的3个任务的调用:
proc work() {.thread.} =
read()
update()
response()
That's all。源码如下:
import threadpool, locks
var
xlock: TLock
ylock: TLock
x {.guard: xlock.} = 100 # 账户 x 的金额 100
y {.guard: ylock.} = 100 # 账户 y 的金额 100
proc longtime() =
for i in 0..200_000_000: discard
proc read() =
echo "--- Read begin"
longtime()
echo ">>> Read finish"
proc response() =
echo "--- Response begin"
longtime()
echo ">>> Response finish"
template lock(x: TLock, y: TLock, body: stmt) =
acquire(x)
acquire(y)
{.locks: [x, y].}: body
release(y)
release(x)
proc update() =
# 启动一个事务
lock(xlock, ylock):
# 把账户 x 减少 1
echo "--- Decrease begin with x " & $x
longtime()
dec(x, 1)
echo ">>> Decrease finish with x " & $x
# 把账户 y 增加 1
echo "--- Increase begin with y " & $y
longtime()
inc(y, 1)
echo ">>> Increase finish with y " & $y
proc work() {.thread.} =
read()
update()
response()
initLock(xlock)
initLock(ylock)
while true:
longtime()
longtime()
for i in 0..2:
spawn work()
sync()
deinitLock(xlock)
deinitLock(ylock)
运行代码
$ nim c -r --threads:on test.nim
会输出
--- Read begin
--- Read begin
--- Read begin
>>> Read finish
--- Decrease begin with x 100
>>> Read finish
>>> Read finish
>>> Decrease finish with x 99
--- Increase begin with y 100
--- Read begin
--- Read begin
>>> Read finish
>>> Increase finish with y 101
--- Response begin
--- Decrease begin with x 99
>>> Read finish
>>> Response finish
--- Read begin
>>> Decrease finish with x 98
--- Increase begin with y 101
>>> Increase finish with y 102
--- Response begin
--- Decrease begin with x 98
>>> Read finish
--- Read begin
>>> Decrease finish with x 97
--- Increase begin with y 102
>>> Response finish
--- Read begin
>>> Read finish
...