In general, we use asynchronous requests to exchange data, wait for the data to return and then call back to perform the required operations. The advantage of this method is that there is no need to block threads to wait for the result of the request. But in some special scenarios, we need to use synchronous waiting for data to get data, such as in Aliyun's oss to get token.
In previous versions of iOS 9.0, we could send synchronization requests to get data from NSURLConnection.
+ (nullable NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse * _Nullable * _Nullable)response error:(NSError **)error API_DEPRECATED("Use [NSURLSession dataTaskWithRequest:completionHandler:] (see NSURLSession.h", macos(10.3,10.11), ios(2.0,9.0), tvos(9.0,9.0)) __WATCHOS_PROHIBITED;
But in the version after iOS 9.0, NSURLConnection is gradually replaced by NSURLSession, and the original synchronization interface is gradually fading out of sight. What about using NSURLSession to implement synchronization requests? Following is an example of AFN to provide two ideas for implementation.
Use of Signal Quantity
A semaphore can be understood as a resource counter. There are two operations for a semaphore to achieve mutual exclusion, namely P and V operations. Generally, it is critical access or mutually exclusive access: set the semaphore value to 1, when a process A runs, use resources to perform P operation, that is, reduce the semaphore value by 1, that is, the number of resources is one less. At this point the semaphore value is 0. The system stipulates that when the semaphore value is 0, it must wait until the semaphore value is not zero to continue operation. At this time, if process B wants to run, it must also do P operation, but at this time the signal is 0, so it can not be reduced by 1, that is, it can not operate P, it will block. This achieves exclusive access to process A. When process A runs, it releases resources and carries out V operation. When the number of resources is re-added to 1, the semaphore value becomes 1. Then process B finds that the number of resources is not zero. The semaphore can perform P operation and immediately perform P operation. The semaphore value changes to 0 again, process B has resources, and the rest of the threads have to wait to achieve exclusive access to thread B. This is how semaphores control thread exclusion.
In iOS, semaphore implementation depends on the following functions:
dispatch_semaphore_create(long value); //create a semaphore dispatch_semaphore_signal(dispatch_semaphore_t dsema); //Send a signal dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); //wait for the signal
The first function has a long shaping parameter, which can be understood as the total amount of the signal. dispatch_semaphore_signal is to send a signal, which naturally adds 1 to the total amount of the signal. dispatch_semaphore_wait waits for the signal. When the total amount of the signal is less than 0, it will wait all the time. Otherwise, it can be executed normally and let the signal be executed. Total amount - 1, according to this principle, can achieve a "synchronous request":
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; __block id _responseObject = nil; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [manager GET:url parameters:params progress:^(NSProgress * _Nonnull downloadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { _responseObject = responseObject; dispatch_semaphore_signal(semaphore); } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { _responseObject = nil; dispatch_semaphore_signal(semaphore); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"Get results:%@", _responseObject);
Here's a minor point to note: dispatch_semaphore_wait is a method that blocks threads, and AFN's callback code defaults to be in the main queue, so this operation can cause deadlock if placed in the main thread: dispatch_semaphore_wait blocks the main thread and needs to wait for a signal to unblock the thread. Blocking, the thread cannot continue to perform tasks until the thread blocks contact. But if the callback is also in the main queue, because the main thread has been blocked, the operation in the callback will never be executed, causing the main thread to be permanently blocked.
So there are two solutions:
- Place this operation in a non-main thread: for example, place requests in a custom queue
dispatch_queue_t queue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH); dispatch_async(queue, ^{ AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; __block id _responseObject = nil; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [manager GET:url parameters:params progress:^(NSProgress * _Nonnull downloadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { _responseObject = responseObject; dispatch_semaphore_signal(semaphore); } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { _responseObject = nil; dispatch_semaphore_signal(semaphore); }]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"Get results:%@", _responseObject); });
- Set up a callback operation queue as a non-primary queue:
manager.completionQueue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_DEFAULT);
Use lock
In multi-threaded development, lock is a tool specially used to control resource competition access privileges to ensure that at most one thread can access resources at the same time to ensure the integrity of resources. Therefore, we can also use lock to achieve "synchronization" request operation.
There are two main functions of NSCondition lock: locking and monitoring. When adding locks, we can monitor whether the specified conditions are met. If not, we can use wait method to block threads, wait for signal or broadcast signal, and try to lock again after receiving signals, if the lock is added. Work, thread blocking is released, otherwise continue blocking.
@interface NSCondition : NSObject <NSLocking> { @private void *_priv; } - (void)wait; - (BOOL)waitUntilDate:(NSDate *)limit; - (void)signal; - (void)broadcast; @property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); @end
The code for synchronization using NSCondition s looks like this:
NSCondition *lock = [[NSCondition alloc] init]; __block NSDictionary *_responseObject = nil; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; [manager GET:url parameters:params progress:^(NSProgress * _Nonnull downloadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { [lock lock]; _responseObject = responseObject; [lock signal]; [lock unlock]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { [lock lock]; _responseObject = @{}; [lock signal]; [lock unlock]; }]; [lock lock]; if (!_responseObject) { [lock wait]; } [lock unlock]; NSLog(@"responseObject == %@", _responseObject);
Similarly, try not to place the request operation in the main thread, so that the callback thread (the main thread) will activate the blocking current thread by sending a signal; if it has to be placed in the main thread, it will need to set the callback of the request (actually the operation of sending a signal) to execute in other threads.
NSConditionLock can also be used to implement:
dispatch_queue_t queue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_HIGH); dispatch_async(queue, ^{ NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0]; __block NSDictionary *_responseObject = nil; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; [manager GET:url parameters:params progress:^(NSProgress * _Nonnull downloadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { [lock lock]; _responseObject = responseObject; [lock unlockWithCondition:1]; } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { [lock lock]; _responseObject = @{}; [lock unlockWithCondition:1]; }]; [lock lockWhenCondition:1]; NSLog(@"responseObject == %@", _responseObject); [lock unlock]; });