多任务处理-GCD

如果使用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单例现在是线程安全的了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容