Malebolgia

repository·master·Indexed 41 days ago

https://github.com/araq/malebolgia

A lightweight Nim library for simplified concurrent and parallel programming. It implements a structured concurrency model using Master objects, spawn, and awaitAll, focusing on bounded memory consumption and low energy usage for environments including embedded devices.

Tokens
1.7K
Snippets
7
Records
8
Agent score
50%

What's inside Malebolgia

  1. Handle exceptions in spawned tasks

    master
    If a task spawned via m.spawn raises an exception, the Master object captures it. The exception is rethrown by the Master after the m.awaitAll block completes. If multiple tasks raise exceptions, only the first exception encountered is preserved and rethrown.
  2. Parallel execution with spawn and awaitAll

    master

    Malebolgia uses a structured concurrency model where tasks are coordinated by a Master object. You can distribute workloads by using m.spawn within an m.awaitAll block. The awaitAll block acts as a barrier, ensuring all spawned subtasks complete before proceeding. Unlike other libraries, Malebolgia does not require FlowVar for result collection; you can capture the return value of a spawned task directly into a local variable using the -> syntax.

    import malebolgia
    
    proc dfs(depth, breadth: int): int {.gcsafe.} =
      if depth == 0: return 1
      var sums = newSeq[int](breadth)
      var m = createMaster()
    
      m.awaitAll:
        for i in 0 ..< breadth:
          m.spawn dfs(depth - 1, breadth) -> sums[i]
    
      result = 0
      for i in 0 ..< breadth:
        result += sums[i]
    
    let answer = dfs(8, 8)
    echo answer
  3. Tune performance with ThreadPoolSize and FixedChanSize

    master

    Malebolgia's efficiency can be tuned via two compiler flags:

    1. -d:ThreadPoolSize: Set this to the number of CPU cores for compute-bound tasks, or higher for IO-bound programs.
    2. -d:FixedChanSize: Controls the fixed size of the communication channels. The default is usually sufficient.

    Example compilation command: nim c -d:ThreadPoolSize=8 -d:FixedChanSize=16 filename.nim

    nim c -d:ThreadPoolSize=8 -d:FixedChanSize=16 dfs.nim
  4. Workaround var T parameters using pointers and TicketLock

    master

    Since var T parameters are not currently supported in spawned tasks, you can pass the address of a variable declared outside the awaitAll block using addr. Because this involves shared mutable state, you must protect the access with a lock. malebolgia/ticketlocks provides a TicketLock which is convenient as it does not require manual deinitLock calls.

    import std / [strutils, tables]
    import malebolgia
    import malebolgia / ticketlocks
    
    proc countWords(filename: string; results: ptr CountTable[string]; L: ptr TicketLock) =
      for w in splitWhitespace(readFile(filename)):
        withLock L[]:
          results[].inc w
    
    proc main() =
      var m = createMaster()
      var results = initCountTable[string]()
      var L = initTicketLock() 
    
      m.awaitAll:
        m.spawn countWords("temp.nim", addr results, addr L)
        m.spawn countWords("tester.nim", addr results, addr L)
    
      results.sort()
      echo results
  5. Cancel tasks using Master.cancel()

    master

    You can manually trigger task cancellation by calling m.cancel() on the Master object. This is useful for stopping execution based on a condition (e.g., after a certain number of iterations).

    import malebolgia
    
    var m = createMaster()
    m.awaitAll:
      m.spawn foo()
      for i in 0..<1000:
        m.spawn bar($i)
        if i == 300:
          m.cancel()
  6. Pass task coordination via MasterHandle

    master

    A Master object cannot be passed directly to subroutines. To allow subroutines to spawn their own tasks while maintaining structured concurrency, use getHandle(m) to obtain a MasterHandle. A MasterHandle supports spawn and cancel, but does not support the awaitAll operation, preventing the accidental breaking of the structured concurrency hierarchy.

    import malebolgia
    
    proc g(m: MasterHandle; i: int) {.gcsafe.} =
      if i < 800:
        m.spawn g(m, i+1)
    
    proc main =
      var m = createMaster()
      m.awaitAll:
        m.spawn g(getHandle(m), 0)
    
    main()
  7. Use Locker[T] for shared mutable data

    master

    To share and mutate data structures across spawned tasks, use the Locker[T] type from malebolgia/lockers. You can wrap a data structure (like a CountTable) and use the lock keyword to safely access and mutate it. Use unprotected to access the data without a lock once the parallel section is complete.

    import std / [strutils, tables]
    import malebolgia
    import malebolgia / lockers
    
    proc countWords(filename: string; results: Locker[CountTable[string]]) =
      for w in splitWhitespace(readFile(filename)):
        lock results as r:
          r.inc w
    
    proc main() =
      var m = createMaster()
      var results = initLocker initCountTable[string]()
    
      m.awaitAll:
        m.spawn countWords("README.md", results)
        m.spawn countWords("malebolgia.nimble", results)
    
      unprotected results as r:
        r.sort()
        echo r
  8. Set timeouts for a Master

    master

    When calling createMaster, you can provide an optional timeout parameter (using initDuration). This timeout applies to all tasks belonging to that master. Long-running tasks can periodically check m.cancelled to determine if they should stop execution gracefully.

    import std / times
    import malebolgia
    
    var m = createMaster(initDuration(milliseconds=500))
    m.awaitAll:
      for i in 0..<1000:
        m.spawn bar($i)
      if not m.cancelled:
        for i in 1000..<2000:
          m.spawn bar($i)