至简,Nim lang 并行编程

作者: Tulayang | 来源:发表于2015-08-01 21:35 被阅读1435次

    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
    ...
    

    相关文章

      网友评论

      本文标题:至简,Nim lang 并行编程

      本文链接:https://www.haomeiwen.com/subject/twkvqttx.html