iyi

Standard library, 0.18.0

std/mutex

Mutex(T): a value two tasks or two threads may both change, kept behind a lock. SPEC.md III.4.4's synchronised type that owns its contents, and the escape the Share rule leaves for state that has to be shared and has to change: Mutex(Array(Int32)) may be captured by IyiThread.start's block although Array(Int32) may not.

import std/mutex to use it. Source: src/std/mutex.iyi at v0.18.0.

  import std/mutex::{Mutex}
  totals = Mutex.new { {} of String => Int32 }
  totals.lock { |t| t["seen"] = (t["seen"]? || 0) + 1 }.or_panic
  seen = totals.get { |t| t.size }.or_panic
  count = Mutex.new { 0 }
  count.update { |n| n + 1 }.or_panic

**Waiting for a change.** wait_for is the condition variable, with the condition and its action one step: its block runs with the lock held each time another task has had the lock, until it answers something other than nil, which wait_for answers. A consumer takes from a queue a producer fills with queue.wait_for { |q| q.empty? ? nil : q.shift }.

**Only a block reaches the value, and the compiler checks every block.** The value is made by the block Mutex.new runs, so nothing outside holds it, and is reached only inside the blocks lock, get and update run with the lock held. Each of those blocks may capture only Share values - a captured array would be within reach of the value and outside the lock at once - and what get answers must be Share, since it leaves the lock. A block that breaks either is refused where it is written, naming the variable or the answer (CleanupTransformer#check_share_boundary). What the check does not see is a method the block calls that stores its argument somewhere global; that is the same edge the thread check has.

**Waiting.** A task that finds the lock held waits without holding its thread hostage. If the holder is on another thread, it first spins a few turns, which is what a short critical section wants. Then, if its own thread has other work - runnable tasks, sleepers, tasks waiting on I/O - it yields to them, and after sixteen yields sleeps a millisecond at a time, so a holder parked on I/O costs the waiter no core. If its thread has nothing else to do and the holder is elsewhere, the thread sleeps in the kernel until the release: a futex on the lock's word on Linux, a semaphore on Windows; darwin, which has neither on the floor, sleeps the millisecond. A waiting task is cancellable, as every blocking call is (III.4.2): lock, get and update answer Cancelled then, and the block does not run.

**What is refused.** A task locking a Mutex it already holds would wait for itself, and panics saying so. A holder parked on the waiter's own thread with nothing left there to wake it is a deadlock, and panics saying that too. A panic inside the block releases the lock as it leaves (defer), so a task that dies does not take the lock with it.

The lock is not fair: a task arriving as it is released may take it ahead of one that waited. A Windows semaphore made for a contended Mutex is kept for the program's life.

Mutex(T)class

std/mutexiyi doc src/std/mutex.iyi
pub class Mutex(T)
  protected def self.build(value : T) : Mutex(T)
  # The block's answer, worked out with the lock held. It must be `Share`,
  # because it leaves the lock.
  def get(&block : (T -> R)) : R | Cancelled forall R
  # Runs the block with the value, the lock held; answers nil, or
  # `Cancelled` when the task was cancelled while it waited.
  def lock(&block : (T -> _)) : Nil | Cancelled
  # Whether some task holds it now: an answer that may be old by the time
  # it is read, for a report rather than a decision.
  def locked? : Bool
  # The value is the block's: made here, so nothing outside holds it. The
  # constructor that takes a value is private for that reason.
  def self.new(&block : (-> U)) : Mutex(U) forall U
  protected def self.task_id : UInt64
  # Whether this thread has anything to run besides the waiting task:
  # tasks ready to run, tasks asleep, tasks waiting on I/O.
  protected def self.thread_busy? : Bool
  protected def self.thread_id : UInt64
  # The value replaced by the block's answer, with the lock held: what a
  # number or any other value type wants, which a block cannot change in
  # place.
  def update(&block : (T -> T)) : Nil | Cancelled
  # Runs the block with the lock held each time the value changes, until it
  # answers something other than nil, and answers that: a condition and
  # what is done once it holds, in one step no other task can come between.
  #
  #     queue = Mutex.new { [] of Int32 }
  #     job = queue.wait_for { |q| q.empty? ? nil : q.shift }.or_panic
  #
  # A change is any lock, get, update or wait_for of this Mutex by another
  # task; between them the waiter holds no lock, and what it does while it
  # waits is `lock`'s: yields, then sleeps a millisecond at a time, and on
  # Linux a thread with nothing else to run sleeps in the kernel until the
  # next release. Cancellable, as `lock` is. A wait nothing could end - one
  # thread, and nothing else on it to run - panics saying so.
  def wait_for(&block : (T -> R | ::Nil)) : R | Cancelled forall R
end