1

It looks like the Swift new concurrency model doesn't work well with the old one. I've tried to incrementally adopt the new swift concurrency model for my new async functions ( using the async/await ceremony) but I quickly hit a wall when the traditional way of avoiding data race problem ( dispatching the tasks on a serial queue) does not work anymore. I know that the new model uses actors to deal with that but I thought the 2 world could live together but did't find a solution. To prove my problem please check the following code. The playground result show that some of the code of Task 2 is executed before Task 1 is completed and that makes the queue.async powerless and it goes agains developers expectation. Is there a way to have the Tasks serialised without using actors?

import Foundation
import _Concurrency

DispatchQueue.main.async{
  print("1")
  Task{
    print("1.1")
    try await Task.sleep(nanoseconds: 1000000000)
    print("1.1.1")
  }
  print("1.1.1.1")
}

DispatchQueue.main.async{
  print("2")
  Task{
    print("2.2")
    try await Task.sleep(nanoseconds: 1000000000)
    print("2.2.2")
  }
  print("2.2.2.2")
}


result:
1
1.1.1.1
2
2.2.2.2
1.1
2.2
2.2.2
1.1.1
onthemoon
  • 3,302
  • 3
  • 17
  • 24
  • `Task.sleep` gives resources to other tasks in a queue to execute so without `sleep` tasks run one by one. – iUrii Feb 04 '22 at 15:56
  • @iUrii, that's precisely the point of my post. Using the new Task API you are no longer in control of the order of execution of the code inside the DispatchQueue.main.async. In my real code I'm using rest service calls instead of Task.sleep, something like Task{ await serlviceCall() ...} and I needed to synchronise then executing them in a serial queue. Something that now doesn't seem possible . So my conclusion is that it is not advisable to mix the 2 concurrency models because AFAIK there's no solution for this problem. Hopefully somebody comes with a suggestion. – onthemoon Feb 04 '22 at 18:58
  • By design, `await` marks a point where you lose control, and after an `await` completes, you must reevaluate all your preconditions because they may have changed (and, along the same lines, other things may have run). To avoid that, I believe the only current solution for serialization is old-school tools like dispatch groups. This is a failing of the current system, and it is discussed a lot, but I don't believe there's a Task-based (or actor-based) solution to it. Actors originally were not reentrant, but that was rejected because it was too easy to deadlock. – Rob Napier Feb 04 '22 at 19:55
  • @RobNapier, I don't see how Dispatch groups could help here. They can only make sure the 2 different tasks are both completed ( in any order)before they hand control to the DispatchGroup.notify() function. In my case I need to ensure that the 2 tasks are executed in the specified order. – onthemoon Feb 05 '22 at 09:45
  • 1
    I think Rob meant use groups and don’t use Tasks. GCD doesn’t work with structured concurrency code because it assumes your thread doesn’t change. – Jano Feb 05 '22 at 15:05
  • What I really meant is that you can use Dispatch Groups like semaphores by scheduling the next operation in the notify. (You can't do this with DispatchSemaphore easily, because it has to block the thread.) You could also use a serial DispatchQueue and use suspend/resume to prevent it from advancing until each partial task is complete. These are all very ugly. I'm not saying this is a nice way to do it. I haven't seen anyone come up with a nice way yet. – Rob Napier Feb 05 '22 at 15:11
  • (If I were going to try to build a semi-general-purpose solution to this, I'd probably build it on serial DispatchQueues and suspend/resume, but I haven't tried to actually do that to see how well it works.) – Rob Napier Feb 05 '22 at 15:12
  • I understand, but the point of my question was to see if anyone come up with a way to use Tasks and DispatchQueues at the same time to achieve what I exposed because Apple AFAIK didn't warn against using the 2 mechanism at the same time. Maybe there's a way. As for me I have reverted the few async functions I had converted to async/await Tasks back to use completion handlers and this way the behaviour of the DispatchQueue is working as expected – onthemoon Feb 06 '22 at 16:01
  • 1
    "but the point of my question was to see if anyone come up with a way to use Tasks and DispatchQueues at the same time to achieve what I exposed because Apple AFAIK didn't warn against using the 2 mechanism at the same time." Then you "know" wrong. They practically screamed about this in the video where they introduced async/await. – matt Aug 21 '22 at 17:12

1 Answers1

1

The new concurrency model has some issues interacting with the older model, but everything can be resolved with some refactoring. The initial concurrency migration is challenging, but as time progresses you will see the benefit of new concurrency model with the compile time check it provides.

For example, you can refactor your above snippet to run serially, and it is much more readable:


print("1")
print("1.1")
try await Task.sleep(nanoseconds: 1000000000)
print("1.1.1")
print("1.1.1.1")

print("2")
print("2.2")
try await Task.sleep(nanoseconds: 1000000000)
print("2.2.2")
print("2.2.2.2")

and you can also run your code on MainActor instead of using DispatchQueue :


Task { @MainActor in
    print("1")
    print("1.1")
    try await Task.sleep(nanoseconds: 1000000000)
    print("1.1.1")
    print("1.1.1.1")
}

Task { @MainActor in
    print("2")
    print("2.2")
    try await Task.sleep(nanoseconds: 1000000000)
    print("2.2.2")
    print("2.2.2.2")
}

If you still aren't satisfied you can use the TaskQueue I have created or implement your own using continuations. You can explore the repo for other synchronization mechanism you might be interested in, i.e. semaphore. The code using TaskQueue becomes:

let queue = TaskQueue()
try await queue.exec(flags: .barrier) {
    print("1")
    print("1.1")
    try await Task.sleep(nanoseconds: 1000000000)
    print("1.1.1")
    print("1.1.1.1")
}

try await queue.exec(flags: .barrier) {
    print("2")
    print("2.2")
    try await Task.sleep(nanoseconds: 1000000000)
    print("2.2.2")
    print("2.2.2.2")
}
Soumya Mahunt
  • 2,148
  • 12
  • 30
  • @Soumia I think you have missed my point that I clearly specify in the first line of my post. I needed to live with existing code using GCD. Thanks for trying anyway – onthemoon Oct 12 '22 at 15:13