Understanding NSRunLoop

Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

From here


The most important feature of CFRunLoop is the CFRunLoopModes. CFRunLoop works with a system of “Run Loop Sources”. Sources are registered on a run loop for one or several modes, and the run loop itself is made to run in a given mode. When an event arrives on a source, it is only handled by the run loop if the source mode matches the run loop current mode.

From here


Run loops are what separates interactive apps from command-line tools.

  • Command-line tools are launched with parameters, execute their command, then exit.
  • Interactive apps wait for user input, react, then resume waiting.

From here

They allow you to wait till user taps and respond accordingly, wait till you get a completionHandler and apply its results, wait till you get a timer and perform a function. If you don't have a runloop then you can't be listening/waiting for user taps, you can't wait till a network call is happening, you can't be awoken in x minutes unless you use DispatchSourceTimer or DispatchWorkItem

Also from this comment:

Background threads don't have their own runloops, but you can just add one. E.g. AFNetworking 2.x did it. It was tried and true technique for NSURLConnection or NSTimer on background threads, but we don't do this ourselves much anymore, as newer APIs eliminate the need to do so. But it appears that URLSession does, e.g., here is simple request, running [see the left panel of the image] completion handlers on the main queue, and you can see it has a run loop on background thread


Specifically about: "Background threads don't have their own runloops". The following timer fails to fire for an async dispatch:

class T {
    var timer: Timer?

    func fireWithoutAnyQueue() {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { _ in
            print("without any queue") // success. It's being ran on main thread, since playgrounds begin running from main thread
        })
    }

    func fireFromQueueAsnyc() {
        let queue = DispatchQueue(label: "whatever")
        queue.async {
            self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { (_) in
                print("from a queue — async") // failed to print
            })
        }
    }

    func fireFromQueueSnyc() {
        let queue = DispatchQueue(label: "whatever")
        queue.sync {
            timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { (_) in
                print("from a queue — sync") // success. Weird. Read my possible explanation below
            })
        }
    }

    func fireFromMain() {
        DispatchQueue.main.async {
            self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { (_) in
                print("from main queue — sync") //success
            })
        }
    }
}

I think the reason the sync block also runs is because:

sync blocks usually just get executed from within their source queue. In this example, source queue is main queue, the whatever queue is the destination queue.

To test that I logged RunLoop.current inside every dispatch.

The sync dispatch had the same runloop as main queue. While the RunLoop within the async block was a different instance from the others. You might be thinking how why does RunLoop.current return a different value. Isn't it a shared value!? Great question! Read further:

IMPORTANT NOTE:

The class property current is NOT a global variable.

Returns the run loop for the current thread.

It's contextual. It's visible only within the scope of the thread ie Thread-local storage. For more on that see here.

This is a known issue with timers. You don't have the same issue if you use DispatchSourceTimer


RunLoops are a bit of like a box where stuff just happens.

Basically, in a RunLoop, you go to process some events and then return. Or return if it doesn't process any events before the timeout is hit. You can say it as similar to asynchronous NSURLConnections, Processing data in the background without interfering your current loop and but at the same time, you require data synchronously. Which can be done with the help of RunLoop which makes your asynchronous NSURLConnection and provides data at calling time. You can use a RunLoop like this:

NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1];

while (YourBoolFlag && [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:loopUntil]) {
    loopUntil = [NSDate dateWithTimeIntervalSinceNow:0.1];
}

In this RunLoop, it will run until you complete some of your other work and set YourBoolFlag to false.

Similarly, you can use them in threads.

Hope this helps you.


A run loop is an abstraction that (among other things) provides a mechanism to handle system input sources (sockets, ports, files, keyboard, mouse, timers, etc).

Each NSThread has its own run loop, which can be accessed via the currentRunLoop method.

In general, you do not need to access the run loop directly, though there are some (networking) components that may allow you to specify which run loop they will use for I/O processing.

A run loop for a given thread will wait until one or more of its input sources has some data or event, then fire the appropriate input handler(s) to process each input source that is "ready.".

After doing so, it will then return to its loop, processing input from various sources, and "sleeping" if there is no work to do.

That's a pretty high level description (trying to avoid too many details).

EDIT

An attempt to address the comment. I broke it into pieces.

  • it means that i can only access/run to run loop inside the thread right?

Indeed. NSRunLoop is not thread safe, and should only be accessed from the context of the thread that is running the loop.

  • is there any simple example how to add event to run loop?

If you want to monitor a port, you would just add that port to the run loop, and then the run loop would watch that port for activity.

- (void)addPort:(NSPort *)aPort forMode:(NSString *)mode

You can also add a timer explicitly with

- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
  • what means it will then return to its loop?

The run loop will process all ready events each iteration (according to its mode). You will need to look at the documentation to discover about run modes, as that's a bit beyond the scope of a general answer.

  • is run loop inactive when i start the thread?

In most applications, the main run loop will run automatically. However, you are responsible for starting the run loop and responding to incoming events for threads you spin.

  • is it possible to add some events to Thread run loop outside the thread?

I am not sure what you mean here. You don't add events to the run loop. You add input sources and timer sources (from the thread that owns the run loop). The run loop then watches them for activity. You can, of course, provide data input from other threads and processes, but input will be processed by the run loop that is monitoring those sources on the thread that is running the run loop.

  • does it mean that sometimes i can use run loop to block thread for a time

Indeed. In fact, a run loop will "stay" in an event handler until that event handler has returned. You can see this in any app simply enough. Install a handler for any IO action (e.g., button press) that sleeps. You will block the main run loop (and the whole UI) until that method completes.

The same applies to any run loop.

I suggest you read the following documentation on run loops:

https://developer.apple.com/documentation/foundation/nsrunloop

and how they are used within threads:

https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1