blob: dfff71999b7689d18fdbf6f905e00b2eedd60dee [file] [log] [blame]
:orphan:
.. ConcurrencyModel:
*Note*: This document is **not an accepted Swift proposal**. It does not describe Swift's concurrency mechanisms as they currently exist, nor is it a roadmap for the addition of concurrency mechanisms in future versions of Swift.
Swift Thread Safety
===================
This document describes the Swift thread-safety layer. It includes the
motivation for allowing users to write thread-safe code and a concrete proposal
for changes in the language and the standard library. This is a proposal and
not a plan of record.
The low-level thread-safety layer allows third-party developers to build
different kinds of high-level safe concurrency solutions (such as Actors,
Coroutines, and Async-Await) in a library. This document describes three
different high-level concurrency solutions to demonstrate the completeness and
efficacy of the thread-safe layer. Designing an official high-level concurrency
model for Swift is outside the scope of this proposal.
Motivation and Requirements
---------------------------
Multi-core processors are ubiquitous and most modern programming languages, such
as Go, Rust, Java, C#, D, Erlang, and C++, have some kind of support for
concurrent, parallel or multi-threaded programming. Swift is a safe programming
language that protects the user from bugs such as integer overflow and memory
corruption by eliminating undefined behavior and by verifying some aspects of
the program's correctness at runtime. Multi-threaded programs in Swift should
not break the safety guarantees of the language.
Swift Memory Model
------------------
This section describes the guarantees that unsupervised Swift provides when
writing multi-threaded code and why the minimal guarantees of atomicity that
Java provide cannot be implemented in Swift.
Let's start by looking at reference counting operations. Swift objects include a
reference-count field that holds the number of references that point to that
object. This field is modified using atomic operations. Atomic operations
ensure that the reference count field is not corrupted by data races. However,
using atomic operations is not enough to ensure thread safety. Consider the
multi-threaded program below.
.. code-block:: swift
import Foundation
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
let queue = dispatch_get_global_queue(priority, 0)
class Bird {}
var single = Bird()
dispatch_async(queue) {
while true { single = Bird() }
}
while true { single = Bird() }
This program crashes very quickly when it tries to deallocate an already
deallocated class instance. To understand the bug try to imagine two threads
executing the SIL code below in lockstep. After they both load the same value
they both try to release the object. One thread succeeds and deallocates the
object while another thread attempts to read the memory of a deallocated
object:
.. code-block:: sil
%10 = global_addr @singleton : $*Bird
bb:
%49 = alloc_ref $Bird
%51 = load %10 : $*Bird
store %49 to %10 : $*Bird
strong_release %51 : $Bird
br bb
Next, we'll look into the problem of sliced values. Intuitively, it is easy to
see why sharing memory between two threads could lead to catastrophic bugs.
Consider the program below:
.. code-block:: none
Thread #1: Thread #2:
A.first = "John" A.first = "Paul"
A.last = "Lennon" A.last = "McCartney"
If thread #1 goes to sleep after executing the first statement and resumes
execution after thread #2 runs, then the value of the struct would be "Paul
Lennon", which is not a valid value. The example above demonstrates how data
races can introduce an inconsistent state.
The Java memory model ensures that pointers and primitive types, such as ints
and floats, are never sliced, even when data races occur. It would be nice if
Swift had a similar guarantee. Intuitively we would want all struct or class
members of primitive types to be aligned to ensure atomic access to the field.
However, this is not possible in Swift. Consider the Swift code below::
enum Fruit {
case Apple(Int64),
case Grape(MyClass)
}
The size of the struct above is 65 bits, which means that even on 64bit
processors the tag of the enum can't be updated at the same time as the payload.
In some race conditions we could accidentally interpret the payload of the
struct as pointer using the value stored into the integer.
To summarize, Swift, just like C++, does not make any guarantees about
unsynchronized memory, and the semantic of programs with races is undefined. When
race conditions occur pointers and primitive data types could be sliced, enums
may contain the wrong tag, protocols may refer to invalid dispatch tables,
references may point to deallocated objects.
Achieving thread safety
-----------------------
This section describes a set of rules that ensure thread safety in programs that
embrace them despite the inherit lack of thread safety in general multi-threaded
Swift code.
Safe concurrency is commonly implemented by eliminating shared mutable memory.
Go, Erlang and Rust ensure some level of program safety by providing mechanisms
for eliminating shared mutable memory. Erlang provides the strongest model by
ensuring complete logical address space separation between threads. Rust
provides powerful abstraction and rely on the type system to ensure that objects
are owned by a single entity. Go provides channels that allow threads to
communicate instead of sharing memory (but allows user to pass pointers in
channels!). It is not necessary to disallow all sharing of mutable data between
threads and it is not necessary to enforce a hermetic separation between the
address spaces. It is very useful to be able to share large data structures
without copying them around. Mutable data can be shared between threads as long
as the access to the data is synchronized and some program properties are
verified by the compiler. In Swift thread safety is implemented by preventing
threads from sharing mutable memory.
Proposal
========
In Swift, new threads are created in a new memory enclave that is separate from
the parent thread. Values can be copied in and out of the new thread context,
but the child thread must never obtain a reference that points to the outside
world. Non-reentrant code needs to be explicitly marked as such. Swift enforces
these rules statically. The rest of this section describes how Swift ensures
safety and deals with global variables and unsafe code.
The three basic elements of thread safety
-----------------------------------------
The Swift language has three features that allow it to ensure thread safety
and enforce it at compile time:
1. Copyable Protocol
~~~~~~~~~~~~~~~~~~~~~~~~
The **Copyable protocol** marks types of instances that can be copied from one
thread context to another.
Instances of some types, such as Int, can be copied safely between threads
because they do not contain references that allow threads to access memory that
they do not own. Some types, such as String and Array (with copyable elements)
can be copied between thread context because they have value semantics and the
internal reference is not exposed.
The compiler derives the conformance of POD types and trivial enums to the
Copyable protocol automatically. Library designers need to manually mark types
with value semantics as Copyable.
Value-semantic types are not the only category of types that can be copied.
Library designers can implement thread-safe or lockless data structures and
manually mark them as Copyable.
Notice that due to rdar://17144340 we still can't mark Arrays and Optionals as
copyable::
// Optionals are copyable if the payload type is copyable.
extension Optional : CopyableType where T : CopyableType {}
2. Reentrant code
~~~~~~~~~~~~~~~~~
We ensure thread-safety by requiring that code that's executed from a worker
thread to only access logical copies of data that belongs to other threads. One
way for user code to break away from the memory enclave is to access **global
variables**. The Swift compiler must verify that threaded code does not access
global variables or unsafe code that it can't verify. There are exceptions to
this rule and the compiler provides special annotations for code that performs
I/O or calls unsafe code.
**Reentrant** code is code that only accesses memory that is accessible from the
passed arguments. In other words, reentrant code does not access global
variables or shared resources.
The thread verifier needs to be able to analyze all of the code that could
potentially be executed by a work thread and ensure that it is reentrant.
Dynamically dispatched calls, file and module boundary limit the efficacy
of the thread-verifier. This means that the information of whether a function is
reentrant or not needs to be a part of the **function signature**.
The **unsafe** attribute is used to denote code that is allowed to access global
variables and unsafe code. Objective-C methods are automatically marked as
**'unsafe'** unless they are explicitly marked with the **safe** attribute. The
`safe` and `unsafe` attributes provide a migration path for large bodies of code
that do not explicitly mark the APIs as reentrant or non-reentrant.
In the example program below the method `fly` may access the global variable
because it is marked with the attribute `unsafe`. The compiler won't allow this
method to be executed from a worker-thread.
.. code-block:: swift
var glob : Int = 1
class Bird {
unsafe func fly() { glob = 1}
}
In the example program below the `issafe` wrapper is used to explicitly mark a
region as safe. The developer is pacifying the compiler and explicitly marking
the code as safe.
The function ``logger`` is still considered by the compiler as reentrant and can
be called by worker-threads.
.. code-block:: swift
func logger(_ x : Int) {
// I know what I'm doing!
issafe {
glob = x
}
}
Most protocols in the standard library, like `Incrementable` and `Equatable` are
annotated as safe by default.
3. Gateways annotation
~~~~~~~~~~~~~~~~~~~~~~
Gateway annotation is a special semantics annotation that marks functions that
create new threads. This allows the compiler to verify that all of the
arguments that are passed to the thread conform to the Copyable protocol and
that the code that is executed by the worker thread is reentrant.
The compiler also verifies a few requirements that are special to the thread
creation site, like making sure that the closure to be executed does not capture
local mutable variables.
Library developers who implement high-level concurrency libraries can use the
gateway annotation to mark the functions that launch new threads.
.. code-block:: swift
@_semantics("swift.concurrent.launch")
public func createTask<ArgsTy>(args : ArgsTy, callback : (ArgsTy) -> Void) {
...
}
Summary
-------
Together, the thread verifier, the Copyable protocol, and the gateway annotation
allow us to implement the thread-safety layer. The rest of this document demonstrates
how these features are used for the implementation of high-level
concurrency systems.
The implementations of the thread-safety layer, the thread verifier, and
programs that use the three concurrency libraries are available in the
``concurrency`` git branch.
Implementing safe Go-lang style concurrency
===========================================
In this section, we describe how the proposed thread-safety layer can be used for
implementing go-lang style concurrency. Go supports concurrency using
coroutines and channels. We are going to demonstrate how to
implement go-style concurrency using verified code, CopyableType protocol
and gateway annotations.
Let's start by implementing Streams, which are analogous to go channels. A
stream is simply a blocking queue with restrictions on the types that can be
passed. Streams are generic data structures where the queue element type is
``CopyableType`` (and conforms to the relevant protocol, discussed above).
Streams are the only legitimate channel of communication between threads.
Streams can be shared by multiple tasks. These tasks can read from and write into the stream
concurrently. Reads from streams that contain no data and writes into full streams
will be blocked, meaning that the operating system will put the calling thread to sleep and wait for
new data to arrive to wake the sleeping thread.
This property allows the Stream to be used as a synchronization mechanism.
The second half of the go concurrency feature is coroutines. In Swift lingo,
we'll call them Tasks. Tasks are functions that are executed by threads
asynchronously. Tasks could have their own stack (this is an implementation
detail that is not important at this point) and can run indefinitely. Tasks are
created using gateways (see above) that ensure thread safety.
Together tasks and streams create a thread-safe concurrency construct. Let's
delve into this claim. Tasks are created using gateways that ensure that all
arguments being passed into the closure that will be executed are
CopyableType. In other words, all of the arguments are either deep-copied or
implemented in a way that will forbid sharing of memory. The gateway also
ensures that the closure that will be executed by the task is verified, which
means that it will not access global variables or unsafe code, and it will not capture
any variable that is accessible by the code that is creating the task. This
ensures a hermetic separation between the newly created thread and the parent
thread. Tasks can communicate using streams that ensure that information that
passes between threads, just like the task's closure arguments, does not leak
references and keeps the hermetic separation between the tasks. Notice that
Streams themselves are CopyableTypes because they can be copied freely between
tasks without violating thread safety.
Stream and Tasks provide safety and allow users to develop server-like tasks
easily. Reading requests from a queue, processing the request and writing it into
another queue are easy, especially since the queues themselves provide the
synchronization mechanisms. Deadlocks manifest themselves as read requests from
an empty queue, which makes debugging and reasoning about these bugs trivial.
Usage Example
-------------
This is an example of a tiny concurrent program that uses Tasks and Streams.
.. code-block:: swift
let input = Stream<String>()
let output = Stream<String>()
func echoServer(_ inp : Stream<String>,
out : Stream<String>) {
while true { out.push(inp.pop()) }
}
createTask((input, output), callback: echoServer)
for val in ["hello","world"] {
input.push(val)
print(output.pop())
}
The program above creates a server task that accepts an input stream and an
output stream that allows it to communicate with the main thread. The compiler
verifies that the task does not access any disallowed memory locations (as
described below).
It is entirely possible to remove the manual declaration of the streams and the
argument types and define a single endpoint for communication with the new task.
In the example below the type declaration of the endpoint helps the type checker
to deduct the type of the stream arguments and allows the developer to omit the
declaration of the streams in the closure.
.. code-block:: swift
let comm : _Endpoint<String, Int> = createTask({var counter = 0
while true {
$0.pop()
$0.push(counter++)
}})
// CHECK: 0, 1, 2,
for ss in ["","",""] {
comm.push(ss)
print("\(comm.pop()), ", appendNewline: false)
}
Stream utilities
----------------
The Swift library can implement a few utilities that will allow users and
library designers to build cool things:
* The ``Funnel`` class accepts multiple incoming streams and weaves them into a
single outgoing stream.
* The ``Fan-out`` class accepts a single incoming stream and duplicates the
messages into multiple outgoing streams.
* The ``waitForStream`` function accepts multiple Streams and returns only when
one or more of the streams are ready to be read.
It is entirely possible to implement MPI-like programs that broadcast messages
or send messages to a specific task. It is also very easy to implement barriers
for SPMD-like programs using fan-out stream.
Implementing Async - Await
==========================
Async-Await is one of the most popular and effective concurrency solutions. In
this section we describe how the proposed thread-safety layer can be used for
implementing Async-Await style concurrency.
Async calls are function calls that return a Future, which is a mechanism that
allows the caller of asynchronous procedures to wait for the results. The async
call execute the callback closure in a secure enclave to ensure thread safety.
Example
-------
Example of a concurrent program using Futures in Swift.
.. code-block:: swift
func merge_sort<T : Comparable>(array: ArraySlice<T>) -> [T] {
if array.count <= 16 { return Array(array).sort() }
let mid = array.count / 2
let left = array[0..<mid]
let right = array[mid..<array.count]
let lf = async(left, callback: merge_sort)
let lr = async(right, callback: merge_sort)
return merge(lf.await(), lr.await())
}
The program above uses async to execute two tasks that sorts the two halves of
the array in parallel. Notice that the arrays in the example above are not
copied when they are sent to and from the async task. Swift arrays are
copy-on-write value types and when an array is copied the underlying storage is
not copied with it. This feature of arrays allows swift to share arrays between
threads in a safe manner without copying data.
Here is another example of async calls using trailing closures and enums.
.. code-block:: swift
enum Shape {
case Circle, Oval, Square, Triangle
}
let res = async(Shape.Oval) {(c : Shape) -> (String) in
switch c {
case .Circle: return "Circle"
case .Oval: return "Oval"
case .Square: return "Square"
case .Triangle: return "Triangle"
}}
//CHECK: Shape: Oval
print("Shape: \(res.await())")
Notice that the swift compiler infers that ``Shape`` and `String` can be sent
between the threads.
UI programming with Async
-------------------------
One of the goals of this proposal is to allow users to develop multi-threaded UI
applications that are safe.
At the moment Swift users that use GCD are advised to start a new block in a new
thread. Once the task finishes the recommendation is to schedule another block
that will be executed by the main event loop.
Notice that the Async call returns a Future, and the callee needs to block on
the result of the Future. In this section we describe the extension to the
Async call that allows it to execute code on the main event loop asynchronously.
One possible solution would be to add an async call that accepts two closures.
One that's executed asynchronously, and another one that will be executed
synchronously after the task is finished. F# provides a similar API (with
StartWithContinuations).
One possible implementation is one where the task creation call return an object
that allows the users to register callbacks of different kinds. The destructor
of the task object would execute the work callback for convenience. The two
useful callbacks are "on completion" that would execute code in the main UI
thread and "on error" that would be executed in case of an exception in the work
closure.
This is a small example from an app that counts the number of prime numbers
between one and million concurrently. The first closure is the worker closure
that does all the work in a separate thread (and is verified by the thread
safety checker), and the second closure is executed by the UI main loop and is
free to make unsafe calls capture locals and access globals.
.. code-block:: swift
@IBAction func onClick(_ sender: AnyObject) {
progress.startAnimating()
Label!.text = ""
asyncWith (1_000_000) {
(num: Int) -> Int in
var sum = 0
for i in 1..<num {
if isPrime(i) { sum += 1 }
}
return sum
}.setOnComplete {
(x : Int) in
self.Label!.text = "Found \(x) primes.\n"
self.progress.stopAnimating()
}
}
Unsafe Concurrency with unsafeAsync
-----------------------------------
In many cases iOS users would need to use unsafe code such as code written in
Objective-C, or code that has access to shared mutable state. In the previous
section we mentioned that it is possible to mark some functions with a special
annotation that will signal to the verifier to stop the verification. For
example, the ``print`` function call would have to be marked with such an
annotation if we want people to be able to use it from thread-safe code. This
feature is useful for library developers, but not for app developers.
Some people may wish to skip the safety checks that the compiler provides and
write unsafe asynchronous code. The ``unsafeAsync`` can allow users to run
asynchronous code using Futures and async calls but without the safety checks.
The `async` call is actually a wrapper around unsafeAsync, except that it
contains the annotation that tells the verifier to verify that the code is
thread-safe (explained in the previous section). For example:
.. code-block:: swift
@_semantics("swift.concurrent.async")
// This annotation tells the compiler to verify the closure and the passed arguments at the call site.
public func async<RetTy, ArgsTy>(args : ArgsTy, callback : (ArgsTy) -> RetTy) -> Future<RetTy> {
return unsafeAsync(args, callback: callback)
}
Example of shared data structures
---------------------------------
In the example below the class PrimesCache is explicitly marked by the user as a
CopyableType. The user implemented a thread-safe class that allows concurrent
access to the method ``isPrime``. To implement a critical section the user
inherit the class ``Sync`` that contains a lock and a method that implements a
critical section. The user also had to annotate the shared method as safe
because the verifier has no way of knowing if the call is safe. Notice that the
critical section itself is not enough to ensure thread safety because the
critical section could be accessing memory that is shared between threads that
are not synchronized on the same lock.
.. code-block:: swift
final class PrimesCache : Sync, CopyableType {
var cache : [Int : Bool] = [:]
@_semantics("swift.concurrent.safe")
func isPrime(_ num : Int) -> Bool {
return self.critical {
if let r = self.cache[num] { return r }
let b = calcIsPrime(num)
self.cache[num] = b
return b
}
}
}
func countPrimes(_ P : PrimesCache) -> Int {
var sum = 0
for i in 2..<10_000 { if P.isPrime(i) { sum += 1} }
return sum
}
let shared = PrimesCache()
let R1 = async(shared, callback: countPrimes)
let R2 = async(shared, callback: countPrimes)
// CHECK: [1229, 1229]
print([R1.await(), R2.await()])
Example of parallel matrix multiply using Async
-----------------------------------------------
This is a small example of the parallel matrix multiplication algorithm using
async and futures. The slices of the matrix are not copied when they are moved
between the threads because ContiguousArray has value semantics and the parallel
code runs significantly faster.
.. code-block:: swift
func ParallelMatMul(_ A : Matrix,_ B : Matrix) -> Matrix {
assert(A.size == B.size, "size mismatch!")
// Handle small matrices using the serial algorithm.
if A.size < 65 { return SerialMatMul(A, B) }
var product = Matrix(A.size)
// Extract 4 quarters from matrices A and B.
let half = A.size/2
let A11 = A.slice(half, 0, 0)
let A12 = A.slice(half, 0, half)
let A21 = A.slice(half, half, 0)
let A22 = A.slice(half, half, half)
let B11 = B.slice(half, 0, 0)
let B12 = B.slice(half, 0, half)
let B21 = B.slice(half, half, 0)
let B22 = B.slice(half, half, half)
// Multiply each of the sub blocks.
let C11_1 = async((A11, B11), callback: ParallelMatMul)
let C11_2 = async((A12, B21), callback: ParallelMatMul)
let C12_1 = async((A11, B12), callback: ParallelMatMul)
let C12_2 = async((A12, B22), callback: ParallelMatMul)
let C21_1 = async((A21, B11), callback: ParallelMatMul)
let C21_2 = async((A22, B21), callback: ParallelMatMul)
let C22_1 = async((A21, B12), callback: ParallelMatMul)
let C22_2 = async((A22, B22), callback: ParallelMatMul)
// Add the matching blocks.
let C11 = C11_1.await() + C11_2.await()
let C12 = C12_1.await() + C12_2.await()
let C21 = C21_1.await() + C21_2.await()
let C22 = C22_1.await() + C22_2.await()
// Save the matrix slices into the correct locations.
product.update(C11, 0, 0)
product.update(C12, 0, half)
product.update(C21, half, 0)
product.update(C22, half, half)
return product
}
Implementing Actors
===================
In this section we describe how the proposed thread-safety layer can be used for
implementing Actor-based concurrency.
Actors communicate using asynchronous messages that don't block. Systems that
use actors can scale to support millions of concurrent actors because actors are
not backed by a live thread or by a stack.
In Swift actors could be implemented using classes that inherit from the generic
``Actor`` class. The generic parameter determines the type of messages that the
actor can accept. The message type needs to be of ``CopyableType`` to ensure the
safety of the model. The actor class exposes two methods: ``send`` and
``accept``. Messages are sent to actors using the ``send`` method and they never
block the sender. Actors process the message using the ``accept`` method.
At this point it should be obvious to the reader of the document why
marking the ``accept`` method as thread safe and allowing the parameter type to
be ``CopyableType`` will ensure the safety of the system (this is discussed at
length in the previous sections).
The ``accept`` method is executed by a user-space scheduler and not by live
thread and this allows the system to scale to tens of thousands of active
actors.
The code below depicts the famous prime numbers sieve program using actors. The
sieve is made of a long chain of actors that pass messages to one another.
Finally, a collector actor saves all of the messages into an array.
.. code-block:: swift
// Simply collect incoming numbers.
class Collector : Actor<Int> {
var numbers = ContiguousArray<Int>()
override func accept(_ x : Int) { numbers.append(x) }
}
// Filter numbers that are divisible by an argument.
class Sieve : Actor<Int> {
var div : Int
var next : Actor<Int>
init(div d : Int, next n : Actor<Int>) {
div = d ; next = n
}
override func accept(_ x : Int) {
if x != div && x % div == 0 { return }
next.send(x)
}
}
var col = Collector()
var head : Actor<Int> = col
// Construct the Sieve
for i in 2..<limit { head = Sieve(div: i, next: head) }
// Send all of the integers
for i in 2..<(limit*limit) { head.send(i) }
// CHECK: [1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41,
print(col.numbers.sort())