diff --git a/CONCURRENCY.md b/CONCURRENCY.md new file mode 100644 index 00000000000..908fbf64401 --- /dev/null +++ b/CONCURRENCY.md @@ -0,0 +1,114 @@ +### Concurrency in Kotlin/Native + + Kotlin/Native runtime doesn't encourage a classical thread-oriented concurrency + model with mutually exclusive code blocks and conditional variables, as this model is + known to be error-prone and unreliable. Instead, we suggest collection of + alternative approaches, allowing to use hardware concurrency and implement blocking IO. + Those approaches are as following, and will be elaborated in further sections: + * Workers with message passing + * Object subgraph ownership transfer + * Object subgraph freezing + * Object subgraph detachment + * Raw shared memory using C globals + * Coroutines for blocking operations (not covered in this document) + + ## Workers + + Instead of threads Kotlin/Native runtime offers concept of workers: concurrently executing + control flow streams with an associated request queue. Workers are very similar to actors + in the Actor Model. Worker can exchange Kotlin objects with other workers, so that at the moment + each mutable object is owned by the single worker, but ownership could be transferred. + See section [Object transfer and freezing](#transfer). + + Once worker is started with `startWorker` function call, it can be uniquely addressed with an integer + worker id. Other workers, or non-worker concurrency primitives, such as OS threads, could send a message + to the worker with `schedule` call. + ```kotlin + val future = schedule(TransferMode.CHECKED, { SomeDataForWorker() }) { + // data returned by the second function argument comes to the + // worker routine as 'input' parameter. + input -> + // Here we create an instance to be returned when someone consumes result future. + WorkerResult(input.stringParam + " result") + } + + future.consume { + // Here we see result returned from routine above. Note that future object or + // id could be transferred to another worker, so we don't have to consume future + // in same execution context it was obtained. + result -> + println("result is $result") + } +``` + The call to `schedule` uses function passed as its second parameter to produce an object subgraph + (i.e. set of mutually referring objects) which is passed as the whole to that worker, and no longer + available to the thread that initiated the request. This property is checked if the first parameter + is `TransferMode.CHECKED` by graph traversal and just assumed to be true, if it is `TransferMode.UNCHECKED`. + Last parameter to schedule is a special Kotlin lambda, which is not allowed to capture any state, + and is actually invoked in target worker's context. Once processed, result is transferred to whoever consumes + the future, and is attached to object graph of that worker/thread. + + If an object is transferred in `UNCHECKED` mode and is still accessible from multiple concurrent executors, + program will likely crash unexpectedly, so consider that last resort in optimizing, not a general purpose + mechanism. + + For more complete example please refer to the [workers example](https://github.com/JetBrains/kotlin-native/tree/master/samples/workers) + in the Kotlin/Native repository. + + + ## Object transfer and freezing + + Important invariant that Kotlin/Native runtime maintains is that object is either owned by a single + thread/worker, or is immutable (_shared XOR mutable_). This ensures that the same data has a single mutator, and so no need for + locking exists. To achieve such an invariant, we use concept of not externally referred object subgraphs. + This is a subgraph which has no external references from outside of the subgraph, what could be checked + algorithmically with O(N) complexity (in ARC systems), where N is number of elements in such a subgraph. + Such subgraphs are usually produced as a result of lambda expression, for example some builder, and may not + contain objects, referred externally. + + Freezing is a runtime operation making given object subgraph immutable, by modifying the object header + so that future mutation attempts lead to throwing an `InvalidMutabilityException`. It is deep, so + if an object has a pointer to another objects - transitive closure of such objects will be frozen. + Freezing is the one way transformation, frozen objects cannot be unfrozen. Frozen objects has a nice + property that due to their immutability, they could freely shared between multiple workers/threads + not breaking the "mutable XOR shared" invariant. + + If object is frozen could be checked with an extension property `isFrozen`, and if it is, object sharing + is allowed. Currently, Kotlin/Native runtime only freezes enum objects after creation, although additional + autofreezing of certain provably immutable objects could be implemented in the future. + + ## Object subgraph detachment + + Object subgraph without external references could be disconnected using `detachObjectGraph` to + a `COpaquePointer` value, which could be stored in `void*` data, so disconnected object subgraphs + could be stored in C data structure, and later attached back with `attachObjectGraph` in arbitrary thread + or worker. Combined with [raw memory sharing](#shared) it allows side channel object transfer between + concurrent threads, if worker mechanisms are insufficient for the particular task. + + ## Raw shared memory + + Considering strong ties of Kotlin/Native with C via interoperability, in conjunction with other mechanisms + mentioned above one could build popular data structures, like concurrent hashmap or shared cache with + Kotlin/Native. One could rely upon shared C data, and store in it references to detached object subgraphs. + Consider the following .def file: +``` +package = global + +--- +typedef struct { + int version; + void* kotlinObject; +} SharedData; + +SharedData sharedData; +``` +After running cinterop tool it allows sharing Kotlin data in versionized global structure, +and interact with it from Kotlin transparently via autogenerated Kotlin like this: +```kotlin +class SharedData(rawPtr: NativePtr) : CStructVar(rawPtr) { + var version: Int + var kotlinObject: COpaquePointer? +} +``` +So combined with the top level variable declared above, it allows seeing the same memory from different +threads and building traditional concurrent structures with platform-specific synchronization primitives.