至简,Nim lang 并行编程

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
...
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容