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)
