@@ -228,197 +228,3 @@ NS_SWIFT_ASYNC_NOTHROW
228
228
NS_SWIFT_UNAVAILABLE_FROM_ASYNC(msg)
229
229
```
230
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