@@ -227,3 +227,198 @@ NS_SWIFT_ASYNC_NAME
227
227
NS_SWIFT_ASYNC_NOTHROW
228
228
NS_SWIFT_UNAVAILABLE_FROM_ASYNC(msg)
229
229
```
230
+
231
+ ## Dispatch
232
+
233
+ ### Ordered work processing in actors, when enqueueing from a synchronous contexts
234
+
235
+ Swift concurrency naturally enforces program order for asynchronous code as long
236
+ as the execution remains in a single Task - this is equivalent to using "a single
237
+ thread" to execute some work, but is more resource efficient because the task may
238
+ suspend while waiting for some work, for example:
239
+
240
+ ```
241
+ // ✅ Guaranteed order, since caller is a single task
242
+ let printer: Printer = ...
243
+ await printer.print(1)
244
+ await printer.print(2)
245
+ await printer.print(3)
246
+ ```
247
+
248
+ This code is structurally guaranteed to execute the prints in the expected "1, 2, 3"
249
+ order, because the caller is a single task. Things
250
+
251
+ Dispatch queues offered the common ` queue.async { ... } ` way to kick off some
252
+ asynchronous work without waiting for its result. In dispatch, if one were to
253
+ write the following code:
254
+
255
+ ``` swift
256
+ let queue = DispatchSerialQueue (label : " queue" )
257
+
258
+ queue.async { print (1 ) }
259
+ queue.async { print (2 ) }
260
+ queue.async { print (3 ) }
261
+ ```
262
+
263
+ The order of the elements printed is guaranteed to be ` 1 ` , ` 2 ` and finally ` 3 ` .
264
+
265
+ At first, it may seem like ` Task { ... } ` is exactly the same, because it also
266
+ kicks off some asynchronous computation without waiting for it to complete.
267
+ A naively port of the same code might look like this:
268
+
269
+ ``` swift
270
+ // ⚠️ any order of prints is expected
271
+ Task { print (1 ) }
272
+ Task { print (2 ) }
273
+ Task { print (3 ) }
274
+ ```
275
+
276
+ This example ** does not** guarantee anything about the order of the printed values,
277
+ because Tasks are enqueued on a global (concurrent) threadpool which uses multiple
278
+ threads to schedule the tasks. Because of this, any of the tasks may be executed first.
279
+
280
+ Another attempt at recovering serial execution may be to use an actor, like this:
281
+
282
+ ``` swift
283
+ // ⚠️ still any order of prints is possible
284
+ actor Printer {}
285
+ func go () {
286
+ // Notice the tasks don't capture `self`!
287
+ Task { print (1 ) }
288
+ Task { print (2 ) }
289
+ Task { print (3 ) }
290
+ }
291
+ }
292
+ ```
293
+
294
+ This specific example still does not even guarantee enqueue order (!) of the tasks,
295
+ and much less actual execution order. The tasks this is because lack of capturing
296
+ ` self ` of the actor, those tasks are effectively going to run on the global concurrent
297
+ pool, and not on the actor. This behavior may be unexpected, but it is the current semantics.
298
+
299
+ We can correct this a bit more in order to ensure the enqueue order, by isolating
300
+ the tasks to the actor, this is done as soon as we capture the ` self ` of the actor:
301
+
302
+ ``` swift
303
+ // ✅ enqueue order of tasks is guaranteed
304
+ //
305
+ // ⚠️ however. due to priority escalation, still any order of prints is possible (!)
306
+ actor Printer {}
307
+ func go () { // assume this method must be synchronous
308
+ // Notice the tasks do capture self
309
+ Task { self .log (1 ) }
310
+ Task { self .log (2 ) }
311
+ Task { self .log (3 ) }
312
+ }
313
+
314
+ func log (_ int : Int ) { print (int) }
315
+ }
316
+ ```
317
+
318
+ This improves the situation because the tasks are now isolated to the printer
319
+ instance actor (by means of using ` Task{} ` which inherits isolation, and refering
320
+ to the actor's ` self ` ), however their specific execution order is _ still_ not deterministic.
321
+
322
+ Actors in Swift are ** not** strictly FIFO ordered, and tasks
323
+ processed by an actor may be reordered by the runtime for example because
324
+ of _ priority escalation_ .
325
+
326
+ ** Priority escalation** takes place when a low-priority task suddenly becomes
327
+ await-ed on by a high priority task. The Swift runtime is able to move such
328
+ task "in front of the queue" and effectively will process the now priority-escalated
329
+ task, before any other low-priority tasks. This effectively leads to FIFO order
330
+ violations, because such task "jumped ahead" of other tasks which may have been
331
+ enqueue on the actor well ahead of it. This does does help make actors very
332
+ responsive to high priority work which is a valuable property to have!
333
+
334
+ > Note: Priority escalation is not supported on actors with custom executors.
335
+
336
+ The safest and correct way to enqueue a number of items to be processed by an actor,
337
+ in a specific order is to use an ` AsyncStream ` to form a single, well-ordered
338
+ sequence of items, which can be emitted to even from synchronous code.
339
+ And then consume it using a _ single_ task running on the actor, like this:
340
+
341
+ ``` swift
342
+ // ✅ Guaranteed order in which log() are invoked,
343
+ // regardless of priority escalations, because the disconnect
344
+ // between the producing and consuming task
345
+ actor Printer {
346
+ let stream: AsyncStream<Int >
347
+ let streamContinuation: AsyncStream<Int >.Continuation
348
+ var streamConsumer: Task<Void , Never >!
349
+
350
+ init () async {
351
+ let (stream, continuation) = AsyncStream.makeStream (of : Int .self )
352
+ self .stream = stream
353
+ self .streamContinuation = continuation
354
+
355
+ // Consuming Option A)
356
+ // Start consuming immediately,
357
+ // or better have a caller of Printer call `startStreamConsumer()`
358
+ // which would make it run on the caller's task, allowing for better use of structured concurrency.
359
+ self .streamConsumer = Task { await self .consumeStream () }
360
+ }
361
+
362
+ deinit {
363
+ self .streamContinuation .finish ()
364
+ }
365
+
366
+ nonisolated func enqueue (_ item : Int ) {
367
+ self .streamContinuation .yield (item)
368
+ }
369
+
370
+ nonisolated func cancel () {
371
+ self .streamConsumer ? .cancel ()
372
+ }
373
+
374
+ func consumeStream () async {
375
+ for await item in self .stream {
376
+ if Task.isCancelled { break }
377
+
378
+ log (item)
379
+ }
380
+ }
381
+
382
+ func log (_ int : Int ) { print (int) }
383
+ }
384
+ ```
385
+
386
+ and invoke it like:
387
+
388
+ ```
389
+ let printer: Printer = ...
390
+ printer.enqueue(1)
391
+ printer.enqueue(2)
392
+ printer.enqueue(3)
393
+ ```
394
+
395
+ We're assuming that the caller has to be in synchronous code, and this is why we make the ` enqueue `
396
+ method ` nonisolated ` but we use it to funnel work items into the actor's stream.
397
+
398
+ The actor uses a single task to consume the async sequence of items, and this way guarantees
399
+ the specific order of items being handled.
400
+
401
+ This approach has both up and down-sides, because now the item processing cannot be affected by
402
+ priority escalation. The items will always be processed in their strict enqueue order,
403
+ and we cannot easily await for their results -- since the caller is in synchronous code,
404
+ so we might need to resort to callbacks if we needed to report completion of an item
405
+ getting processed.
406
+
407
+ Notice that we kick off an unstructured task in the actor's initializer, to handle the
408
+ consuming of the stream. This also may be sub-optimal, because as cancellation must
409
+ now be handled manually. You may instead prefer to _ not_ create the consumer task
410
+ at all in this Printer type, but require that some existing task invokes ` await consumeStream() ` , like this:
411
+
412
+ ```
413
+ let printer: Printer = ...
414
+ Task { // or some structured task
415
+ await printer.consumeStream()
416
+ }
417
+
418
+ printer.enqueue(1)
419
+ printer.enqueue(2)
420
+ printer.enqueue(3)
421
+ ```
422
+
423
+ In this case, you'd should make sure to only have at-most one task consuming the stream,
424
+ e.g. by using a boolean flag inside the printer actor.
0 commit comments