NSURLSession with NSBlockOperation and queues
Asked Answered
V

3

55

I have an app that currently uses NSURLConnection for the vast majority of its networking. I would like to move to NSURLSession because Apple tells me that is the way to go.

My app just uses the synchronous version of NSURLConnection by way of the + (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error class method. I do this within a NSBlockOperation running on an NSOperationQueue so I am not needlessly blocking the main queue. The big advantage to doing things this way is that I can make the operations dependent on one another. For example, I can have the task that is requesting data be dependent on the login task finishing.

I have not seen any support for synchronous operations within NSURLSession. All I can find are articles deriding me for even thinking of using it synchronously and that I am a horrible person for blocking the threads. Fine. But I see no way to make NSURLSessionTasks dependent on each other. Is there a way to do that?

Or is there a description of how I would do such a thing in a different way?

Vaporizer answered 18/1, 2014 at 0:22 Comment(3)
There is one place where a synchronous NSURLSession is very useful, and is the easiest way to go. When writing a command line utility that goes out and interacts with the web, there is no reason to be asynchronous unless you intend to spin off multiple requests at once. To make it synchronous, I add a semaphore lock around it. Without that, the app will exit before the request finishes because there is nothing else (i.e. the GUI runloop) to keep the app alive. The great majority of iOS/OS/X programmers don't do this, so the subject doesn't come up very much.Underdog
Thanks for this (and to @Fecteau for the answer). Everywhere I look, all I see is a bunch of nannies whining about how you should never do synchronous requests, instead of answering the question. There are times when you need to be synchronous - in my case, I'm dealing with a third-party library that makes callbacks to my code, which needs to do url requests and not return to the library until the request completes.Restricted
Rob's answer is great. I was also frustrated by the number of answers I stumbled across saying "just use async". I've rolled the accepted answer up into an iOS Category if anyone wants an easy to consume, drop-in replacement for NSURLConnection sendSynchronousRequest:.Siftings
F
108

The harshest criticisms of synchronous network requests are reserved for those who do it from the main queue (as we know that one should never block the main queue). But you're doing it on your own background queue, which addresses the most egregious problem with synchronous requests. But you're losing some wonderful features that asynchronous techniques provide (e.g. cancelation of requests, if needed).

I'll answer your question (how to make NSURLSessionDataTask behave synchronously) below, but I'd really encourage you to embrace the asynchronous patterns rather than fighting them. I'd suggest refactoring your code to use asynchronous patterns. Specifically, if one task is dependent upon another, simply put the initiation of the dependent task in the completion handler of the prior task.

If you have problems in that conversion, then post another Stack Overflow question, showing us what you tried, and we can try to help you out.


If you want to make an asynchronous operation synchronous, a common pattern is to use a dispatch semaphore so your thread that initiated the asynchronous process can wait for a signal from the completion block of the asynchronous operation before continuing. Never do this from the main queue, but if you're doing this from some background queue, it can be a useful pattern.

You can create a semaphore with:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

You can then have the completion block of the asynchronous process signal the semaphore with:

dispatch_semaphore_signal(semaphore);

And you can then have the code outside of the completion block (but still on the background queue, not the main queue) wait for that signal:

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

So, with NSURLSessionDataTask, putting that all together, that might look like:

[queue addOperationWithBlock:^{

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    NSURLSession *session = [NSURLSession sharedSession]; // or create your own session with your own NSURLSessionConfiguration
    NSURLSessionTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (data) {
            // do whatever you want with the data here
        } else {
            NSLog(@"error = %@", error);
        }

        dispatch_semaphore_signal(semaphore);
    }];
    [task resume];

    // but have the thread wait until the task is done

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    // now carry on with other stuff contingent upon what you did above
]);

With NSURLConnection (now deprecated), you have to jump through some hoops to initiate requests from a background queue, but NSURLSession handles it gracefully.


Having said that, using block operations like this means that the operations won't respond to cancellation events (while they're running, at least). So I generally eschew this semaphore technique with block operations and just wrap the data tasks in asynchronous NSOperation subclass. Then you enjoy the benefits of operations, but you can make them cancelable, too. It's more work, but a much better pattern.

For example:

//
//  DataTaskOperation.h
//
//  Created by Robert Ryan on 12/12/15.
//  Copyright © 2015 Robert Ryan. All rights reserved.
//

@import Foundation;
#import "AsynchronousOperation.h"

NS_ASSUME_NONNULL_BEGIN

@interface DataTaskOperation : AsynchronousOperation

/// Creates a operation that retrieves the contents of a URL based on the specified URL request object, and calls a handler upon completion.
///
/// @param  request                    A NSURLRequest object that provides the URL, cache policy, request type, body data or body stream, and so on.
/// @param  dataTaskCompletionHandler  The completion handler to call when the load request is complete. This handler is executed on the delegate queue. This completion handler takes the following parameters:
///
/// @returns                           The new session data operation.

- (instancetype)initWithRequest:(NSURLRequest *)request dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler;

/// Creates a operation that retrieves the contents of a URL based on the specified URL request object, and calls a handler upon completion.
///
/// @param  url                        A NSURL object that provides the URL, cache policy, request type, body data or body stream, and so on.
/// @param  dataTaskCompletionHandler  The completion handler to call when the load request is complete. This handler is executed on the delegate queue. This completion handler takes the following parameters:
///
/// @returns                           The new session data operation.

- (instancetype)initWithURL:(NSURL *)url dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler;

@end

NS_ASSUME_NONNULL_END

and

//
//  DataTaskOperation.m
//
//  Created by Robert Ryan on 12/12/15.
//  Copyright © 2015 Robert Ryan. All rights reserved.
//

#import "DataTaskOperation.h"

@interface DataTaskOperation ()

@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, weak) NSURLSessionTask *task;
@property (nonatomic, copy) void (^dataTaskCompletionHandler)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error);

@end

@implementation DataTaskOperation

- (instancetype)initWithRequest:(NSURLRequest *)request dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler {
    self = [super init];
    if (self) {
        self.request = request;
        self.dataTaskCompletionHandler = dataTaskCompletionHandler;
    }
    return self;
}

- (instancetype)initWithURL:(NSURL *)url dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler {
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    return [self initWithRequest:request dataTaskCompletionHandler:dataTaskCompletionHandler];
}

- (void)main {
    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:self.request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        self.dataTaskCompletionHandler(data, response, error);
        [self completeOperation];
    }];

    [task resume];
    self.task = task;
}

- (void)completeOperation {
    self.dataTaskCompletionHandler = nil;
    [super completeOperation];
}

- (void)cancel {
    [self.task cancel];
    [super cancel];
}

@end

Where:

//
//  AsynchronousOperation.h
//

@import Foundation;

@interface AsynchronousOperation : NSOperation

/// Complete the asynchronous operation.
///
/// This also triggers the necessary KVO to support asynchronous operations.

- (void)completeOperation;

@end

And

//
//  AsynchronousOperation.m
//

#import "AsynchronousOperation.h"

@interface AsynchronousOperation ()

@property (nonatomic, getter = isFinished, readwrite)  BOOL finished;
@property (nonatomic, getter = isExecuting, readwrite) BOOL executing;

@end

@implementation AsynchronousOperation

@synthesize finished  = _finished;
@synthesize executing = _executing;

- (instancetype)init {
    self = [super init];
    if (self) {
        _finished  = NO;
        _executing = NO;
    }
    return self;
}

- (void)start {
    if ([self isCancelled]) {
        self.finished = YES;
        return;
    }

    self.executing = YES;

    [self main];
}

- (void)completeOperation {
    self.executing = NO;
    self.finished  = YES;
}

#pragma mark - NSOperation methods

- (BOOL)isAsynchronous {
    return YES;
}

- (BOOL)isExecuting {
    @synchronized(self) {
        return _executing;
    }
}

- (BOOL)isFinished {
    @synchronized(self) {
        return _finished;
    }
}

- (void)setExecuting:(BOOL)executing {
    @synchronized(self) {
        if (_executing != executing) {
            [self willChangeValueForKey:@"isExecuting"];
            _executing = executing;
            [self didChangeValueForKey:@"isExecuting"];
        }
    }
}

- (void)setFinished:(BOOL)finished {
    @synchronized(self) {
        if (_finished != finished) {
            [self willChangeValueForKey:@"isFinished"];
            _finished = finished;
            [self didChangeValueForKey:@"isFinished"];
        }
    }
}

@end
Fecteau answered 18/1, 2014 at 15:19 Comment(5)
The method you show looks promising, but I decided to heed your other advice and I spent some time working on being asynchronous. My biggest fight was trying to replicate dependencies. I ended up using an NSCondition to simulate that feature, but I'm not confident that I implemented it correctly. It would be nice if the NSURLSessionDataTask supported NSOperation so I could call addDependency. :-)Vaporizer
@ErikAllen There's absolutely nothing that prevents you from wrapping the NSURLSessionTask in a concurrent NSOperation subclass, and then enjoy dependencies (and if doing many concurrent requests, controlling the degree of concurrency). If you just want a simple "when login task is done, start another task", you can use the rendition of dataTaskWithURL with completionHandler for the login task, and inside the `completionHandler, initiate the next task(s).Fecteau
If I have 4 dependent tasks, A -> B -> C -> D, would it be good practice to go 3 levels deep with completion handlers inside completion handlers inside completion handlers...?Isma
@ArthurThompson - If I had that sort of nesting, I might suggest separate functions whose names indicate their role. E.g. A might be "login", B might be "retrieveTableOfContents", C might be "retrieveDetails" and D might be "retrieveImages". Then each function might just call the next function in its completion handler, resolving the dreaded nesting and making it far more comprehensible. The other approach would be the asynchronous operation approach, and then you can create the four operations and either declare dependencies between them or just add them to a serial queue.Fecteau
if i hit the api and get "401" response which means that my session has expired..so for that i have to hit the login api and then the api which is previously executed will be hitted again....i have to do with nsurlsession..how can we execute this?Keelboat
C
2

@Rob I would encourage you to post your reply as a solution, in view of the following documentation note from NSURLSession.dataTaskWithURL(_:completionHandler:):

This method is intended as an alternative to the sendAsynchronousRequest:queue:completionHandler: method of NSURLConnection, with the added ability to support custom authentication and cancellation.

Commission answered 21/11, 2014 at 10:6 Comment(0)
R
0

If semaphore based approach doesn't work, try polling based approach.

var reply = Data()
/// We need to make a session object.
/// This is key to make this work. This won't work with shared session.
let conf = URLSessionConfiguration.ephemeral
let sess = URLSession(configuration: conf)
let task = sess.dataTask(with: u) { data, _, _ in
    reply = data ?? Data()
}
task.resume()
while task.state != .completed {
    Thread.sleep(forTimeInterval: 0.1)
}
FileHandle.standardOutput.write(reply)

Polling based approach works very reliably, but effectively limits maximum throughput to polling interval. In this example, it's been limited to 10 times/sec.


Semaphore based approach has been worked well so far, but since Xcode 11 era, it's getting broken. (maybe only for me?)

A data task does not finish if I wait for semaphores. If I wait for semaphore on different thread, it task fails with an error.

nw_connection_copy_protocol_metadata [C2] Client called nw_connection_copy_protocol_metadata on unconnected nw_connection error.

It seems something has been changed in the implementation as Apple is moving Network.framework.

Ranna answered 15/10, 2019 at 10:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.