Use an NSRecursive Lock to wait for an operation to finish
Would it be save to use an NSRecursiveLock to开发者_高级运维 purposefully wait for an operation to complete on a background thread? Here is an example:
I have a class where I want to have a loadParts function that can be asynchronous or synchronous. The asynchronous function will possibly be called early so that the parts can be loaded before the data is actually needed. The synchronous one should check if the data has been loaded, or is being loaded currently. If it has been loaded it can just return the data; if it is currently loaded, then it should wait for it to be loaded and then return; and if it isn't even being loaded, then it should just load it synchronously. This is the code I am trying to use:
// Private function to be run either on main thread or
// background thread
-(void)_loadParts
{
[_loadingPartsLock lock];
_loadingParts = YES;
// Do long loading operation
_loadingParts = NO;
_partsLoaded = YES;
[_loadingPartsLock unlock];
}
// Asynchronous loading of parts
-(void)preloadParts
{
if( _loadingParts || _partsLoaded )
return;
[self performSelectorInBackground:@selector(_loadParts) withObject:nil];
}
// Synchronous loading of parts
-(void)loadParts
{
if( _loadingParts )
{
[_loadingPartsLock lock];
[_loadingPartsLock unlock];
}
if( !_partsLoaded )
{
[self _loadParts];
}
}
Is this safe / an efficient way to do this? I already see some possible problems with it. Is it thread safe to set and test the value of a BOOL without a lock? I am also locking twice in the synchronous function if it is called while the background thread is still loading.
Is there a more common and better way to achieve this functionality?
Thanks!
A far, far, better solution is to use a dispatch_queue
or NSOperationQueue
(configured for serial operation).
Enqueue your loading operations and then enqueue whatever is supposed to happen when it is done. If the "done" operation is "tell the main thread to update", that's fine -- perform a method on your main thread that is effectively an event that triggers the update in response to the now loaded data.
This avoids the issues and overhead associated with locking entirely while also solving the "is done" notification issue without requiring some kind of polling mechanism.
Inspired by bbum's answer I found a solution that uses NSOperationQueue but not quite in the way he described. In my preloadParts function I create and store a load operation that is an instance of NSInvocationOperation that runs my background thread function. I then add it to the NSOperationQueue.
If at any point, the data is requested by another class. I first check if the data is loaded (the variable is set). If not, I check if the operation is in the queue. If it is, then I call [_loadOperation waitUntilFinished]. Otherwise, I add it to the operation queue with the argument to waitUntilFinished. Here is the code I came up with:
-(void)preloadCategories
{
if( [[_operationQueue operations] containsObject:_loadOperation] )
return;
[_operationQueue addOperation:_loadOperation];
}
-(CCPart*)getCategoryForName:(NSString*)name
{
if( nil == _parts )
{
[self loadCategories];
}
return [_parts objectForKey:name];
}
-(void)loadCategories
{
if( nil != _parts )
return;
if( [[_operationQueue operations] containsObject:_loadOperation] )
{
[_loadOperation waitUntilFinished];
}
else
{
[_operationQueue addOperations:[NSArray arrayWithObject:_loadOperation]
waitUntilFinished:YES];
}
}
-(void)_loadCategories
{
// Function that actually does the loading and sets _parts to be an array of the data
_parts = [NSArray array];
}
In the initialization function I set the _operationQueue and _loadOperation as follows:
_operationQueue = [[NSOperationQueue alloc] init];
_loadOperation = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(_loadCategories)
object:nil];
精彩评论