Темы NSURLSession: отслеживание загрузки нескольких фонограмм

Итак, я создаю свою загрузку в основном потоке

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

и добавив NSManagedContextID, связанный с загрузкой, в NSMutableDictionary, поэтому я могу получить его позже в call-backs делегата

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

Мой self.downloadSession выше настроен следующим образом

- (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;
}

Моя проблема заключается в том, что обратные вызовы делегатов, кажется, вызываются в разных потоках

- (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];

} 

Поэтому, когда я получаю доступ к self.downloads, чтобы получить правильный идентификатор объекта, я фактически получаю доступ к NSMutableDictionary из другого потока, чем тот, на котором он был создан, и я считаю, что NSMutableDictionary не является потокобезопасным. Итак, какое лучшее решение для этого, я мог бы использовать что-то вроде этого

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

при объявлении сеанса установите очередь делегатов в mainQueue, которая вызывает всех делегатов для вызова в основном потоке, но я хотел бы сохранить все обратные вызовы в фоновом потоке, если это возможно.

Ответы

Ответ 1

В вашем примере это не проблема, так как ваш словарь передается системе уведомлений и больше не используется потоком очереди операций. Безопасность потоков - это только проблема, когда объект потенциально доступен из нескольких потоков одновременно.

Если ваш dict будет iVar, вы должны сделать это следующим образом:

Создайте свою собственную очередь следующим образом

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

Затем планируйте каждый доступ к вашему словарю в этой очереди, например:

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

И, конечно же, используйте эту очередь для делегирования URLSesson:

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

Так как эта очередь настроена как последовательная очередь, всегда будет только один поток, обращающийся к dict в фоновом режиме.

Позаботьтесь, когда вы вычислите что-то с информацией о дикторе. Вы должны сделать это и в этой очереди. Однако вы можете поместить результат своих вычислений в любую другую очередь/поток, например, для обновления пользовательского интерфейса в основном потоке.

[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 
    });
}];

Я думаю, что для публикации уведомления вам не нужно использовать этот шаблон, потому что система правильно обрабатывает потоки. Но чтобы быть спасенным, вы должны это проверить.

Ответ 2

Вы можете использовать последовательную очередь GCD для обеспечения одновременного выполнения только одного делегата.

Вы можете объявить очередь как переменную экземпляра вашего класса и инициализировать ее в методе init, например:

dispatch_queue_t delegateQueue;

...

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

и в методе делегирования просто выполните его в этой очереди:

- (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];
});

} 

Таким образом, хотя каждый делегат вызывается в своем потоке, есть только один из них, который позволяет получить доступ к файлам self.downloads за один раз, и вы можете сохранить их в отдельных потоках.