守卫和锁

Nim 提供了诸如锁、原子性内部函数或条件变量这样的常见底层并发机制。

Nim 通过附带编译指示,显著地提高了这些功能的安全性:

  1. 引入 guard 注解,以防止数据竞争。
  2. 每次访问受保护的内存位置,都需要在适当的 locks 语句中进行。

守卫和锁块

受保护的全局变量

对象字段和全局变量都可以使用 guard 编译指令进行标注:

  1. import std/locks
  2. var glock: Lock
  3. var gdata {.guard: glock.}: int

然后,编译器会确保每次访问 gdata 都在 locks 块中:

  1. proc invalid =
  2. # invalid: unguarded access:
  3. echo gdata
  4. proc valid =
  5. # valid access:
  6. {.locks: [glock].}:
  7. echo gdata

为了能够方便地初始化,始终允许在顶层访问 gdata。 假定 (但不强制)所有顶层语句都在发生并发操作之前执行。

我们故意让 locks 块看起来很丑,因为它没有运行时的语意,也不应该被直接使用! 它应该只在模板里出现,再由模板实现运行时的加锁操作:

  1. template lock(a: Lock; body: untyped) =
  2. pthread_mutex_lock(a)
  3. {.locks: [a].}:
  4. try:
  5. body
  6. finally:
  7. pthread_mutex_unlock(a)

守卫不需要属于某种特定类型。它足够灵活,可以对低级无锁机制建模:

  1. var dummyLock {.compileTime.}: int
  2. var atomicCounter {.guard: dummyLock.}: int
  3. template atomicRead(x): untyped =
  4. {.locks: [dummyLock].}:
  5. memoryReadBarrier()
  6. x
  7. echo atomicRead(atomicCounter)

locks 指示接受一个锁表达式列表 locks: [a, b, …] ,以支持 多锁 语句。

保护常规地址

guard 注解也可以用于保护对象中的字段。这时,需要用同一个对象的另一个字段或者一个全局变量作为守卫。

由于对象可以驻留在堆上或栈上,这就大大地增强了语言的表达能力:

  1. import std/locks
  2. type
  3. ProtectedCounter = object
  4. v {.guard: L.}: int
  5. L: Lock
  6. proc incCounters(counters: var openArray[ProtectedCounter]) =
  7. for i in 0..counters.high:
  8. lock counters[i].L:
  9. inc counters[i].v

有 x.L 的守卫就可以访问字段 x.v。模板展开后得到:

  1. proc incCounters(counters: var openArray[ProtectedCounter]) =
  2. for i in 0..counters.high:
  3. pthread_mutex_lock(counters[i].L)
  4. {.locks: [counters[i].L].}:
  5. try:
  6. inc counters[i].v
  7. finally:
  8. pthread_mutex_unlock(counters[i].L)

编译器会分析检查 counters[i].L 是否是用于受保护位置 counters[i].v 的那个锁。 因为这个分析能够处理像 obj.field[i].fieldB[j] 这样的路径,所以我们叫它 path analysis “路径分析”。

路径分析 目前不健全 ,但也不是一点儿用都没有。如果两条路径在语法上相同,则认为是等价的。

这意味着下面的代码(目前)可以编译通过,虽然真不应该如此:

  1. {.locks: [a[i].L].}:
  2. inc i
  3. access a[i].v