Skip to content
FutureBlitz

    Install

    Concurrency, 5 of 8

    Sendable and Shared Class

    Know which values can cross threads, and make a Class whose objects every thread can share.

    What you’ll build

    Three jobs that run as tasks and report to one progress tracker that they all share. The jobs are Structs, which copy safely to another thread; the tracker is a Shared Class, whose objects every thread can use at once. On the way, you’ll see what the compiler says about data that can’t cross.

    What’s Sendable

    A value is Sendable when it’s safe to use from another thread. The compiler works it out from the type, so no type has to say so:

    • numbers, Bools, Chars, Enums, and Strings are;
    • Structs, tuples, and fixed arrays are when everything in them is, since they’re copied;
    • Channels, Atomics, Mutexes, and Shared Classes’ objects are, being built for sharing;
    • Callbacks and Sendable Funcs are, but a Ptr or a CString isn’t, since nothing keeps what it points at for another thread;
    • an ordinary Class’s object isn’t, and neither is an array, since they’re shared by reference, and their reference counts aren’t made for threads. A Parallel For shares an outside array’s elements on its own terms, as Parallel For shows.

    Everything that goes to another thread is checked: what a Spawn’s work captures, what a Parallel For’s body uses, and what goes in a Channel. A type can also say Implements Sendable, to have the compiler check it where it’s declared. Here both go wrong:

    Import std.core
    
    Class Session
        Field Name: String
    End Class
    
    Struct Holder Implements Sendable
        Field Target: Session?
    End Struct
    
    Const sessions = Channel<Session>(1)
    Print(sessions.IsClosed())
    $ fb run main.fb
       Compiling main.fb
    main.fb:7:26: error: Holder isn't Sendable: its field 'Target' is Session?, and a Class's objects stay on the thread that made them, unless it's a Shared Class
      |
    7 | Struct Holder Implements Sendable
      |                          ^^^^^^^^
    main.fb:11:18: error: a Channel carries Sendable values to other threads, which Session isn't: a Class's objects stay on the thread that made them, unless it's a Shared Class
       |
    11 | Const sessions = Channel<Session>(1)
       |                  ^^^^^^^^^^^^^^^^
    error: couldn't build main.fb because of 2 errors

    A Shared Class

    A Shared Class is a Class whose objects can be used from any thread. Each of its fields is ReadOnly, set only in New, or an Atomic, or a Mutex, so no two threads can change one at once. Its objects count their references atomically, and its Methods can run on any thread.

    Import std.core
    Import std.collections
    
    Struct Job
        Field Name: String
        Field Size: Int
    End Struct
    
    Shared Class Progress
        ReadOnly Field Title: String
        Field Done: Atomic<Int>
        Field Log: Mutex<String>
    
        New(title: String)
            Title = title
        End New
    
        Method Finish(job: Job)
            Done.Add(1)
            Lock Log As text
                text += "{job.Name} "
            End Lock
        End Method
    End Class
    
    Function Work(job: Job, progress: Progress): Int
        Local sum = 0
        For i = 1 To job.Size
            sum += i % 7
        End For
        progress.Finish(job)
        Return sum
    End Function
    
    Const progress = Progress("nightly")
    Const jobs = [Job("a", 1000), Job("b", 2000), Job("c", 3000)]
    Local tasks: Task<Int>[] = []
    For job In jobs
        tasks.Add(Spawn(Function() => Work(job, progress)))
    End For
    
    Local total = 0
    For t In tasks
        total += t.Wait()
    End For
    Print("{progress.Title}: {progress.Done.Load()} jobs, total {total}")
    Output
    nightly: 3 jobs, total 18001

    Each task gets its own copy of a Job, a Struct of a String and an Int, and the same Progress object as the others. Finish counts with the Atomic and writes the job’s name into the log under its Mutex, from whichever thread calls it.

    A plain field in a Shared Class is an error:

    Import std.core
    
    Shared Class Tally
        Field Count: Int
    End Class
    
    Print(Tally().Count)
    $ fb run main.fb
       Compiling main.fb
    main.fb:4:11: error: 'Count' must be an Atomic, a Mutex, or ReadOnly, since threads share Tally's objects
      |
    4 |     Field Count: Int
      |           ^^^^^
    error: couldn't build main.fb because of 1 error

    Most data never leaves the thread that made it, and needs nothing. Data a task only reads, like its input, goes as a Struct or a String, copied or shared without locks. A result comes back through the Task’s Wait(), or a Channel. A Shared Class is for the few objects that several threads use at once, like a cache, a counter, or a log. A Shared Class extends only another, and only a Shared Class extends it.

    What you learned

    • Sendable values, which the compiler works out from their types, are the ones that can cross threads.
    • Structs of Sendable values are Sendable; ordinary Classes’ objects and arrays aren’t.
    • A Shared Class has only ReadOnly, Atomic, and Mutex fields, and any thread can use its objects.
    • Every crossing, to a task, a Parallel For, or a Channel, is checked at compile time.

    Report an issue with this page or with FutureBlitz on GitHub.