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())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}")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)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 Classhas 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.
The rules are in the language reference.
Report an issue with this page or with FutureBlitz on GitHub.