hariszaman
hariszaman

Reputation: 8442

Running multiple background threads iOS

Is it possible to run multiple background threads to improve performance on iOS . Currently I am using the following code for sending lets say 50 network requests on background thread like this:

 dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
            // send 50 network requests 
 });

EDIT:

After updating my code to something like this no performance gain was achieved :( Taken from here

dispatch_queue_t fetchQ = dispatch_queue_create("Multiple Async Downloader", NULL);
dispatch_group_t fetchGroup = dispatch_group_create();

// This will allow up to 8 parallel downloads.
dispatch_semaphore_t downloadSema = dispatch_semaphore_create(8);

// We start ALL our downloads in parallel throttled by the above semaphore.
for (NSURL *url in urlsArray) {
    dispatch_group_async(fetchGroup, fetchQ, ^(void) {
        dispatch_semaphore_wait(downloadSema, DISPATCH_TIME_FOREVER);
        NSMutableURLRequest *headRequest = [NSMutableURLRequest requestWithURL:url  cachePolicy: NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];
        [headRequest setHTTPMethod: @"GET"];
        [headRequest addValue: cookieString forHTTPHeaderField: @"Cookie"];

         NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
        [NSURLConnection sendAsynchronousRequest:headRequest
                                           queue:queue // created at class init
                               completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
                                   // do something with data or handle error
                                   NSLog(@"request completed");
                               }];
        dispatch_semaphore_signal(downloadSema);
    });
}

// Now we wait until ALL our dispatch_group_async are finished.
dispatch_group_wait(fetchGroup, DISPATCH_TIME_FOREVER);

// Update your UI
dispatch_sync(dispatch_get_main_queue(), ^{
    //[self updateUIFunction];
});

// Release resources
dispatch_release(fetchGroup);
dispatch_release(downloadSema);
dispatch_release(fetchQ);

Upvotes: 1

Views: 3562

Answers (2)

Hamish
Hamish

Reputation: 80951

Be careful not to confuse threads with queues

A single concurrent queue can operate across multiple threads, and GCD never guarantees which thread your tasks will run on.

The code you currently have will submit 50 network tasks to be run on a background concurrent queue, this much is true.

However, all 50 of these tasks will be executed on the same thread.

GCD basically acts like a giant thread pool, so your block (containing your 50 tasks) will be submitted to the next available thread in the pool. Therefore, if the tasks are synchronous, they will be executed serially. This means that each task will have to wait for the previous one to finish before preceding. If they are asynchronous tasks, then they will all be dispatched immediately (which begs the question of why you need to use GCD in the first place).

If you want multiple synchronous tasks to run at the same time, then you need a separate dispatch_async for each of your tasks. This way you have a block per task, and therefore they will be dispatched to multiple threads from the thread pool and therefore can run concurrently.

Although you should be careful that you don't submit too many network tasks to operate at the same time (you don't say specifically what they're doing) as it could potentially overload a server, as gnasher says.

You can easily limit the number of concurrent tasks (whether they're synchronous or asynchronous) operating at the same time using a GCD semaphore. For example, this code will limit the number of concurrent operations to 6:

long numberOfConcurrentTasks = 6;

dispatch_semaphore_t semaphore = dispatch_semaphore_create(numberOfConcurrentTasks);

for (int i = 0; i < 50; i++) {

    dispatch_async(concurrentQueue, ^{

        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

        [self doNetworkTaskWithCompletion:^{
            dispatch_semaphore_signal(semaphore);
            NSLog(@"network task %i done", i);
        }];

    });

}

Edit

The problem with your code is the line:

dispatch_queue_t fetchQ = dispatch_queue_create("Multiple Async Downloader", NULL);

When NULL is passed to the attr parameter, GCD creates a serial queue (it's also a lot more readable if you actually specify the queue type here). You want a concurrent queue. Therefore you want:

dispatch_queue_t fetchQ = dispatch_queue_create("Multiple Async Downloader", DISPATCH_QUEUE_CONCURRENT);

You need to be signalling your semaphore from within the completion handler of the request instead of at the end of the request. As it's asynchronous, the semaphore will get signalled as soon as the request is sent off, therefore queueing another network task. You want to wait for the network task to return before signalling.

[NSURLConnection sendAsynchronousRequest:headRequest
                                       queue:queue // created at class init
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
                           // do something with data or handle error
                           NSLog(@"request completed");
                           dispatch_semaphore_signal(downloadSema);
                       }];

Edit 2

I just noticed you are updating your UI using a dispatch_sync. I see no reason for it to be synchronous, as it'll just block the background thread until the main thread has updated the UI. I would use a dispatch_async to do this.


Edit 3

As CouchDeveloper points out, it is possible that the number of concurrent network requests might be being capped by the system.

The easiest solution appears to be migrating over to NSURLSession and configuring the maxConcurrentOperationCount property of the NSOperationQueue used. That way you can ditch the semaphores altogether and just dispatch all your network requests on a background queue, using a callback to update the UI on the main thread.

I am not at all familiar with NSURLSession though, I was only answering this from a GCD stand-point.

Upvotes: 4

gnasher729
gnasher729

Reputation: 52632

You can send multiple requests, but sending 50 requests in parallel is usually not a good idea. There is a good chance that a server confronted with 50 simultaneous request will handle the first few and return errors for the rest. It depends on the server, but using a semaphore you can easily limit the number of running requests to anything you like, say four or eight. You need to experiment with the server in question to find out what works reliably on that server and gives you the highest performance.

And there seems to be a bit of confusion around: Usually all your network requests will run asynchronously. That is you send the request to the OS (which goes very quick usually), then nothing happens for a while, then a callback method of yours is called, processing the data. Whether you send the requests from the main thread or from a background thread doesn't make much difference.

Processing the results of these requests can be time consuming. You can process the results on a background thread. You can process the results of all requests on the same serial queue, which makes it a lot easier to avoid multithreading problems. That's what I do because it's easy and even in the worst case uses one processor for intensive processing of the results, while the other processor can do UI etc.

If you use synchronous network requests (which is a bad idea), then you need to dispatch each one by itself on a background thread. If you run a loop running 50 synchronous network requests on a background thread, then the second request will wait until the first one is completely finished.

Upvotes: 1

Related Questions