美文网首页
多任务处理-GCD

多任务处理-GCD

作者: Saxon_Geoffrey | 来源:发表于2015-03-03 01:20 被阅读578次

    如果使用GCD,完全由系统管理线程,我们不需要编写线程代码。只需定义想要执行的任务,然后添加到适当的分派队列(dispatch queue)即向队列中添加代码块。GCD会负责创建线程和调度你的任务,系统直接提供线程管理。

    和操作队列不同的是,块添加到分派队列后就无法取消了。分派队列是严格的FIFO结构,所以无法在队列中使用优先级或调整块的次序。如果需要这些特性,应该使用NSOperationQueue。不过,分派队列可可以完成一些操作无法完成的事情。

    1.分派队列

    GCD有5个不同的队列:运行在主线程中的main queue,3个不同优先级的后台队列,以及一个优先级更低的后台队列(用于I/O)。另外,开发者可以创建自定义队列:串行或者并行队列。自定义队列非常强大,在自定义队列中被调度的所有block最终都将被放入到系统的全局队列中和线程池中。

    2.创建和管理dispatch queue

    1).concurrent(并发) dispatch queue

    并发dispatch queue可以同时并行地执行多个任务,不过并发queue仍然按先进先出的顺序来启动任务。并发queue会在之前的任务完成之前就出列下一个任务并开始执行。

    系统给每个应用提供三个并发dispatch queue:

    DISPATCH_QUEUE_PRIORITY_DEFAULT,

    DISPATCH_QUEUE_PRIORITY_HIGH,

    DISPATCH_QUEUE_PRIORITY_LOW

    整个应用内全局共享,三个queue的区别是优先级。你不需要显式地创建这些queue,一般使用dispatch_get_global_queue函数来获取这三个queue:

    // 获取默认优先级的全局并发dispatch queue  
    dispatch_queue_t  queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
    

    2).serial(串行) dispatch queue

    应用的任务需要按特定顺序执行时,就需要使用串行Dispatch Queue,串行queue每次只能执行一个任务。你可以使用串行queue来替代锁,保护共享资源或可变的数据结构。和锁不一样的是,串行queue确保任务按可预测的顺序执行。而且只要你异步地提交任务到串行queue,就永远不会产生死锁。

    和并发queue不同,我们必须显式地创建和管理所有你使用的串行queue,应用可以创建任意数量的串行queue,但不要为了同时执行更多任务而创建更多的串行queue。如果你需要并发地执行大量任务,应该把任务提交到全局并发queue

    利用dispatch_queue_create函数创建串行queue,两个参数分别是queue名和一组queue属性

    // 创建一个串行queue
    dispatch_queue_t queue = dispatch_queue_create("queueName", NULL);
    

    当你在网上搜索例子时,你会经常看人们传递0或者NULL给dispatch_queue_create的第二个参数。这是一个创建串行队列的过时方式;明确你的参数总是更好。

    3).运行时获得公共Queue

    a.使用dispatch_get_current_queue函数作为调试用途,或者测试当前queue的标识。

    b.使用dispatch_get_main_queue函数获得应用主线程关联的串行dispatch queue。

    c.使用dispatch_get_global_queue来获得共享的并发queue。

    3.添加任务到queue

    要执行一个任务,你需要将它添加到一个适当的dispatch queue,你可以单个或按组来添加,也可以同步或异步地执行一个任务。一旦进入到queue,queue会负责尽快地执行你的任务。一般可以用一个block来封装任务内容。

    1.添加单个任务到queue

    a.dispatch_async

    异步地调度任务,绝大多数情况下,我们都会异步添加任务,因为添加任务到Queue中时,无法确定这些代码什么时候能够执行。因此异步地添加block或函数,可以让你立即调度这些代码的执行,然后调用线程可以继续去做其它事情。特别是应用主线程一定要异步地dispatch任务,这样才能及时地响应用户事件。

    何时以及何处使用dispatch_async:

    • 自定义串行队列:当你想串行执行后台任务并追踪它时就是一个好选择。这消除了资源争用,因为你知道一次只有一个任务在执行。注意若你需要来自某个方法的数据,你必须内联另一个 Block 来找回它或考虑使用 dispatch_sync。
    • 主队列(串行):这是在一个并发队列上完成任务后更新 UI 的共同选择。要这样做,你将在一个 Block 内部编写另一个 Block 。以及,如果你在主队列调用 dispatch_async 到主队列,你能确保这个新任务将在当前方法完成后的某个时间执行。
    • 并发队列:这是在后台执行非UI工作的共同选择。

    b.dispatch_sync

    少数时候你可能希望同步地调度任务,以避免竞争条件或其它同步错误。使用dispatch_sync和dispatch_sync_f函数同步地添加任务到Queue,这两个函数会阻塞当前调用线程,直到相应任务完成执行。注意:绝对不要在任务中调用 dispatch_sync函数,并同步调度新任务到当前正在执行的queue。对于串行queue这一点特别重要,因为这样做肯定会导致死锁;而并发queue也应该避免这样做。

    何时以及何处使用dispatch_sync :

    • 自定义串行队列:在这个状况下要非常小心!如果你正运行在一个队列并调用 dispatch_sync 放在同一个队列,那你就百分百地创建了一个死锁。
    • 主队列(串行):同上面的理由一样,必须非常小心!这个状况同样有潜在的导致死锁的情况。
    • 并发队列:这才是做同步工作的好选择,不论是通过调度障碍,或者需要等待一个任务完成才能执行进一步处理的情况。

    我们举例使用GCD来创建一个并发queue异步加载

    - (void)showImage{
        dispatch_queue_t concurrentQueue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_async(concurrentQueue, ^{
            __block UIImage *image = nil;
            dispatch_sync(concurrentQueue, ^{//同步
                //download image
                NSLog(@"showImage thread is %@",[NSThread currentThread]);
                NSError *downError = nil;
                NSData *imageData = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:kURL]]
                                                          returningResponse:nil
                                                                      error:&downError];
                if (downError == nil&&imageData !=nil) {
                    image = [UIImage imageWithData:imageData];
                }else if (downError != nil){
                    NSLog(@"error happen:%@",downError);
                }else{
                    NSLog(@"no data can get from the url");
                }
            });
            dispatch_async(concurrentQueue, ^{//异步
                //show image
                if (image != nil){
                    [self.imageView setImage:image];
                    NSLog(@"image loading");
                }else{
                    NSLog(@"image isn't download ,nothing to display");
                }
            });
        });
    }
    

    2.在主线程中执行任务

    GCD提供一个特殊的dispatchqueue,可以在应用的主线程中执行任务。调用dispatch_get_main_queue函数获得应用主线程的dispatch queue,添加到这个queue的任务由主线程串行化执行。

    - (void)showImage{
    //...省略
        // 回到主线程显示图片  
        dispatch_async(dispatch_get_main_queue(), ^{  
            self.imageView.image = image;  
        });  
    });  
    }
    

    4.暂停和继续queue

    我们可以使用dispatch_suspend函数暂停一个queue以阻止它执行block对象;使用dispatch_resume函数继续dispatch queue。调用dispatch_suspend会增加queue的引用计数,调用dispatch_resume则减少queue的引用计数。当引用计数大于0时,queue就保持挂起状态。因此你必须对应地调用suspend和resume函数。挂起和继续是异步的,而且只在执行block之间(比如在执行一个新的block之前或之后)生效。挂起一个queue不会导致正在执行的block停止。

    5.Dispatch Group

    我们可以使用dispatch_group_async函数将多个任务关联到一个Dispatch Group和相应的queue中,group会并发地同时执行这些任务。而且Dispatch Group可以用来阻塞一个线程, 直到group关联的所有的任务完成执行。有时候你必须等待任务完成的结果,然后才能继续后面的处理。

    -(void)dispatchGroup1{
        NSLog(@"group1");
    }
    
    -(void)dispatchGroup2{
        NSLog(@"group2");
    }
    
    -(void)dispatchGroup3{
        NSLog(@"group3");
    }
    
    -(void)dispatchGroup{
        dispatch_group_t taskGroup = dispatch_group_create();
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        dispatch_group_async(taskGroup, mainQueue, ^{
            [self dispatchGroup1];
        });
        dispatch_group_async(taskGroup, mainQueue, ^{
            [self dispatchGroup2];
        });
        dispatch_group_async(taskGroup, mainQueue, ^{
            [self dispatchGroup3];
        });
        
        //dispatch_group_notify 以异步的方式工作。当 Dispatch Group中没有任何任务时会开始执行
        dispatch_group_notify(taskGroup, mainQueue, ^{
            [[[UIAlertView alloc]initWithTitle:@"Finish" message:@"all task are finished" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]show];
        });
    }
    
    

    6.dispatch_after

    我们可以使用dispatch_after延后任务的执行:

    - (void)dispatchAfterSeconds{
        NSLog(@"current thread is %@",[NSThread currentThread]);
        double delayInSeconds = 4.0;
        //指定一个距离现在3秒的时间delayInNanoSeconds
        dispatch_time_t delayInNanoSeconds = dispatch_time(DISPATCH_TIME_NOW, (int64_t)delayInSeconds*NSEC_PER_SEC);
        dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_after(delayInNanoSeconds, concurrentQueue, ^{
            NSLog(@"I'm showing");
        });
    }
    

    dispatch_after就像一个延迟版的dispatch_async 。你依然不能控制实际的执行时间,且一旦 dispatch_after 返回也就不能再取消它。一般来讲,我们会在主队列上使用它。

    7.dispatch_once

    我们看下面的例子:

    -(void)PerformingTaskOnlyOnce{
        void(^executeOnlyOnce)(void)=^{
            static NSUInteger numberOfEntries = 0;
            numberOfEntries++;
            NSLog(@"Executed %lu time(s)",(unsigned long)numberOfEntries);
        };
        
        static dispatch_once_t onceToken;
        dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_once(&onceToken, ^{
            dispatch_async(concurrentQueue, executeOnlyOnce);
        });
        dispatch_once(&onceToken, ^{
            dispatch_async(concurrentQueue, executeOnlyOnce);
        });
    }
    

    打印结果

    Executed 1 time(s)
    

    从打印结果可以得知,尽管我们调用了两次dispatch_once,但实际上只执行了一次。通常我们使用dispatch_once来写单例:
    我们建立一个PhotoManager类,PhotoManager.m如下:

    @interface PhotoManager ()
    @property (nonatomic, strong) NSMutableArray *photosArray;
    @property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue;
    @end
    
    + (instancetype)sharedManager 
    { 
        static PhotoManager *sharedPhotoManager = nil; 
        static dispatch_once_t onceToken; 
        dispatch_once(&onceToken, ^{ 
            sharedPhotoManager = [[PhotoManager alloc] init]; 
            sharedPhotoManager->_photosArray = [NSMutableArray array];
        }); 
        return sharedPhotoManager; 
    } 
    

    说起单例,我们必须来讲一个概念:线程安全:

    线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是 NSDictionary 。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary 就不是线程安全的,应该保证一次只能有一个线程访问它。

    如果我们上面的代码是这样写的:

    + (instancetype)sharedManager     
    { 
        static PhotoManager *sharedPhotoManager = nil; 
        if (!sharedPhotoManager) { 
            sharedPhotoManager = [[PhotoManager alloc] init]; 
            sharedPhotoManager->_photosArray = [NSMutableArray array];
        } 
        return sharedPhotoManager; 
    } 
    

    这时if条件分支不是线程安全的;如果你多次调用这个方法,有一个可能性是在某个线程(就叫它线程A)上进入 if 语句块并可能在 sharedPhotoManager 被分配内存前发生一个上下文切换。然后另一个线程(线程B)可能进入 if ,分配单例实例的内存,然后退出。

    当系统上下文切换回线程A,你会分配另外一个单例实例的内存,然后退出。在那个时间点,你有了两个单例的实例——很明显这不是你想要的,所以使用dispatch_once() 可以以线程安全的方式执行且仅执行其代码块一次。试图访问临界区(即传递给 dispatch_once 的代码)的不同的线程会在临界区已有一个线程的情况下被阻塞,直到临界区完成为止。不过这只是让访问共享实例线程安全。它绝对没有让类本身线程安全。类中可能还有其它竞态条件,例如任何操纵内部数据的情况。

    8.dispatch barriers

    线程安全实例不是处理单例时的唯一问题。如果单例属性表示一个可变对象,那么你就需要考虑是否那个对象自身线程安全。例如上面的sharedPhotoManager->_photosArray。

    虽然许多线程可以同时读取 NSMutableArray 的一个实例而不会产生问题,但当一个线程正在读取时让另外一个线程修改数组就是不安全的。你的单例在目前的状况下不能预防这种情况的发生。

    假设我们有一个PhotoManager的类,PhotoManager.m中的addPhoto如下:

    @interface PhotoManager ()
    @property (nonatomic, strong) NSMutableArray *photosArray;
    @end
    
    
    - (void)addPhoto:(Photo *)photo 
    { 
        if (photo) { 
            [_photosArray addObject:photo]; 
        } 
    } 
    

    这是一个写方法,它修改一个私有可变数组对象。

    现在看看photos :

    - (NSArray *)photos 
    { 
      return [NSArray arrayWithArray:_photosArray]; 
    } 
    

    这是所谓的读方法,它读取可变数组。它为调用者生成一个不可变的拷贝,防止调用者不当地改变数组,但这不能提供任何保护来对抗当一个线程调用读方法 photos 的同时另一个线程调用写方法addPhoto: 。

    这就是软件开发中经典的读写问题。GCD通过用dispatch barriers创建一个读写锁提供了一个优雅的解决方案。

    Dispatch barriers是一组函数,在并发队列上工作时扮演一个串行式的瓶颈。使用 GCD barrier API确保提交的Block在那个特定时间上是指定队列上唯一被执行的条目。这就意味着所有的先于调度障碍提交到队列的条目必能在这个Block执行前完成。

    当这个Block的时机到达,调度障碍执行这个Block并确保在那个时间里队列不会执行任何其它Block。一旦完成,队列就返回到它默认的实现状态。 GCD 提供了同步和异步两种障碍函数。

    下图显示了障碍函数对多个异步队列的影响:

    实例图2.png

    正常部分的操作就如同一个正常的并发队列。但当障碍执行时,它本质上就如同一个串行队列。也就是,障碍是唯一在执行的事物。在障碍完成后,队列回到一个正常并发队列的样子。我们一般在自定义并发队列使用Dispatch barriers

    将上面的例子中的写方法修改如下:

    @interface PhotoManager ()
    @property (nonatomic, strong) NSMutableArray *photosArray;
    @property (nonatomic, strong) dispatch_queue_t concurrentPhotoQueue;
    @end
    
    - (void)addPhoto:(Photo *)photo 
    { 
        if (photo) {
            dispatch_barrier_async(self.concurrentPhotoQueue, ^{ 
                [_photosArray addObject:photo]; 
            }); 
        } 
    } 
    

    你还需要实现photos读方法。

    在写入方法打扰的情况下,要确保线程安全,你需要在 concurrentPhotoQueue 队列上执行读操作。对于读操作,dispatch_sync是一个好的选择。

    dispatch_sync() 同步地提交工作并在返回前等待它完成。使用dispatch_sync跟踪你的调度障碍工作,或者当你需要等待操作完成后才能使用Block处理过的数据。如果你使用第二种情况做事,你将不时看到一个__block变量写在dispatch_sync范围之外以便返回时在dispatch_sync使用处理过的对象。

    但你需要很小心。想像如果你调用dispatch_sync并放在你已运行着的当前队列。这会导致死锁,因为调用会一直等待直到Block完成,但Block不能完成(它甚至不会开始!),直到当前已经存在的任务完成,而当前任务无法完成!这将迫使你自觉于你正从哪个队列调用——以及你正在传递进入哪个队列。

    - (NSArray *)photos 
    { 
        __block NSArray *array; // 1 
        dispatch_sync(self.concurrentPhotoQueue, ^{ // 2 
            array = [NSArray arrayWithArray:_photosArray]; // 3 
        }); 
        return array; 
    } 
    

    最后在单例sharedManager中实例化self.concurrentPhotoQueue:

    + (instancetype)sharedManager 
    { 
        static PhotoManager *sharedPhotoManager = nil; 
        static dispatch_once_t onceToken; 
        dispatch_once(&onceToken, ^{ 
            sharedPhotoManager = [[PhotoManager alloc] init]; 
            sharedPhotoManager->_photosArray = [NSMutableArray array]; 
    
            sharedPhotoManager->_concurrentPhotoQueue = dispatch_queue_create("com.selander.GooglyPuff.photoQueue", 
                                                        DISPATCH_QUEUE_CONCURRENT);  
        }); 
     
        return sharedPhotoManager; 
    } 
    

    现在PhotoManager单例现在是线程安全的了。

    相关文章

      网友评论

          本文标题:多任务处理-GCD

          本文链接:https://www.haomeiwen.com/subject/fqrmxttx.html