Nimでロックと条件変数を提供する locks


はじめに

こんにちは。高校3年の樅山です。
今回は、Nimでロックと条件変数を提供する標準ライブラリ、locksを解説します。

この記事は、Nim Advent Calendar 2020 その2 の15日目です。

他の標準ライブラリを調べる👇

locks

前回まで、Nimではマルチスレッドプログラミングをthreadsモジュールとchannelsモジュールでサポートしていることを説明してきました。
複数のスレッドから同時にデータへ書き込みを行うことで、データ競合に陥ります。

ロック

locksモジュールで提供されているロックを用いることで、明示的にアクセス制御をすることができます。

データ競合により適切に処理されない例

以下は、共有変数numに複数のスレッドが更新を行うサンプルコードです。
全てが正しく加算されると、最終的にnumの値は675になりますが、データ競合によって625になっています。

データ競合が発生する
var
  num = 0
  workers: array[5, Thread[tuple[a, b: int]]]

proc task (interval: tuple[a, b: int]) {.thread.} =
  for i in interval.a .. interval.b:
    num += i
    echo num - i, " => ", num

for i in 0..4:
  createThread(workers[i], task, (i*10 , i*10+5))

workers.joinThreads()
echo num
stdout
-10 => 50
50 => 91
0 => 50
50 => 50
164 => 165
40 => 50
167 => 178
178 => 190
190 => 203
165 => 167
91 => 133
220 => 263
263 => 307
203 => 217
352 => 367
133 => 164
307 => 352
217 => 220
399 => 403
403 => 408
30 => 50
408 => 429
429 => 451
367 => 399
451 => 474
507 => 531
474 => 507
556 => 590
590 => 625
531 => 556
625

ロックを用いた実装

NimのロックはLock型のオブジェクトで、initLockプロシージャによって初期化できます。
guardプラグマを用いて変数に対してロックオブジェクトを適用します。

ロックされた変数はデフォルトでアクセスが禁止されます。
これにアクセスするには、ロックを取得する必要があります。逆に、取得した後はロックを解放してアクセスできないように戻す必要があります。
withLockプロシージャは、与えた変数のロックを取得してbodyに与えられた文の処理を行った後、ロックを解放します。

locks
import locks

var
  lock: Lock
  num {.guard: lock.} = 0
  workers: array[5, Thread[tuple[a, b: int]]]

proc task (interval: tuple[a, b: int]) {.thread.} =
  for i in interval.a .. interval.b:
    withLock lock:
      num += i
      echo num - i, " => ", num

lock.initLock

for i in 0..4:
  createThread(workers[i], task, (i*10 , i*10+5))

workers.joinThreads()
echo num
stdout
0 => 10
10 => 21
21 => 33
33 => 73
73 => 114
114 => 156
156 => 176
176 => 197
197 => 219
219 => 242
242 => 266
266 => 291
291 => 321
321 => 352
352 => 395
395 => 439
439 => 484
484 => 484
484 => 485
485 => 487
487 => 490
490 => 494
494 => 499
499 => 512
512 => 526
526 => 541
541 => 573
573 => 606
606 => 640
640 => 675
675

このように、データ競合が発生せずに正しく処理されていることがわかります。

ロックを取得する

withLockテンプレートを使用せず、ロックを取得するにはacquireプロシージャを利用します。

acquire
proc acquire(lock: var Lock) {.raises: [], tags: [].}

また、成功するかわからない場合にはtryAcquireプロシージャを呼び出します。成功した場合はtrueを、失敗した場合はfalseを返します。

tryAcquire
proc tryAcquire(lock: var Lock): bool {.raises: [], tags: [].}

ロックを解放する

withLockテンプレートを使用せず、ロックを解放するにはreleaseプロシージャを利用します。

release
proc release(lock: var Lock) {.raises: [], tags: [].}

条件変数

条件変数は、initCondで初期化することができます。

条件変数の初期化
proc initCond(cond: var Cond) {.inline, raises: [], tags: [].}

また、waitに条件変数とロックを与えることで、条件を満たすまで処理を待機できます。

条件を満たすまで待機
proc wait(cond: var Cond; lock: var Lock) {.inline, raises: [], tags: [].}

signalに条件変数を与えると、スレッドにシグナルを送信します。

シグナルの送信
proc signal(cond: var Cond) {.inline, raises: [], tags: [].}

参考文献