nim-chronos/asyncdispatch2/asyncsync.nim

331 lines
10 KiB
Nim

#
# Asyncdispatch2 synchronization primitives
#
# (c) Coprygith 2018 Eugene Kabanov
# (c) Copyright 2018 Status Research & Development GmbH
#
# Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
## This module implements some core synchronization primitives, which
## `asyncdispatch` is really lacking.
import asyncloop, deques
type
AsyncLock* = ref object of RootRef
## A primitive lock is a synchronization primitive that is not owned by
## a particular coroutine when locked. A primitive lock is in one of two
## states, ``locked`` or ``unlocked``.
##
## When more than one coroutine is blocked in ``acquire()`` waiting for
## the state to turn to unlocked, only one coroutine proceeds when a
## ``release()`` call resets the state to unlocked; first coroutine which
## is blocked in ``acquire()`` is being processed.
locked: bool
waiters: Deque[Future[void]]
AsyncEvent* = ref object of RootRef
## A primitive event object.
##
## An event manages a flag that can be set to `true` with the ``fire()``
## procedure and reset to `false` with the ``clear()`` procedure.
## The ``wait()`` coroutine blocks until the flag is `false`.
##
## If more than one coroutine blocked in ``wait()`` waiting for event
## state to be signaled, when event get fired, then all coroutines
## continue proceeds in order, they have entered waiting state.
flag: bool
waiters: Deque[Future[void]]
AsyncQueue*[T] = ref object of RootRef
## A queue, useful for coordinating producer and consumer coroutines.
##
## If ``maxsize`` is less than or equal to zero, the queue size is
## infinite. If it is an integer greater than ``0``, then "await put()"
## will block when the queue reaches ``maxsize``, until an item is
## removed by "await get()".
getters: Deque[Future[void]]
putters: Deque[Future[void]]
queue: Deque[T]
maxsize: int
AsyncQueueEmptyError* = object of Exception
## ``AsyncQueue`` is empty.
AsyncQueueFullError* = object of Exception
## ``AsyncQueue`` is full.
AsyncLockError* = object of Exception
## ``AsyncLock`` is either locked or unlocked.
proc newAsyncLock*(): AsyncLock =
## Creates new asynchronous lock ``AsyncLock``.
##
## Lock is created in the unlocked state. When the state is unlocked,
## ``acquire()`` changes the state to locked and returns immediately.
## When the state is locked, ``acquire()`` blocks until a call to
## ``release()`` in another coroutine changes it to unlocked.
##
## The ``release()`` procedure changes the state to unlocked and returns
## immediately.
# Workaround for callSoon() not worked correctly before
# getGlobalDispatcher() call.
discard getGlobalDispatcher()
result = new AsyncLock
result.waiters = initDeque[Future[void]]()
result.locked = false
proc acquire*(lock: AsyncLock) {.async.} =
## Acquire a lock ``lock``.
##
## This procedure blocks until the lock ``lock`` is unlocked, then sets it
## to locked and returns.
if not lock.locked:
lock.locked = true
else:
var w = newFuture[void]("asynclock.acquire")
lock.waiters.addLast(w)
yield w
lock.locked = true
proc own*(lock: AsyncLock) =
## Acquire a lock ``lock``.
##
## This procedure not blocks, if ``lock`` is locked, then ``AsyncLockError``
## exception would be raised.
if lock.locked:
raise newException(AsyncLockError, "AsyncLock is already acquired!")
lock.locked = true
proc locked*(lock: AsyncLock): bool =
## Return `true` if the lock ``lock`` is acquired, `false` otherwise.
result = lock.locked
proc release*(lock: AsyncLock) =
## Release a lock ``lock``.
##
## When the ``lock`` is locked, reset it to unlocked, and return. If any
## other coroutines are blocked waiting for the lock to become unlocked,
## allow exactly one of them to proceed.
var w: Future[void]
proc wakeup(udata: pointer) {.gcsafe.} = w.complete()
if lock.locked:
lock.locked = false
while len(lock.waiters) > 0:
w = lock.waiters.popFirst()
if not w.finished:
callSoon(wakeup)
break
else:
raise newException(AsyncLockError, "AsyncLock is not acquired!")
proc newAsyncEvent*(): AsyncEvent =
## Creates new asyncronous event ``AsyncEvent``.
##
## An event manages a flag that can be set to `true` with the `fire()`
## procedure and reset to `false` with the `clear()` procedure.
## The `wait()` procedure blocks until the flag is `true`. The flag is
## initially `false`.
# Workaround for callSoon() not worked correctly before
# getGlobalDispatcher() call.
discard getGlobalDispatcher()
result = new AsyncEvent
result.waiters = initDeque[Future[void]]()
result.flag = false
proc wait*(event: AsyncEvent) {.async.} =
## Block until the internal flag of ``event`` is `true`.
## If the internal flag is `true` on entry, return immediately. Otherwise,
## block until another task calls `fire()` to set the flag to `true`,
## then return.
if event.flag:
discard
else:
var w = newFuture[void]("asyncevent.wait")
event.waiters.addLast(w)
yield w
proc fire*(event: AsyncEvent) =
## Set the internal flag of ``event`` to `true`. All tasks waiting for it
## to become `true` are awakened. Task that call `wait()` once the flag is
## `true` will not block at all.
proc wakeupAll(udata: pointer) {.gcsafe.} =
if len(event.waiters) > 0:
var w = event.waiters.popFirst()
if not w.finished:
w.complete()
callSoon(wakeupAll)
if not event.flag:
event.flag = true
callSoon(wakeupAll)
proc clear*(event: AsyncEvent) =
## Reset the internal flag of ``event`` to `false`. Subsequently, tasks
## calling `wait()` will block until `fire()` is called to set the internal
## flag to `true` again.
event.flag = false
proc isSet*(event: AsyncEvent): bool =
## Return `true` if and only if the internal flag of ``event`` is `true`.
result = event.flag
proc newAsyncQueue*[T](maxsize: int = 0): AsyncQueue[T] =
## Creates a new asynchronous queue ``AsyncQueue``.
# Workaround for callSoon() not worked correctly before
# getGlobalDispatcher() call.
discard getGlobalDispatcher()
result = new AsyncQueue[T]
result.getters = initDeque[Future[void]]()
result.putters = initDeque[Future[void]]()
result.queue = initDeque[T]()
result.maxsize = maxsize
proc full*[T](aq: AsyncQueue[T]): bool {.inline.} =
## Return ``true`` if there are ``maxsize`` items in the queue.
##
## Note: If the ``aq`` was initialized with ``maxsize = 0`` (default),
## then ``full()`` is never ``true``.
if aq.maxsize <= 0:
result = false
else:
result = len(aq.queue) >= aq.maxsize
proc empty*[T](aq: AsyncQueue[T]): bool {.inline.} =
## Return ``true`` if the queue is empty, ``false`` otherwise.
result = (len(aq.queue) == 0)
proc putNoWait*[T](aq: AsyncQueue[T], item: T) =
## Put an item into the queue ``aq`` immediately.
##
## If queue ``aq`` is full, then ``AsyncQueueFullError`` exception raised
var w: Future[void]
proc wakeup(udata: pointer) {.gcsafe.} = w.complete()
if aq.full():
raise newException(AsyncQueueFullError, "AsyncQueue is full!")
aq.queue.addLast(item)
while len(aq.getters) > 0:
w = aq.getters.popFirst()
if not w.finished:
callSoon(wakeup)
proc getNoWait*[T](aq: AsyncQueue[T]): T =
## Remove and return ``item`` from the queue immediately.
##
## If queue ``aq`` is empty, then ``AsyncQueueEmptyError`` exception raised.
var w: Future[void]
proc wakeup(udata: pointer) {.gcsafe.} = w.complete()
if aq.empty():
raise newException(AsyncQueueEmptyError, "AsyncQueue is empty!")
result = aq.queue.popFirst()
while len(aq.putters) > 0:
w = aq.putters.popFirst()
if not w.finished:
callSoon(wakeup)
proc put*[T](aq: AsyncQueue[T], item: T) {.async.} =
## Put an ``item`` into the queue ``aq``. If the queue is full, wait until
## a free slot is available before adding item.
while aq.full():
var putter = newFuture[void]("asyncqueue.putter")
aq.putters.addLast(putter)
yield putter
aq.putNoWait(item)
proc get*[T](aq: AsyncQueue[T]): Future[T] {.async.} =
## Remove and return an item from the queue ``aq``.
##
## If queue is empty, wait until an item is available.
while aq.empty():
var getter = newFuture[void]("asyncqueue.getter")
aq.getters.addLast(getter)
yield getter
result = aq.getNoWait()
proc len*[T](aq: AsyncQueue[T]): int {.inline.} =
## Return the number of elements in ``aq``.
result = len(aq.queue)
proc size*[T](aq: AsyncQueue[T]): int {.inline.} =
## Return the maximum number of elements in ``aq``.
result = len(aq.maxsize)
when isMainModule:
# Locks test
block:
var test = ""
var lock = newAsyncLock()
proc testLock(n: int, lock: AsyncLock) {.async.} =
await lock.acquire()
test = test & $n
lock.release()
lock.own()
asyncCheck testLock(0, lock)
asyncCheck testLock(1, lock)
asyncCheck testLock(2, lock)
asyncCheck testLock(3, lock)
asyncCheck testLock(4, lock)
asyncCheck testLock(5, lock)
asyncCheck testLock(6, lock)
asyncCheck testLock(7, lock)
asyncCheck testLock(8, lock)
asyncCheck testLock(9, lock)
lock.release()
poll()
doAssert(test == "0123456789")
# Events test
block:
var test = ""
var event = newAsyncEvent()
proc testEvent(n: int, ev: AsyncEvent) {.async.} =
await ev.wait()
test = test & $n
event.clear()
asyncCheck testEvent(0, event)
asyncCheck testEvent(1, event)
asyncCheck testEvent(2, event)
asyncCheck testEvent(3, event)
asyncCheck testEvent(4, event)
asyncCheck testEvent(5, event)
asyncCheck testEvent(6, event)
asyncCheck testEvent(7, event)
asyncCheck testEvent(8, event)
asyncCheck testEvent(9, event)
event.fire()
poll()
doAssert(test == "0123456789")
# Queues test
block:
const queueSize = 10
const testsCount = 1000
var test = 0
proc task1(aq: AsyncQueue[int]) {.async.} =
for i in 1..(testsCount - 1):
var item = await aq.get()
test -= item
proc task2(aq: AsyncQueue[int]) {.async.} =
for i in 1..testsCount:
await aq.put(i)
test += i
var queue = newAsyncQueue[int](queueSize)
discard task1(queue) or task2(queue)
poll()
doAssert(test == testsCount)