La programmazione multithread è un’operazione abbastanza delicata in qualunque linguaggio; se va male qualcosa di solito lo fa nella maniera peggiore possibile. Di solito si cerca sempre o di evitarla completamente se possibile (e difficilmente lo è al giorno d’oggi) o di testarne l’implementazione quanto più pesantemente, onde evitare le brutte soprese. Fortunatamente da OS X 10.5 in poi (e anche su Mac OS X), Apple ha introdotto alcune interessanti funzioni di gestione per la classe NSThread e ha aggiunto due nuovi oggetti: NSOperation e NSOperationQueue. Se si viene da Java la cosa più vicina a cui si possono associare è l’interfaccia Runnable; proprio come in Java l’oggetto NSOperation è disegnato per essere esteso ed implementare l’insieme di azioni che vanno eseguite in un threading; queste azioni vanno posizionate all’interno del metodo – (void) main {}. Il modo più facile per usare un oggetto NSOperation è quello di farlo eseguire da NSOperationQueue, che come è possibile intuire dal nome permette di mantenere una coda di azioni (oggetti NSOperation) che potranno essere anche esauriti in parallelo. Ecco un esempio tipico:
NSOperationQueue * queue = [[NSOperationQueue alloc] init]; NSOperation * myOperation = [[[MyOperationObject alloc] init] autorelease]; [queue addOperation: myOperation];
NSOperationQueue si occuperà di gestire la coda in maniera intelligente distribuendo al meglio il carico sull’hardware che c’è a disposizione. In questo post però non ci occuperemo di introdurre la gestione multithreading, ma analizzeremo un particolare caso che la riguarda: la possibilità di far gestire, all’interno di un NSOperation, un’operazione asincrona come può essere quella di NSURLConnection. Il problema in questa situazione è quello di mantenere vivo l’oggetto operation anche dopo che il codice all’interno del main è stato eseguito; una volta creato e fatto partire NSURLConnection (con delegato l’oggetto coda stesso) e terminato il codice del main, l’oggetto viene automaticamente “marcato” come eseguito e rimosso dal gestore; il risultato è che il nostro NSURLConnection non verrà mai realmente utilizzato. Come risolvere? NSOperation può lavorare in due modalità: concorrente e non concorrente. Generalmente una sottoclasse di NSOperation lavorerà, se non specificato, in modalità non concorrente (ovvero in parallelo); sebbene però la terminologia possa portare a confusione, le operazioni che vengono eseguite in parallelo sono definite non-concorrenti. Per avviare un’operazione in modalità concorrente è necesssario sovrascrivere il metodo – (BOOL) isConcurrent per ritornare YES:
- (BOOL)isConcurrent { return YES; }
Il primo passo è quello di far partire il nostro NSOperationQueue sul thread primario e da li attivare la connessione asincrona (su un altro thread) con NSURLConnection; a questo punto dovremmo prenderci cura dell’oggetto NSOperation e far si che non venga rimosso dalla coda prima che il download dei dati sia completato. Per far partire NSOperation nel thread principale sarà necessario sovrascrivere il metodo -start:
- (void)start { if (![NSThread isMainThread]) { [self performSelectorOnMainThread: @selector(start) withObject:nil waitUntilDone:NO]; return; } [super start]; } - (void) main { NSLog(@"our opeartion for %@ is now started.", _url); [self willChangeValueForKey:@"isExecuting"]; _isExecuting = YES; [self didChangeValueForKey:@"isExecuting"]; NSURLRequest * request = [NSURLRequest requestWithURL:_url]; _connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; if (_connection == nil) [self finish]; }
Analizziamo il codice: nella prima parte abbiamo fatto si che il l’operazione fosse forzata ad essere avviata nel main loop principale; poi abbiamo notificato in una variabile creata appositamente il fatto che l’operazione di download è iniziata. Infine abbiamo fatto partire il download. La seconda parte consiste nel creare un metodo – (void) finish che si occuperà di porre fine alla coda una volta esaurito il compito del download:
- (void)finish { [_connection release]; _connection = nil; [self willChangeValueForKey:@"isExecuting"]; [self willChangeValueForKey:@"isFinished"]; _isExecuting = NO; _isFinished = YES; [self didChangeValueForKey:@"isExecuting"]; [self didChangeValueForKey:@"isFinished"]; }
Il punto chiave è nelle variabili _isExecuting e _isFinished: soltanto quando sono rispettivamente NO e YES l’operazione potrà essere rimossa dala coda; i rispettivi valori sono monitorati tramite l’uso del key-value observing. Infine sarà necessario implementare il delegato di NSURLConnection che si occuperà di chiamare, dove necessario (quindi nei casi di download completo o fallito) il metodo finish.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { [_data release]; _data = [[NSMutableData alloc] init]; NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)response; _statusCode = [httpResponse statusCode]; } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [_data appendData:data]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self finish]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { _error = [error copy]; [self finish]; }
Come abbiamo visto non è stato necessario implementare le API di download in modo sincrono; questa tecnica ci ha permesso di pacchettizzare facilmente all’interno di una coda una funzione asincrona. Una funzione del genere può essere particolarmente utile quando si ha a che fare con la gestione di più download simultaneamente ed è necessario monitorarne lo stato (cosa che non sarebbe possibile con una normale chiamata sincrona) o mantenere un massimo di attività contemporaneamente (facendo uso di -(void) setMaxConcurrentOperationCount: su NSOperationQueue). Cliccando qui potrete scaricare la classe RFDataDownloader (1.0.1) che gestisce quanto detto in maniera completamente trasparente. [AGGIORNATO|14 Marzo]
E’ stato corretto un bug che impediva l’aggiunta di più download simultanei. (Download)