标签:ios-多线程
Here’s a quick comparison of the two that will help you decide when and where to use GCD or NSOperation and NSOperationQueue:
Every application has at least one thread known as the main thread. A thread’s job is to execute a sequence of instructions. In Cocoa Touch, the main thread contains the application’s main run loop. Nearly all application code that you write gets executed on the main thread, unless you specifically create a separate thread and execute some code in the new thread.
Threads have two specific characteristics:
Therefore, it is important to be aware of techniques to overcome these issues, and prevent unexpected errors! :] This is a brief list of the challenges that multi-threaded applications face — and some tips on how to deal with them effectively.
Race Condition: the fact that every thread can access the same memory may cause what is known as a race condition.
When multiple concurrent threads access shared data, the thread that gets to the memory first will change the shared data — and there’s no guarantee which thread will get there first. You might assume a variable has the value your thread last wrote to this shared memory, but another thread may have changed the shared memory in the meantime, and your variable is out of date!
If you know that this condition might exist in your code (i.e. you know that you are going to read / write data concurrently from multiple threads) you should use mutex lock. Mutex stands for “mutual exclusion”. You create a mutex lock for instance variables by wrapping it around a “@synchronized block”. This way you make sure that the code within it is accessed only by one thread at a time:
@synchronized (self) {
myClass.object = value;
}
“Self” in the above code is called a “semaphore”. When a thread reaches this piece of code, it checks to see if any other thread is accessing “self”. If nobody else is accessing “self”, it executes the block; otherwise execution of the thread is blocked until the mutex lock becomes available.
Atomicity: you have likely seen “nonatomic” in property declarations numerous times. When you declare a property as atomic, it is usually wrapped in a @synchronized block to make it thread safe. Of course, this approach does add some extra overhead. To give you an idea, here is a rough implementation of an atomic property:
// If you declare a property as atomic ...
@property (atomic, retain) NSString *myString;
// ... a rough implementation that the system generates automatically,
// looks like this:
- (NSString *)myString {
@synchronized (self) {?
return [[myString retain] autorelease];?
}
?}
In this code, “retain” and “autorelease” calls are used as the returned value is being accessed from multiple threads, and you do not want the object to get deallocated between calls.
Therefore, you retain the value first, and then put it in an autorelease pool. You can read more in Apple’s documentation about Thread Safety. This is worth knowing, if only for the reason that most iOS programmers never bother to find this out. Protip: this makes a great job interview question! :]
Most of the UIKit properties are not thread-safe. To find out whether a class is thread-safe or not, take a look at the API documentation. If the API documentation does not say anything about thread-safety, then you should assume that the class is not thread-safe.
As a general rule, if you are executing on a secondary thread and you must do something to a UIKit object, use performSelectorOnMainThread.
Deadlock: a situation where a thread is blocked waiting for a condition that can never be met. For example, if two threads that are executing with synchronized code call each other, then each thread will be waiting for the other one to finish and open the lock. But this will never happen, and both threads will be deadlocked.
Sleepy Time: this occurs when there are too many threads executing simultaneously and the system gets bogged down. NSOperationQueue has a property that you can set to tell it how many concurrent threads you want executing at the same time.
The NSOperation class has a fairly easy and short declaration. To create a customized operation, follow these steps:
The reason you should create your own autorelease pool is that you do not have access to the autorelease pool of the main thread, so you should create your own. Here is an example:
#import <Foundation/Foundation.h>
@interface MyLengthyOperation: NSOperation
@end
@implementation MyLengthyOperation
- (void)main {
// a lengthy operation
@autoreleasepool {
for (int i = 0 ; i < 10000 ; i++) {
NSLog(@"%f", sqrt(i));
}
}
}
@end
The code above shows the ARC syntax for autorelease pool usage. You should definitely be using ARC by now! :]
In threaded operations, you never know exactly when the operation is going to start, and how long it will take to finish. Most of the time you don’t want to perform an operation in the background if the user has scrolled away or has left a page — there’s no reason to perform the operation. The key is to check for isCancelled property of NSOperation class frequently. For example, in the imaginary sample code above, you would do this:
@interface MyLengthyOperation: NSOperation
@end
@implementation MyLengthyOperation
- (void)main {
// a lengthy operation
@autoreleasepool {
for (int i = 0 ; i < 10000 ; i++) {
// is this operation cancelled?
if (self.isCancelled)
break;
NSLog(@"%f", sqrt(i));
}
}
}
@end
To cancel an operation, you call the NSOperation’s cancel method, as shown:
// In your controller class, you create the NSOperation
// Create the operation
MyLengthyOperation *my_lengthy_operation = [[MyLengthyOperation alloc] init];
.
.
.
// Cancel it
[my_lengthy_operation cancel];
NSOperation class has a few other methods and properties:
Start: Normally, you will not override this method. Overriding “start” requires a more complex implementation, and you have to take care of properties such as isExecuting, isFinished, isConcurrent, and isReady. When you add an operation to a queue (an instance of NSOperationQueue, which will be discussed later), the queue will call “start” on the operation and that will result in some preparation and the subsequent execution of “main”.
If you call “start” on an instance of NSOperation, without adding it to a queue, the operation will run in the main loop.
Dependency: you can make an operation dependent on other operations. Any operation can be dependent on any number of operations. When you make operation A dependent on operation B, even though you call “start” on operation A, it will not start unless operation B isFinished is true. For example:
MyDownloadOperation *downloadOp = [[MyDownloadOperation alloc] init]; // MyDownloadOperation is a subclass of NSOperation
MyFilterOperation *filterOp = [[MyFilterOperation alloc] init]; // MyFilterOperation is a subclass of NSOperation
[filterOp addDependency:downloadOp];
To remove dependencies:
[filterOp removeDependency:downloadOp];
Priority: sometimes the operation you wish to run in the background is not crucial and can be performed at a lower priority. You set the priority of an operation by using “setQueuePriority:”.
[filterOp setQueuePriority:NSOperationQueuePriorityVeryLow];
Other options for thread priority are: NSOperationQueuePriorityLow, NSOperationQueuePriorityNormal, NSOperationQueuePriorityHigh, and NSOperationQueuePriorityVeryHigh.
When you add operations to a queue, the NSOperationQueue looks through all of the operations, before calling “start” on them. Those that have higher priorities will be executed first. Operations with the same priority will be executed in order of submission to the queue (FIFO).
(Historical note: In 1997, an embedded system in the Mars Rover suffered from priority inversion, perhaps the most expensive illustration of why it is important to get priority and mutex locks right. See http://research.microsoft.com/en-us/um/people/mbj/Mars_Pathfinder/Mars_Pathfinder.html for further background information on this event.)
**Completion block: **another useful method in NSOperation class is setCompletionBlock:. If there is something that you want to do once the operation has been completed, you can put it in a block and pass it into this method. Note that there is no guarantee the block will be executed on the main thread.
[filterOp setCompletionBlock: ^{
NSLog(@"Finished filtering an image.");
}];
Some additional notes on working with operations:
#import <Foundation/Foundation.h>
@interface MyOperation : NSOperation
-(id)initWithNumber:(NSNumber *)start string:(NSString *)string;
@end
[(NSObject *)self.delegate performSelectorOnMainThread:@selector(delegateMethod:) withObject:object waitUntilDone:NO];
NSOperationQueue also has a fairly simple interface. It is even simpler than NSOperation, because you don’t need to subclass it, or override any method — you simply create one. It is a good practice to give your queue a name; this way you can identify your operation queues at run time and make its debugging easier:
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
myQueue.name = @"Download Queue";
Concurrent operations: a queue is not the same thing as thread. A queue can have multiple threads. Each operation within a queue is running on its own thread. Take the example where you create one queue, and add three operations to it. The queue will launch three separate threads, and run all operations concurrently on their own threads.
By default, NSOperationQueue class will do some magic behind the scenes, decide what is best for the particular platform the code is running on, and will launch the maximum possible number of threads.
Consider the following example. Assume the system is idle, and there are lots of resources available, so NSOperationQueue could launch something like eight simultaneous threads. Next time you run the program, the system could be busy with other unrelated operations which are consuming resources, and NSOperationQueue will only launch two simultaneous threads.
Maximum number of concurrent operations: you can set the maximum number of operations that NSOperationQueue can run concurrently. NSOperationQueue may choose to run any number of concurrent operations, but it won’t be more than the maximum.
myQueue.MaxConcurrentOperationCount = 3;
If you change your mind, and want to set MaxConcurrentOperationCount back to its default, you would perform the following changes:
myQueue.MaxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
[myQueue addOperation:downloadOp];
[downloadOp release]; // manual reference counting
NSArray *active_and_pending_operations = myQueue.operations;
NSInteger count_of_operations = myQueue.operationCount;
// Suspend a queue
[myQueue setSuspended:YES];
.
.
.
// Resume a queue
[myQueue setSuspended: NO];
[myQueue cancelAllOperations];
// Create a weak reference
__weak MyViewController *weakSelf = self;
// Add an operation as a block to a queue
[myQueue addOperationWithBlock: ^ {
NSURL *aURL = [NSURL URLWithString:@"http://www.somewhere.com/image.png"];
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfURL:aURL options:nil error:&error];
UIImage *image = nil;
If (data)
image = [UIImage imageWithData:data];
// Update UI on the main thread.
[[NSOperationQueue mainQueue] addOperationWithBlock: ^ {
weakSelf.imageView.image = image;
}];
}];
You’ve come a long way in this tutorial! Your little project is responsive and shows lots of improvement over the original version. However, there are still some small details that are left to take care of. You want to be a great programmer, not just a good one!
You may have noticed that as you scroll away in table view, those offscreen cells are still in the process of being downloaded and filtered. Didn’t you put cancellation provisions in your code? Yes, you did — you should probably make use of them! :]
Go back to Xcode, and switch to ListViewController.m. Go to the implementation of tableView:cellForRowAtIndexPath:, and wrap [self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath]; in an if-clause as follows:
// in implementation of tableView:cellForRowAtIndexPath:
if (!tableView.dragging && !tableView.decelerating) {
[self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath];
}
You tell the table view to start operations only if the table view is not scrolling. These are actually properties of UIScrollView, and because UITableView is a subclass of UIScrollView, you automatically inherit these properties.
Now, go to the end of ListViewController.m and implement the following UIScrollView delegate methods:
#pragma mark -
#pragma mark - UIScrollView delegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
// 1
[self suspendAllOperations];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
// 2
if (!decelerate) {
[self loadImagesForOnscreenCells];
[self resumeAllOperations];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// 3
[self loadImagesForOnscreenCells];
[self resumeAllOperations];
}
A quick walkthrough of the code above shows the following:
Add the implementation of suspendAllOperations, resumeAllOperations, loadImagesForOnscreenCells to the very end of ListViewController.m:
#pragma mark - Cancelling, suspending, resuming queues / operations
- (void)suspendAllOperations {
[self.pendingOperations.downloadQueue setSuspended:YES];
[self.pendingOperations.filtrationQueue setSuspended:YES];
}
- (void)resumeAllOperations {
[self.pendingOperations.downloadQueue setSuspended:NO];
[self.pendingOperations.filtrationQueue setSuspended:NO];
}
- (void)cancelAllOperations {
[self.pendingOperations.downloadQueue cancelAllOperations];
[self.pendingOperations.filtrationQueue cancelAllOperations];
}
- (void)loadImagesForOnscreenCells {
// 1
NSSet *visibleRows = [NSSet setWithArray:[self.tableView indexPathsForVisibleRows]];
// 2
NSMutableSet *pendingOperations = [NSMutableSet setWithArray:[self.pendingOperations.downloadsInProgress allKeys]];
[pendingOperations addObjectsFromArray:[self.pendingOperations.filtrationsInProgress allKeys]];
NSMutableSet *toBeCancelled = [pendingOperations mutableCopy];
NSMutableSet *toBeStarted = [visibleRows mutableCopy];
// 3
[toBeStarted minusSet:pendingOperations];
// 4
[toBeCancelled minusSet:visibleRows];
// 5
for (NSIndexPath *anIndexPath in toBeCancelled) {
ImageDownloader *pendingDownload = [self.pendingOperations.downloadsInProgress objectForKey:anIndexPath];
[pendingDownload cancel];
[self.pendingOperations.downloadsInProgress removeObjectForKey:anIndexPath];
ImageFiltration *pendingFiltration = [self.pendingOperations.filtrationsInProgress objectForKey:anIndexPath];
[pendingFiltration cancel];
[self.pendingOperations.filtrationsInProgress removeObjectForKey:anIndexPath];
}
toBeCancelled = nil;
// 6
for (NSIndexPath *anIndexPath in toBeStarted) {
PhotoRecord *recordToProcess = [self.photos objectAtIndex:anIndexPath.row];
[self startOperationsForPhotoRecord:recordToProcess atIndexPath:anIndexPath];
}
toBeStarted = nil;
}
suspendAllOperations, resumeAllOperations and cancelAllOperations have a straightforward implementation. You basically use factory methods to suspend, resume or cancel operations and queues. For convenience, you put them together in separate methods.
LoadImagesForOnscreenCells is little complex. Here’s what’s going on:
And finally, the last piece of this puzzle is solved by didReceiveMemoryWarning of ListViewController.m.
// If app receive memory warning, cancel all operations
- (void)didReceiveMemoryWarning {
[self cancelAllOperations];
[super didReceiveMemoryWarning];
}
Build and run and you should have a more responsive, and better resource-managed application! Give yourself a round of applause!
NSOperations and NSOperationQueues学习笔记
标签:ios-多线程
原文地址:http://blog.csdn.net/leochang130731/article/details/43971105