Controlling concurrent network requests using GCD semaphores in iOS

lead

Anyone who knows about the computer will know the role of semaphores. When multiple threads want to access the same resource, we often set a semaphore. When the semaphore is greater than 0, the new thread can operate the resource. The semaphore is - 1 during operation and + 1 after operation. When the semaphore is equal to 0, we must wait, so by controlling the semaphore, We can control the number of concurrent operations that can be performed simultaneously.

In the development of network requests, I often encounter two situations. One is that I need to request multiple data at the same time in one interface, such as list data and advertising data, and refresh the interface together after all requests are received. The other is that my requests must meet a certain order. For example, I must first request personal information, and then request relevant content according to personal information. These requirements can achieve concurrency control and dependent operation for ordinary operations, but for network requests, which require time, the effect is often different from what is expected. At this time, semaphores need to be used for control.

GCD semaphore

A semaphore is an integer. When it is created, it will have an initial value, which often represents the concurrency of simultaneous operations I want to control. In operation, there are two operations for semaphores: signal notification and waiting. During signal notification, the semaphore will be + 1. During waiting, if the semaphore is greater than 0, the semaphore will be - 1. Otherwise, it will wait until the semaphore is greater than 0. When will it be greater than zero? Often, after a previous operation, we send a signal notification to make the semaphore + 1.

After finishing the concept, let's take a look at the three semaphore operations in GCD:

  • dispatch_semaphore_create: create a semaphore
  • dispatch_semaphore_signal: signal notification, that is, let the semaphore + 1
  • dispatch_semaphore_wait: wait until the semaphore is greater than 0. At the same time, set the semaphore to - 1

When in use, one semaphore is often created, and then multiple operations are performed. Each operation waits until the semaphore is greater than 0. At the same time, the signal is on - 1. After the operation, the semaphore is + 1, similar to the following process:

dispatch_semaphore_t sema = dispatch_semaphore_create(5);
for (100 Second cycle operation) {
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // operation
        dispatch_semaphore_signal(sema);
    });
}

The above code indicates that I need to operate 100 times, but the control allows concurrent operations only 5 times at most. When the concurrency reaches 5, the semaphore is reduced to 0. At this time, the wait operation will work. DISPATCH_TIME_FOREVER indicates that it will wait forever until the semaphore is greater than 0, that is, when an operation is completed and the semaphore is + 1, the waiting can be ended, the operation can be carried out, and the semaphore is - 1, so that a new task has to wait.

Unified operation after multiple requests

Suppose we need to make multiple requests on a page at the same time. They don't require sequential relationship, but they are required to refresh the interface or other operations after all their requests are completed.

We can generally use GCD group and notify to meet this requirement:

    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //Request 1
        NSLog(@"Request_1");
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //Request 2
        NSLog(@"Request_2");
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //Request 3
        NSLog(@"Request_3");
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        //Interface refresh
        NSLog(@"All tasks are completed, refresh the interface");
    });

The function of notify is to operate your own content after all other operations in the group are completed, so we will see that the content refreshed in the interface is printed after the above three contents are printed.

But when the above three operations are changed into real network operations, this simple approach will become invalid. Why? Because the network request takes time, and the execution of the thread does not wait until the request is completed, but is only responsible for sending the request. The thread considers its task completed. When all three requests are sent, it will execute the contents in notify. However, the return time of the request result is not certain, which leads to the refresh of the interface and the return of the request, This is invalid.

To solve this problem, we need to use the semaphores mentioned above to operate.

Before each request starts, we create a semaphore, initially 0. After the request operation, we set a dispatch_semaphore_wait, after the result is requested, add the semaphore + 1, that is, dispatch_semaphore_signal. The purpose of this is to ensure that the thread will wait until the request result is returned. If the task of such a thread is waiting all the time, it will not be counted as completed, and the content of notify will not be executed. The thread task can not end until the result of each request is returned. At this time, notify can also be executed. The pseudo code is as follows:

dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[Network request:{
        success: dispatch_semaphore_signal(sema);
        Failed: dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

Multiple requests are executed sequentially

Sometimes we need to execute multiple requests in order. For example, we first request the user information, and then request the relevant data according to the content of the user information. In normal code, we can write the code directly in order, but here, because the relationship between multiple threads is involved, it is called thread dependency.

It is troublesome to use GCD for thread dependency. It is recommended to use NSOperationQueue to set the dependency between tasks more conveniently.

    // 1. Task 1: obtain user information
    NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
        [self request_A];
    }];
 
    // 2. Task 2: request relevant data
    NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
        [self request_B];
    }];
 
    // 3. Set dependency
    [operation2 addDependency:operation1];// Task 2 depends on task 1
 
    // 4. Create a queue and join the task
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperations:@[operation2, operation1] waitUntilFinished:NO];

This is possible for general multithreading operations. Thread 2 will wait for thread 1 to complete before executing. But for network requests, the problem comes again. Similarly, network requests take time. After the thread sends the request, it is considered that the task is completed and will not wait for the operation after return, which is meaningless.

To solve this problem, semaphores are used to control. In fact, the code is the same. In a task operation:

dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[Network request:{
        success: dispatch_semaphore_signal(sema);
        Failed: dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

Or wait for the request to return before the task ends. Dependencies are implemented through NSOperationQueue.

junction

In fact, in the final analysis, the central idea is to use semaphores to control when thread tasks are considered to be finished. If semaphores are not used, the task is considered to be completed after the request is sent, and the network request will take different times, so the order will be disrupted. Therefore, when a semaphore is used to control a single thread operation, you must wait for the request to return. After the operation you want to perform is completed, you can add the semaphore + 1. At this time, the code that has been waiting can also be executed, and the task can be counted as completed.

Through this method, we can solve some unexpected multithreading problems caused by the time-consuming characteristics of network requests.

reference material: 1,http://www.cocoachina.com/ios/20170428/19150.html 2,http://blog.csdn.net/fhbystudy/article/details/25918451

Added by railgun on Sat, 08 Jan 2022 04:07:35 +0200