The original address

Multithreading has always been one of the technical knowledge that interests me a lot. I especially like GCD, a lightweight multithreading solution. In order to understand its implementation, I have been reading libDispatch’s source code tirelessly. Even because I liked it so much, I wanted to write the corresponding source code parsing series, but I was afraid of writing it badly, so in addition to the opening type introduction, it was hastily done, without the following

A few days ago, some of my friends posted some GCD problems, but the results were unexpected. After a careful exploration, I found that Apple made some interesting changes based on libDispatch, so I wanted to share these two problems. Since the running code provided by my friend is written for Swift, I will convert it into the equivalent OC code to tell the story. The following two concepts will make subsequent reading easier if you understand them:

  • The concept of synchronization and asynchrony
  • The difference between queues and threads

Misunderstood concept

We might have an understanding of the main thread and main queue

The main thread only performs the tasks of the main queue. Also, the main queue is executed only on the main thread

The main thread only performs the tasks of the main queue

The first is that the main thread only performs the tasks of the main queue. In iOS, only the main thread has the permission to submit the packaged layer tree information to the rendering service to complete the display of the graphics. The UI updates we submitted in the Work Queue were always invalid and even caused crashes. Since there is only one main queue and all other queues are work queues, it can be concluded that the main thread only performs the tasks of the main queue. However, there is the following code:

dispatch_queue_t mainQueue = dispatch_get_main_queue(); dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); dispatch_queue_set_specific(mainQueue, "key", "main", NULL); dispatch_sync(globalQueue, ^{ BOOL res1 = [NSThread isMainThread]; BOOL res2 = dispatch_get_specific("key") ! = NULL; NSLog(@"is main thread: %zd --- is main queue: %zd", res1, res2); });Copy the code

The output indicates that the main thread is executing the work Queue at this time

dispatch_sync

The above code gets the expected result when replaced with async, but causes this problem when executed synchronously. Before finding the cause, use the code in bestSwifter article. First, sync call stack and roughly source code are as follows:

Dispatch_sync └ ─ ─ dispatch_sync_f └ ─ ─ _dispatch_sync_f2 └ ─ ─ _dispatch_sync_f_slow static void _dispatch_sync_f_slow(dispatch_queue_t dq, void *ctxt, dispatch_function_t func) { _dispatch_thread_semaphore_t sema = _dispatch_get_thread_semaphore(); struct dispatch_sync_slow_s { DISPATCH_CONTINUATION_HEADER(sync_slow); } dss = { .do_vtable = (void*)DISPATCH_OBJ_SYNC_SLOW_BIT, .dc_ctxt = (void*)sema, }; _dispatch_queue_push(dq, (void *)&dss); _dispatch_thread_semaphore_wait(sema); _dispatch_put_thread_semaphore(sema); / /... }Copy the code

You can see that libDispatch processes synchronous tasks by blocking the calling thread with sema semaphores until the task is processed, which is why sync nested use is a deadlock problem. The flow chart of the implementation can be obtained from the source code:

But in practice, blocks are executed on the main thread, and the actual flow of the code looks like this:

So you can make a guess:

The sync function itself blocks the current thread of execution until the task is executed. In order to reduce the overhead of thread switching and avoid the waste of resources when threads are blocked, the sync function has been modified to perform synchronization tasks directly on the current thread in most cases

Once you have a guess, you need to test it. We say most cases because the main queue is currently valid only on the main thread, so we exclude the condition global-sync -> main. So to verify the effect, we need to create a serial thread:

dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL); dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); dispatch_sync(globalQueue, ^{ BOOL res1 = [NSThread isMainThread]; BOOL res2 = dispatch_get_specific("key") ! = NULL; NSLog(@"is main thread: %zd --- is main queue: %zd", res1, res2); }); dispatch_async(globalQueue, ^{ NSThread *globalThread = [NSThread currentThread]; dispatch_sync(serialQueue, ^{ BOOL res = [NSThread currentThread] == globalThread; NSLog(@"is same thread: %zd", res); }); });Copy the code

When it was run, the answer was YES both times, which was enough to confirm the conjecture and confirm that Apple had made changes to Sync to improve performance. In addition, the global-sync -> main test results show that the sync call process is not optimized

The main queue is executed only on the main thread

As mentioned above, only the main thread has permission to submit render tasks. Again, this understanding should be true because of the following two assumptions:

  • The main queue can always be calledUIKitThe interface of theapi
  • Only one thread can execute a serial queue at a time

Similarly, my friend gave me another code:

dispatch_queue_set_specific(mainQueue, "key", "main", NULL); dispatch_block_t log = ^{ printf("main thread: %zd", [NSThread isMainThread]); void *value = dispatch_get_specific("key"); printf("main queue: %zd", value ! = NULL); } dispatch_async(globalQueue, ^{ dispatch_async(dispatch_get_main_queue(), log); }); dispatch_main();Copy the code

After running, the output is NO and YES, which means that the main queue task is not running on the main thread. This is obviously more difficult to understand than the previous problem, because if the child thread can perform the main queue task, then it cannot submit the packaging layer information to the render service at this point

Again, we can guess why. Instead of a normal project startup code, the Swift file runs more like a script because it is missing a startup code:

@autoreleasepool
{
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
Copy the code

To find the answer, you first need to modify the code in which the main thread of the problem only performs tasks in the main queue. The mach_thread_self function returns the id of the current thread, which can be used to determine whether the two threads are the same:

thread_t threadId = mach_thread_self();

dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

dispatch_async(globalQueue, ^{
    dispatch_async(mainQueue, ^{
        NSLog(@"%zd --- %zd", threadId == mach_thread_self(), [NSThread isMainThread]);
    });
});

@autoreleasepool
{
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
Copy the code

The result of this code is YES, indicating that the thread ID of the main queue task is the same before and after UIApplicationMain. Therefore, two conditions can be obtained:

  • Tasks in the main queue are always executed on the same thread
  • inUIApplicationMainAfter the function call,isMainThreadThe correct result is returned

Combined with these two conditions, we can make a guess: there is an operation in UIApplicationMain that causes the main queue thread to become the main thread. The guess is as follows:

Since UIApplicationMain is a private API, we do not have the implementation code, but we know that the main thread runloop will be started after this function is called. To verify this, check the thread when manually starting runloop:

dispatch_block_t log = ^{ printf("is main thread: %zd\n", [NSThread isMainThread]); Dispatch_after (dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), log); } dispatch_async(globalQueue, ^{ dispatch_async(dispatch_get_main_queue(), log); }); [[NSRunLoop currentRunLoop] run];Copy the code

After runloop starts, all checks are YES:

// console log
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
"is main thread: 1"
Copy the code

The code runs to verify the conjecture, but the conclusion becomes:

thread -> runloop -> main thread

This conclusion can be easily overturned by starting a runloop of the Work Queue. Is it possible that only the thread that starts the runloop for the first time can become the main thread? To test this conjecture, modify the code further:

dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL); dispatch_block_t logSerial = ^{ printf("is main thread: %zd\n", [NSThread isMainThread]); Dispatch_after (dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), serialQueue, log); } dispatch_async(serialQueue, ^{ [[NSRunLoop currentRunLoop] run]; }); dispatch_async(globalQueue, ^{ dispatch_async(serialQueue, logSerial); }); dispatch_main();Copy the code

If the runloop of the child thread is started first, the output of all runs is NO, which means that the hypothesis that runloop has changed the priority of the thread is not valid. So two conditions based on the UIApplicationMain test code cannot explain what the main queue column is not running on the main thread

The main queue is not always executed on the same thread

After going back and forth, I found that the condition that the main queue always executes on the same thread limits the possibility of further enlarging the guess. To verify this condition, check whether this condition is true by regularly output the threadId of the main queue task:

thread_t threadId = mach_thread_self(); dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL); printf("current thread id is: %d\n", threadId); dispatch_block_t logMain = ^{ printf("=====main queue======> thread id is: %d\n", mach_thread_self()); Dispatch_after (dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), logMain); } dispatch_block_t logSerial = ^{ printf("serial queue thread id is: %d\n", mach_thread_self()); Dispatch_after (dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), serialQueue, logSerial); } dispatch_async(globalQueue, ^{ dispatch_async(serialQueue, logSerial); dispatch_async(dispatch_get_main_queue(), logMain); }); dispatch_main();Copy the code

Adding subqueues to the test code for periodic comparison, it is found that both the serial queue and the main queue may run on different threads. But if subqueues are removed for comparison, the main queue will only execute on one thread, but the threadId of that thread will always be different from the value we saved:

// console log
current thread id is: 775
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 7171"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 1547"
"serial queue thread id is: 1547"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 1547"
"serial queue thread id is: 1547"
"=====main queue======> thread id is: 6403"
"=====main queue======> thread id is: 1547"
"serial queue thread id is: 6403"
"serial queue thread id is: 4355"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 4355"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 4355"
"=====main queue======> thread id is: 4355"
"serial queue thread id is: 6403"
"serial queue thread id is: 1547"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 1547"
"=====main queue======> thread id is: 6403"
"serial queue thread id is: 1547"
"serial queue thread id is: 6403"
"=====main queue======> thread id is: 1547"
"serial queue thread id is: 1547"
Copy the code

The discovery of this new phenomenon, combined with the previous information, leads to a new guess:

A dedicated start thread is used to start the main thread runloop, and the main queue is executed by this thread before starting

It’s easy to test this conjecture by comparing threadId before and after runloop:

thread_t threadId = mach_thread_self(); printf("current thread id is: %d\n", threadId); dispatch_block_t logMain = ^{ printf("=====main queue======> thread id is: %d\n", mach_thread_self()); Dispatch_after (dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), logMain); } dispatch_block_t logSerial = ^{ printf("serial queue thread id is: %d\n", mach_thread_self()); Dispatch_after (dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), serialQueue, logSerial); } dispatch_async(globalQueue, ^{ dispatch_async(serialQueue, logSerial); dispatch_async(dispatch_get_main_queue(), logMain); }); [[NSRunLoop currentRunLoop] run]; // console log current thread id is: 775 "=====main queue======> thread id is: 775" "=====main queue======> thread id is: 775" "=====main queue======> thread id is: 775" "=====main queue======> thread id is: 775" "=====main queue======> thread id is: 775" "=====main queue======> thread id is: 775" "=====main queue======> thread id is: 775" "=====main queue======> thread id is: 775" "=====main queue======> thread id is: 775"Copy the code

The result shows that there is no starting thread. Once the runloop starts, the main queue is always executed on the same thread, which is the main thread. Since Runloop itself is an endless loop of events, this is why the main queue always runs on the main thread after it starts. Finally, in order to test the effect of starting runloop on the serial queue, we found another phenomenon after starting the sub-queue separately and together:

  • The home side columnrunloopOnce started, tasks are executed only by that thread
  • The son of the queuerunloopUnable to bind queue and thread execution relationship

Since async calls in the source code behave differently for the primary queue and the subqueue, the latter directly enables a thread to perform the tasks of the subqueue, which is why Runloop differentiates between the primary queue and the subqueue, and also shows that Apple didn’t make any major changes to libDispatch’s source code.

Interesting runloop wake up mechanism

If you’ve read any blogs or documentation about Runloop, you’ll probably know that it’s an endless loop of messages and events, but an endless loop is a huge CPU drain (spin locks are idling in an endless loop). In order to improve thread efficiency and reduce unnecessary wastage, runloop goes to sleep when there is no event processing if any timer, port or source exists. If one of the three is not present, runloop exits

So to discuss runloop wake up, we can keep runloop running by adding an empty port:

CFRunLoopRef runloop = NULL;
NSThread *thread = [[NSThread alloc] initWithBlock: ^{
    runloop = [NSRunLoop currentRunLoop].getCFRunLoop;
    [[NSRunLoop currentRunLoop] addPort: [NSMachPort new] forMode: NSRunLoopCommonModes];
    [[NSRunLoop currentRunLoop] run];
}];
Copy the code

The main discussion here is the fifth hamster big guy, the original question can be directly to the bottom of the link. The main things to note are the two apis mentioned in the question for adding tasks to the runloop:

CFRunLoopPerformBlock(runloop, NSRunLoopCommonModes, ^{
    NSLog(@"runloop perform block 1");
});

[NSObject performSelector: @selector(log) onThread: thread withObject: obj waitUntilDone: NO];

CFRunLoopPerformBlock(runloop, NSRunLoopCommonModes, ^{
    NSLog(@"runloop perform block 2");
});
Copy the code

If you omit the second perform call, the first call will not be output, and both will be output. By their names, both calls add execution tasks to the thread they are in. The difference is that the latter call does not actually insert a task block directly, but adds the task wrapped in a timer event that wakes up the Runloop. That is, of course, if the runloop is asleep.

CFRunLoopPerformBlock provides the ability to add tasks to a runloop without waking up the runloop, reducing the overhead of thread state switching when events are rare

other

After a long Spring Festival holiday, I feel in urgent need of a holiday to rest, but it is just a luxury. Due to the post-holiday syndrome, the state of reworking this week is not so good, and occasionally I feel unrefreshed. I hope I can recover as soon as possible. In addition, with the continuous accumulation, some strange problems that we think we are familiar with can always bring new cognition and harvest. I think this is the greatest joy of learning

About using code

Due to the differences between Swift syntax and OC syntax, the second code can not be well restored. If you are interested in this, you can pay attention to the hamster boss’s blog link below, and he will release the source code later. In addition, if you don’t want to read the libDispatch source code and want to understand the logic of this section, you can check out the linked article below

Further reading

Hamster bosses

Learn more about the COMMUNIST Party of China