Question

So I'm creating my download on the main thread

NSURLRequest *request = [NSURLRequest requestWithURL:download.URL];
NSURLSessionDownloadTask *downloadTask = [self.downloadSession downloadTaskWithRequest:request];
[downloadTask resume];

and adding the NSManagedContextID associated with the download to an NSMutableDictionary, so I can retrieve it later in the delegate call-backs

[self.downloads setObject:[download objectID] forKey:[NSNumber numberWithInteger:downloadTask.taskIdentifier]];

My self.downloadSession above is configured like this

- (NSURLSession *)backgroundSession
{
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.test.backgroundSession"];
    configuration.discretionary = YES;
    session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
});
return session;
}

My problem is the delegate callbacks seem to be called on different threads

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{   
    NSManagedObjectID *downloadID = [self.downloads objectForKey:[NSNumber numberWithInteger:downloadTask.taskIdentifier]];

    double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;

    NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:downloadID,@"download",[NSNumber numberWithDouble:progress],@"progress", nil];

    [[NSNotificationCenter defaultCenter] postNotificationName:@"DownloadProgress" object:nil userInfo:userInfo];

} 

So when I access self.downloads to get the correct objectID, I'm actually accessing the NSMutableDictionary from a different thread than the one it was created on, and I believe NSMutableDictionary isn't thread safe. So what is the best solution for this, I could used something like this

session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];

when declaring the session, set the delegate queue to the mainQueue which causes all the delegates to be called on the main thread, but I would like to keep all the callbacks on a background thread if possible

Was it helpful?

Solution

In your example that is not a problem, since your dictionary is handed over to the notification system and is not used by the operation queue thread again. Thread safety is only a problem when an object is potentially accessed from multiple threads at the same time.

If your dict would be an iVar you should do it this way:

Create your own queue like this

myQueue = [[NSOperationQueue alloc] init];
// This creates basically a serial queue, since there is just on operation running at any time.
[myQueue setMaxConcurrentOperationCount:1];

Then schedule every Access to your dictionary on this queue, like this for example:

[myQueue addOperationWithBlock:^
{
    // Access your dictionary 
}];

And of course use this queue for your URLSesson delegation:

session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:myQueue];

Since this queue is setup as a serial queue, there will always be just one thread accessing the dict in the background.

Take care when you calculate something with the dict information. You have to do this on that queue as well. However, you can put the result of your calculation on any other queue/thread, for example to update the UI on the main thread.

[myQueue addOperationWithBlock:^
{
    // Calculate with your dictionary
    // Maybe the progress calcualtion
    NSString* progress = [self calculateProgress: iVarDict];
    dispatch_async(dispatch_get_main_queue(), ^
    {
       // use progress to update UI 
    });
}];

I think for posting a notification you don't have to use that pattern, because the system handles the threading correctly. But to be save you should check this.

OTHER TIPS

You can use a GCD serial queue to ensure only one delegate is executing simultaneously.

You can declare the queue as an instance variable of your class and initialize it in the init method, like this:

dispatch_queue_t delegateQueue;

...

delegateQueue = dispatch_queue_create("com.yourcompany.mydelegatequeue", 0x0);

and in your delegate method, simply make it execute in this queue:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{   
    dispatch_sync(delegateQueue, ^{
    NSManagedObjectID *downloadID = [self.downloads objectForKey:[NSNumber numberWithInteger:downloadTask.taskIdentifier]];

    double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;

    NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:downloadID,@"download",[NSNumber numberWithDouble:progress],@"progress", nil];

    [[NSNotificationCenter defaultCenter] postNotificationName:@"DownloadProgress" object:nil userInfo:userInfo];
});

} 

This way, although every delegate is called in its thread, there is only of them accessing self.downloads at one time, and you can keep them in separate threads.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top