How to Delay a for Loop in Swift Without Interrupting The Main Thread

How do I delay a for loop in swift without interrupting the main thread?

Re your first concern, the use of a series of asyncAfter are going to suffer from “timer coalescing”, where the OS will group future, individually scheduled timers to fire all at once, to make best use of the device battery (the less often the OS needs to wake up the device, the better the battery life). The further out the scheduled timers are, the more coalescing the OS will do.

One can avoid this by using a repeating Timer:

func handle(string: String) {
guard !string.isEmpty else { return }

var index = string.startIndex
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if string[index] == "0" {
// handle "0"
} else {
// handle not "0"
}

index = string.index(after: index)
if index == string.endIndex { timer.invalidate() }
}
}

Delaying a queue for a finite time, conditionally

You can suspend custom dispatch queues (but not global queues nor main queue). That stops new tasks from starting on that queue, but it does not affect things already running on that queue. You can resume to start running items that had previously been dispatched to the queue, but had not yet started.

GCD also provides a native mechanism to cancel a particular work item, and dispatch it again later when you want execution to resume. Note that cancel does not perform preemptive cancellation, but rather only sets a Boolean, isCancelled, which your dispatched task would need to periodically check and manually exit.

(If you want to cancel tasks on a queue, you might consider OperationQueue, as that has more graceful cancelation capabilities than dispatch queues. Or you might consider the “structured concurrency” of async-await of Swift concurrency, which also has cancelation built-in.)


Now, while GCD does not have a notion of “suspending” a task dispatched to a background thread, you might be able to jury-rig something something with a very careful use a semaphores. But the details would vary greatly based upon your implementation, so it is hard to advise further without more details.


You asked:

The docs for RunLoop suggest a while loop around the function run with a custom condition in the while loop.

As a general rule, anything that involves spinning in a while loop is to be avoided. It is s very inefficient pattern and is to be avoided. Many years ago (e.g. before GCD, before URLSession, etc.), this spin-on-run-loop pattern was not unheard of (e.g., it was the go-to technique for running NSURLConnection on a background thread), but it is an anachronism nowadays. It is an inefficient approach; an anti-pattern.

Wait for a while without blocking main thread

Thread.Sleep(500) will force the current thread to wait 500ms. It works, but it's not what you want if your entire application is running on one thread.

In that case, you'll want to use a Timer, like so:

using System.Timers;

void Main()
{
Timer t = new Timer();
t.Interval = 500; // In milliseconds
t.AutoReset = false; // Stops it from repeating
t.Elapsed += new ElapsedEventHandler(TimerElapsed);
t.Start();
}

void TimerElapsed(object sender, ElapsedEventArgs e)
{
Console.WriteLine("Hello, world!");
}

You can set AutoReset to true (or not set it at all) if you want the timer to repeat itself.

How do you schedule a block to run on the next run loop iteration?

You might not be aware of everything that the run loop does in each iteration. (I wasn't before I researched this answer!) As it happens, CFRunLoop is part of the open-source CoreFoundation package, so we can take a look at exactly what it entails. The run loop looks roughly like this:

while (true) {
Call kCFRunLoopBeforeTimers observer callbacks;
Call kCFRunLoopBeforeSources observer callbacks;
Perform blocks queued by CFRunLoopPerformBlock;
Call the callback of each version 0 CFRunLoopSource that has been signalled;
if (any version 0 source callbacks were called) {
Perform blocks newly queued by CFRunLoopPerformBlock;
}
if (I didn't drain the main queue on the last iteration
AND the main queue has any blocks waiting)
{
while (main queue has blocks) {
perform the next block on the main queue
}
} else {
Call kCFRunLoopBeforeWaiting observer callbacks;
Wait for a CFRunLoopSource to be signalled
OR for a timer to fire
OR for a block to be added to the main queue;
Call kCFRunLoopAfterWaiting observer callbacks;
if (the event was a timer) {
call CFRunLoopTimer callbacks for timers that should have fired by now
} else if (event was a block arriving on the main queue) {
while (main queue has blocks) {
perform the next block on the main queue
}
} else {
look up the version 1 CFRunLoopSource for the event
if (I found a version 1 source) {
call the source's callback
}
}
}
Perform blocks queued by CFRunLoopPerformBlock;
}

You can see that there are a variety of ways to hook into the run loop. You can create a CFRunLoopObserver to be called for any of the “activities” you want. You can create a version 0 CFRunLoopSource and signal it immediately. You can create a connected pair of CFMessagePorts, wrap one in a version 1 CFRunLoopSource, and send it a message. You can create a CFRunLoopTimer. You can queue blocks using either dispatch_get_main_queue or CFRunLoopPerformBlock.

You will need to decide which of these APIs to use based on when you are scheduling the block, and when you need it to be called.

For example, touches are handled in a version 1 source, but if you handle the touch by updating the screen, that update isn't actually performed until the Core Animation transaction is committed, which happens in a kCFRunLoopBeforeWaiting observer.

Now suppose you want to schedule the block while you're handling the touch, but you want it to be executed after the transaction is committed.

You can add your own CFRunLoopObserver for the kCFRunLoopBeforeWaiting activity, but this observer might run before or after Core Animation's observer, depending on the order you specify and the order Core Animation specifies. (Core Animation currently specifies an order of 2000000, but that is not documented so it could change.)

To make sure your block runs after Core Animation's observer, even if your observer runs before Core Animation's observer, don't call the block directly in your observer's callback. Instead, use dispatch_async at that point to add the block to the main queue. Putting the block on the main queue will force the run loop to wake up from its “wait” immediately. It will run any kCFRunLoopAfterWaiting observers, and then it will drain the main queue, at which time it will run your block.

Wait until swift for loop with asynchronous network requests finishes executing

You can use dispatch groups to fire an asynchronous callback when all your requests finish.

Here's an example using dispatch groups to execute a callback asynchronously when multiple networking requests have all finished.

override func viewDidLoad() {
super.viewDidLoad()

let myGroup = DispatchGroup()

for i in 0 ..< 5 {
myGroup.enter()

Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"]).responseJSON { response in
print("Finished request \(i)")
myGroup.leave()
}
}

myGroup.notify(queue: .main) {
print("Finished all requests.")
}
}

Output

Finished request 1
Finished request 0
Finished request 2
Finished request 3
Finished request 4
Finished all requests.

Is it possible to check that main thread is idle / to drain a main run loop?

you can try this

while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) == kCFRunLoopRunHandledSource);

this will run until no more things in the run loop. you can try to change the time interval to 0.1 if 0 is not working.

Populating UITableView with timeout

Your problem is that you are running on the main thread and never giving the system the chance to update the table. You violated the rule: never call sleep on the main thread.

You can fix this several ways:

1) Run your loop on a background thread, and return to the main thread to call self.sendMessage and print.

let size = dictionary[key]!.count;
let pointer = random(lower: 0, upper: UInt32(size) - 1);
let part = dictionary[key]![pointer];

DispatchQueue.global().async {
for row in part {
if let message = row as? Message {
DispatchQueue.main.async {
self.sendMessage(message: message);
print("MESSAGE TRIGGERED");
}
usleep(1 * 1000 * 1000)
}

if let question = row as? Question {

}
}
}

2) Use a repeating timer with an interval of 1 second and add one item to your table each time the timer fires.

let size = dictionary[key]!.count;
let pointer = random(lower: 0, upper: UInt32(size) - 1);
let part = dictionary[key]![pointer];

var idx = 0
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in

if idx >= part.count {
timer.invalidate()
} else {
let row = part[idx]
if let message = row as? Message {
self.sendMessage(message: message);
print("MESSAGE TRIGGERED");
}

if let question = row as? Question {

}
}
idx += 1
}

3) Delay each table addition by an increasingly larger delay:

let size = dictionary[key]!.count;
let pointer = random(lower: 0, upper: UInt32(size) - 1);
let part = dictionary[key]![pointer];

var delay = 0.0
for row in part {
if let message = row as? Message {
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
self.sendMessage(message: message);
print("MESSAGE TRIGGERED");
}
delay += 1.0
}

if let question = row as? Question {

}
}


Related Topics



Leave a reply



Submit