iOS开发:多线程与GCD

一、多线程

1.1进程与线程

进程:进程是指在系统中正在运行的一个应用程序;每个进程之间是独立的,每个进程均运行在其专用的受保护的内存空间内。

线程:线程是进程的基本执行单元,一个进程的所有任务都在线程中执行;进程要想执行任务,必须要有线程,进程至少要有一条线程;程序启动回默认开启一条线程,即主线程。

多线程原理:同一时间单核CPU只能处理一个线程,即只有一个线程在执行。多线程同时执行是CPU快速的在多个线程之间切换,CPU调度线程的时间足够快,就造成了多线程同时执行的效果。如果线程非常多,CPU会在N个线程之间切换,消耗大量的CPU资源,每个线程被调度的次数会降低,线程的执行效率也会降低。

多线程技术方案

方案 说明 语言 生命周期 使用频率
pthread 一套通用的多线程API;适用于Unix/Linux/Windows等平台;跨平台,可移植;适用难度大 c 开发管理 很低
NSThread 使用更加面向对象;简单易用,可直接操作线程对象 OC 开发管理
GCD 旨在替代NSThread技术,充分利用设备的多核 c 自动管理
NSOperation 基于GCD(底层是GCD);比GCD多了一些更简单实用的功能;实用更加面向对象 OC 自动管理

1.2 任务

任务是指执行的操作,简单说就是在线程中执行的那段代码。在GCD中是放在block中的。

任务的执行有两种方式:同步执行和异步执行。两者的区别主要在于是否等待队列中的任务执行完毕,以及是否具备开启新线程的能力。

  • 同步执行(sync):同步添加任务到指定的队列中,在添加的任务执行结束前会一直等待,直到队列里的任务完成后再继续执行;只能在当前线程执行任务,不具备开启新线程的能力。加入方式dispatch_sync
  • 异步执行(async):异步添加任务到指定队列,不会做任何等待,可以继续执行任务;可以在新的线程执行任务,具备开启新线程的能力,但是并不一定开启新线程,这跟任务所在的队列有关。加入方式dispatch_async

任务执行速度的影响因素:1.CPU。2.线程状态。3.任务的复杂度。4.任务的优先级。其中任务的优先级包括用户指定的qualityServiceuserInteractiveuserInitiatedutilitybackgrounddefault);等待的频繁程度(不执行)。

二、GCD

队列(Dispatch Queue)指的是执行任务的等待队列,即用来存放任务的队列。队列采用FIFO(先进先出)的原则,新任务总是被插入到队列的末尾,读取任务的时候总是从队列的头部开始读取,每读取一个任务,则从队列中释放一个任务。

GCD中有2种队列:串行队列和并发队列,两者均遵循FIFO的原则,不同点在于执行的顺序以及开启线程的数量。

  • 串行队列(Serial Dispatch Queue):只开启一个线程,一个任务执行完毕才会执行下一个任务。每次只有一个任务被执行。
  • 并发队列(Concurrent Dispatch Queue):可以开启多个线程,并且同时执行任务。可以让多个任务同时执行。

队列的创建:可以使用dispatch_queue_create创建,该方法需要传入2个参数:第一个参数表示队列的标识,可为空;第二个参数用来识别是串行队列还是并发队列,DISPATCH_QUEUE_SERIAL(==NULL)标识串行队列,DISPATCH_QUEUE_CONCURRENT

//串行队列
dispatch_queue_t s = dispatch_queue_create("com.appex.queue.serial", DISPATCH_QUEUE_SERIAL);
//并发队列
dispatch_queue_t c = dispatch_queue_create("com.appex.queue.concurrent", DISPATCH_QUEUE_CONCURRENT);

主队列:主队列(Main Dispatch Queue)是一种特殊的串行队列,说它特殊是因为默认情况下代码就在主队列中,主队列的代码又都会放在主线程中执行。获取方式:

//获取主队列
dispatch_group_t main = dispatch_get_main_queue();

全局并发队列:全局并发队列是(Global Dispatch Queue)是系统提供的并发队列,获取方法:

//获取全局并发队列
dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//第一个参数表示优先级的高低,一般传`DISPATCH_QUEUE_PRIORITY_DEFAULT`
DISPATCH_QUEUE_PRIORITY_HIGH       
DISPATCH_QUEUE_PRIORITY_DEFAULT    
DISPATCH_QUEUE_PRIORITY_LOW      
DISPATCH_QUEUE_PRIORITY_BACKGROUND 
//第二个参数暂时没用,传0即可。

如果当前在主线程,按照队列的串行和并发,任务的同步和异步特性组合,我们归纳如下:

区别 并发队列 串行队列 主队列
同步 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 死锁
异步 有开启新线程,并发执行任务 有开启新线程(1条),串行执行任务 没有开启新线程,串行执行任务
/*死锁案例-1*/
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
    /*async没有开启新线程,以下代码在主线程中运行*/
    NSLog(@"开始:%@", [NSThread currentThread]);
    dispatch_sync(queue, ^{
         NSLog(@"sync:%@", [NSThread currentThread]);
     });
     NSLog(@"结束:%@", [NSThread currentThread]);
});

在主线程中,向主队列添加同步任务会死锁。这是因为添加的任务和主队列自身的任务相互等待,阻塞了主队列,最终造成主队列所载的线程(主线程)死锁。如果在其他线程向主队列添加同步任务,则不会死锁。

/*死锁案例-2*/
dispatch_queue_t queue = dispatch_queue_create("com.app.serial", 0);
dispatch_async(queue, ^{
    /*async开启1条新线程,以下代码在子线程中运行*/
    NSLog(@"开始:%@", [NSThread currentThread]);
    dispatch_sync(queue, ^{
         NSLog(@"sync:%@", [NSThread currentThread]);
   });
   NSLog(@"结束:%@", [NSThread currentThread]);
});

在子线程中,向串行队列添加同步任务会死锁。这是因为添加的任务和串行队列自身的任务相互等待,阻塞了串行队列,最终造成串行队列所在的线程(子线程)死锁。如果在其他线程向该队列添加同步任务,则不会死锁。

以上案例可以概括为在一个串行队列所在的线程,向该队列添加同步任务会造成串行队列追加的任务和原有的任务相互等待而阻塞当前线程。

三、GCD其他常用函数

3.1 dispatch_after:表示在某个队列中异步延迟执行任务

void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t work){
    _dispatch_after(when, queue, NULL, work, true);
}

第一个参数when表示开始的时间,通常在现在的时间时间的基础上加时间,如dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 2)表示2秒之后;第二个参数queue传入队列,第三个参数work传入任务的block代码。
常规用法如下:

NSLog(@"开始:%@", [NSThread currentThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 2), dispatch_get_main_queue(), ^{
   NSLog(@"after:%@", [NSThread currentThread]);
});

3.2 dispatch_once:代码只执行一次

void dispatch_once(dispatch_once_t *val, dispatch_block_t block){
    dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}

第一个参数val传入一个dispatch_once_t类型的指针地址,第二个参数block传入只执行一次的block任务。
常规用法:

@interface NXDownloader : NSObject
+ (NXDownloader *)downloader;
@end

@implementation NXDownloader
+ (NXDownloader *)downloader{
    static dispatch_once_t t;
    static NXDownloader *sharedInstance;
    dispatch_once(&t, ^{
        sharedInstance = [[NXDownloader alloc] init];
    });
    return sharedInstance;
}
@end

这样我们通过NXDownloader *downloader = [NXDownloader downloader];获取到的实例都是同一个,而且是线程安全的。

3.3dispatch_barrier_async/dispatch_barrier_sync:栅栏函数

栅栏函数的作用就是隔离栅栏函数之前与之后的代码执行,只有前面的代码执行完毕才会执行后面的代码,函数如下:

void dispatch_barrier_async(dispatch_queue_t dq, dispatch_block_t work){
    ...
}

void dispatch_barrier_sync(dispatch_queue_t dq, dispatch_block_t work){
    ...
}

栅栏函数的第一个参数dq接收一个队列,按照代码注释的说明,这里需要传入一个并发队列才会真正的发挥栅栏函数的功能,如果传入的dq是个串行队列,则函数的表现与dispatch_barrier_asyncdispatch_barrier_sync表现一样。

那么这里的asyncsync的作用有什么不同呢?看如下代码:

dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"1:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"2:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"3:%@", [NSThread currentThread]);
});
NSLog(@"0-0:%@",[NSThread currentThread]);

//dispatch_barrier_async或dispatch_barrier_sync
dispatch_barrier_async(queue, ^{
    sleep(2);
    NSLog(@"barrier:%@", [NSThread currentThread]);
});

NSLog(@"0-1:%@",[NSThread currentThread]);
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"4:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"5:%@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"6:%@", [NSThread currentThread]);
});

多次打印,结果如下:


async.png
sync.png

执行结果显示:{1,2,3}执行的顺序是不确定,{4,5,6}执行顺序也是不确定的,可以确定的是{1,2,3}执行完毕后执行barrier,再执行{4,5,6}。在async的情况下0-00-1最先执行。而sync的情况下0-0在barrier之前执行,0-1barrier之后执行。也就是sync会如同dispatch_sync一样执行完毕后再执行后面的代码。并且在sync的情况下barrier任务会在原有的线程(这里是主线程)中执行。

还需要注意一点,栅栏函数的只在自定义的并发队列才会生效,这一点也好理解,因为并发队列,系统也会向里边添加任务,我们设置一个栅栏那么后续加入的系统任务岂不是要等待栅栏执行完毕?这显然不合理。

3.4dispatch_group,队列组

队列组简言之就是一组任务执行完毕后会有一个单独的回调。

//队列组的创建
dispatch_group_t group = dispatch_group_create();
//添加任务
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
    NSLog(@"执行");
});
//任务执行完毕的回掉
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
      NSLog(@"结束:%@");
});

其中dispatch_group_async会把任务放入队列,再把队列放入队列组。也可以用dispatch_group_enterdispatch_group_leave成对使用。 dispatch_group_notify会在任务执行完毕后回调,你可以指定一个队列继续做其他事情。还有一个不太常用的dispatch_group_wait函数,这个函数第一个参数传入一个group,第二个参数传入一个time,这个时间指定的是dispatch_group_wait之后的代码等待的最大时间,假定这里设定的时间是3秒,如果前面的任务2秒执行完毕,那么wait后面的代码会在2秒后执行。如果前面的任务4秒执行完毕,那么wait后面的代码会在第3秒的时候开始执行。

dispatch_group_notify案例

//dispatch_group_notify案例:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);

for (int i= 0; i < 10; i++){
    dispatch_group_async(group, queue, ^{
        NSLog(@"%d:%@", i, [NSThread currentThread]);
    });

    /*以上三行代码等价于
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
         NSLog(@"%d:%@", i, [NSThread currentThread]);
         dispatch_group_leave(group);
    });
    */
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"结束:%@", [NSThread currentThread]);
});

dispatch_group_wait案例

//dispatch_group_wait案例
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.app.concurrent", DISPATCH_QUEUE_CONCURRENT);

for (int i= 0; i < 10; i++){
    dispatch_group_async(group, queue, ^{
        sleep(2);
        NSLog(@"%d:%@", i, [NSThread currentThread]);
    });
}
//dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC*2)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"结束:%@", [NSThread currentThread]);

这个场景在实际开发中使用较多,比如现在有一组网络图片需要分享到某个平台,我们需要等待多张网络图片全部下载完毕后才能开始分享。

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

推荐阅读更多精彩内容