Design pattern for scheduling simulation steps

I'm running a simulation (SwiftUI app), which has 100 steps.

I need each step to be executed in order.

A first try was to dispatch with delay to schedule each second:

for step in 0..<100 {
     DispatchQueue.main.asyncAfter(deadline: .now() + Double(step) * 1.0) {
        // simulation code 
     }
}

Very poor results as 100 running threads are too much load for the system.

So I split in 2 stages:

for bigStep in 0..<10 { 
   DispatchQueue.main.asyncAfter(deadline: .now() + Double(bigStep) * 10.0 ) { 
          for step in 0..<10 {
               DispatchQueue.main.asyncAfter(deadline: .now() + Double(step) * 1.0) { 
                 // simulation code
               }
          }
    }
}

It works much better, as now there are a max of 20 threads active (in fact I create more levels to limit to a max of 8 concurrent threads).

It addition, it allows to interrupt the simulation before end.

My questions:

  • is it the appropriate pattern ?
  • Would a timer be better ?
  • Other options ?

Accepted Reply

Should I run in a global() instead of main?

No. Apple generally recommends that you avoid the global concurrent queue, for reasons I explain in Avoid Dispatch Global Concurrent Queues.

If an individual step runs quickly, putting it on .main is fine. Remember that your code running on the main queue is serialised with respect to all other code running on the main queue, so scheduling long-running code there will cause UI responsiveness problems. Your goal should be to limit such code to a few milliseconds, which ensures that all the other stuff running on the main queue has time to run within the current frame (frame times are somewhere between 7 and 33 ms, depending on the device you’re targeting).

If a step takes longer than that then you need a secondary thread. How you get that depends on the structure of your code. The easiest option is to create a Dispatch serial queue or, in modern Swift, an actor.

Keep in mind that code running on a secondary thread can’t update your UI directly, so it’ll have to bounce back to the main thread to do that. And then you have to make sure that that code runs quickly. So, for example, if you have an image to display, render the image on a your secondary thread and then pass the fully rendered image back to the main thread which then just plonks it in the image view.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • I changed from main to a Dispatch in case it overpasses duration limit. Works quite OK. Thanks for all the help.

Add a Comment

Replies

is it the appropriate pattern ?

Probably not.

If you schedule all your work up front like this you can’t cancel, which is a serious problem. Regardless of what else you do, you need to provide some way to cancel the work.

Very poor results as 100 running threads are too much load for the system.

I don’t understand this. You’re scheduling your work on the main queue, so there’s only ever one thread running it. I suspect that this is not too many threads, but rather overloading the main thread.

Can you give us more details about your simulation. Are you trying to run your steps as quickly as possible? Or run them in real time?

And within each step, how much work do you need to do? Roughly how long does it take with a single thread? And if that’s a long time, is the work amenable to parallelisation?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks Quinn.

.

Can you give us more details about your simulation. Are you trying to run your steps as quickly as possible? Or run them in real time?

  • It is simulated time, not real time.
  • I do not need to run very fast, but at a regular intervals, to have a smooth simulation.
  • In fact I have 1s between 2 steps of simulation, which should be largely enough.

Problem now is that the first 50 steps run regularly, then simulation stope and restarts after several seconds.

.

And within each step, how much work do you need to do? Roughly how long does it take with a single thread?

A single step is much shorter: 0.00012 seconds

When I run the 256 steps, it is never more than 0.000240 seconds.

.

Regardless of what else you do, you need to provide some way to cancel the work.

I did it in a limited way: at each level (hugeStep, veryBigStep, bigStep, step, as described in original post, I test a state var to stop by exiting the loop. Not perfect, but so far, good enough.

But finally, thanks to your idea to measure the step duration, I have found an issue.

In the deadline, I took margins (100 s at highest level when in fact 64 would be enough !). So no surprise the app stooped for more than 30 seconds.

So, it works pretty well now (not perfect though), but I do not find it a very clean design.

.

I suspect that this is not too many threads, but rather overloading the main thread.

How should I avoid this ? Should I run in a global() instead of main ?

Should I run in a global() instead of main?

No. Apple generally recommends that you avoid the global concurrent queue, for reasons I explain in Avoid Dispatch Global Concurrent Queues.

If an individual step runs quickly, putting it on .main is fine. Remember that your code running on the main queue is serialised with respect to all other code running on the main queue, so scheduling long-running code there will cause UI responsiveness problems. Your goal should be to limit such code to a few milliseconds, which ensures that all the other stuff running on the main queue has time to run within the current frame (frame times are somewhere between 7 and 33 ms, depending on the device you’re targeting).

If a step takes longer than that then you need a secondary thread. How you get that depends on the structure of your code. The easiest option is to create a Dispatch serial queue or, in modern Swift, an actor.

Keep in mind that code running on a secondary thread can’t update your UI directly, so it’ll have to bounce back to the main thread to do that. And then you have to make sure that that code runs quickly. So, for example, if you have an image to display, render the image on a your secondary thread and then pass the fully rendered image back to the main thread which then just plonks it in the image view.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • I changed from main to a Dispatch in case it overpasses duration limit. Works quite OK. Thanks for all the help.

Add a Comment

@eskimo, I followed your advice and changed the design accordingly. Simpler, cleaner and much more efficient! Thanks.

Simple code sample on how to implement: https://stackoverflow.com/questions/64202210/dispatchqueue-main-asyncafter-not-delaying

Add a Comment