iOS中的多线程编程:重温GCD(一)

引言

关于iOS开发中的多线程,一直是工作中的重要组成部分。由于难以理解且对app的用户体验影响重大,也是面试中的考察重点,一名合格的iOS程序🐒理应掌握多线程编程。

自己虽然以前有学习过关于iOS多线程的部分,但由于当时对iOS开发还处于懵懂阶段,很多地方理解可能均有问题,遂查阅一些资料重温了GCD的相关内容,并撰写此文已记录自己的学习路程。

ps:本文将分为三部分,以下内容是关于GCD的基本概念及常见API
为了更好的阅读体验,推荐到我的博客阅读。

此文的第二部分👇
iOS中的多线程编程:重温GCD(二)

码字不易,各位看官看的喜欢烦请点个赞吧!以示鼓励啊😢

什么是GCD

🍎Crand Central Dispatch Reference👇:

Grand Central Dispatch (GCD) comprises language features, runtime libraries, and system enhancements that provide systemic, comprehensive improvements to the support for concurrent code executikkon on multicore hardware in iOS and OS X.

重温GCD术语

任务与队列 Task & Queue

GCD术语中有两个核心的概念:任务,队列。

在本文中,我们可以将任务暂定为objc中的一个block。我们可以把任务与队列都看成是objc中的对象。任务与队列都有他们自己的属性与行为

任务的属性是:同步or异步

队列的属性是:并发or串行

串行与并发 | Serial & Concurrent

串行与并发是队列派发任务的行为描述,可以理解为队列的属性。既然是队列,那么必定遵循FIFO的原则。无论是串行还是并发队列,一定都是先进先出即先进入队列的一定会被先执行,但是不一定先完成。

串行:

串行含义就是一个接一个的派发任务,注意理解一个接一个,即为要等待上一个任务完成了,才会派发下一个任务。这些任务的执行时机受到 GCD 的控制;唯一能确保的事情是 GCD 一次只执行一个任务,并且按照我们添加到队列的顺序来执行。

串行队列
串行队列

虽然串行队列只有在上一个任务执行完毕了才会派发下一个任务,但这并不意味着下一个任务就会被立即执行。注意理解执行派发,执行是GCD来做的,而派发是由队列来做的。

并行:

并行的意义就与串行相反,虽然它也会按照FIFO原则派发任务,但是它并不会等待上一个任务完成后才会派发下一个任务,而是直接将任务抛出。

并发队列

就像图中那样,四个任务都会按照你添加的顺序去执行,and that’s about all you’re guaranteed!与串行队列同样,任务何时去执行,同时有几个任务去执行等都完全取决于GCD

ps:注意此处是==并发==不是==并行==,二者不可混淆。若有兴趣,可参考👇:

所谓的“并发”,英文翻译是concurrent。而并行是parallelism。

并发指的是一种现象,一种经常出现,无可避免的现象。它描述的是“多个任务同时发生,需要被处理”这一现象。它的侧重点在于“发生”。

比如有很多人排队等待检票,这一现象就可以理解为并发。

并行指的是一种技术,一个同时处理多个任务的技术。它描述了一种能够同时处理多个任务的能力,侧重点在于“运行”。

比如景点开放了多个检票窗口,同一时间内能服务多个游客。这种情况可以理解为并行。

常见的队列们:

首先,系统提供给你一个叫做 主队列main queue) 的特殊队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。然而,它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。这个队列就是用于发送消息UIView发送通知的。

系统同时提供给你好几个并发队列。它们叫做 全局调度队列Global Dispatch Queues) 。目前的四个全局队列有着不同的优先级:background、low、default 以及 high。要知道,AppleAPI 也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务

最后,你也可以创建自己的串行队列或并发队列。这就是说,至少有五个队列任你处置:主队列、四个全局调度队列,再加上任何你自己创建的队列。

以上是调度队列的大框架!

同步与异步 | Synchronous & Asynchronous

上文提到同步与异步都是任务对象的“属性”,这个属性的作用就是标志着任务被执行的方式:

同步

同步的含义是该任务执行时,需要等这个任务完成了,才继续线程中的下一个任务。如果你对多线程有一定的了解,你能立即领悟到同步肯定是在当前线程中执行任务的,而需要等待这个任务完成的特性使得同步任务必然会阻塞当前线程

异步

与同步相反,异步任务并不需要被等待。即线程执行异步任务时,不会阻塞住当前线程。但是需要注意的是:执行异步任务时,并不一定会开辟新的线程。原因是在主队列进行任务时,无论是异步还是同步,都将会被放在主线程中。

放个表格总结下:

example 同步 异步
主队列 主线程 主线程
串行队列 当前线程 开辟线程
并发队列 当前线程 开辟线程

注:一定要理解好“派发”与“执行”之间的关系,其实这个地方用对象来理解队列,线程,GCD,串行or并行是很方便的。

上下文切换 Context Switch

一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。

线程安全 Thread Safe

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

小心!线程死锁

线程死锁是使用GCD使用不当时的常见问题,尤其是主线程中,需要格外小心。造成线程死锁的原因是同步执行某个任务A,但是这个任务又在串行队列的后方。此时GCD要求任务A立即执行,即阻塞住线程。可队列是要求遵循先进先出的原则,于是线程死锁。

举个栗子🌰:(这地方之前理解有问题,已经重新写了,感谢评论区🍊桔子同学指出)

//同步串行队列

NSLog(@"1"); 

dispatch_sync(dispatch_get_main_queue(), ^{//这句的作用是:将block里的内容作为任务添加到当前队列里,并且等待此任务执行结束

    NSLog(@"2");

});

NSLog(@"3"); 

这样使用是会导致线程死锁的,控制台只会输出“1”。这个地方一定要理解好,当程序开始运行时,从第3行开始,到第11行都应该看做一个任务,即此时当前串行队列里应该是只有一个任务的。当程序执行到第5行时,当前这个任务还没有执行完毕!还在队列里!此时又将NSLog(@"2");加入到了当前队列里,此时队列里有了两个任务!,即从3到11行是一个任务,block里又是一个任务。如下图:


//程序开始执行时...
<—————————— 出队顺序
任务1 (从第3行到第11行)

//程序执行到第5行...
<—————————— 出队顺序
任务1 (从第3行到第11行) ---- 任务2 (这个是执行到第5行时添加的任务)


由于任务1卡在任务2前,导致2任务无法被执行。但由于2任务是同步任务,必须先被执行,于是1等2,2等1造成线程死锁。

这样就解释了🍊桔子同学的疑惑,甚至这样写也会导致线程死锁,这就验证了我们上面的结论:

- (void)viewDidLoad{
    
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"123");
    });
    
}

控制台是不会输出123的,理由同上,此时dispatch_sync任务无法执行完毕。

API到使用场景

讲完了基本概念,就可以开始code啦~

dispatch_async 处理后台任务:

在讲解dispatch_async之前,先来看看这样的一个🌰:

假设你有一个vc用于展示一些从网络上加载的图片,于是再没有学习GCD之前你可能会这样处理:

- (void)viewDidLoad{
    [super viewDidLoad];
       
    //a long time work like download a img from network..
    [self ALongTimeWork];
    
        //some ui code here..
    [self initUI];

}

是的,这看上去合乎情理:在viewDidLoad方法里我们先去拉取网络数据,然后更新UI界面。但是通过上面的学习你可以了解到:

拉取网络数据是在主线程中同步进行的,这会阻塞住主线程。这就意味着在[self ALongTimeWork]方法结束前,不仅[self initUI]不会调用,连viewDidLoad也会无法结束。这就会引起这个vc加载的非常慢,带来的用户体验就是“这个app很卡!”这样的糟糕印象。

于是我们可以通过dispath_async这个函数去开辟一个新的线程,并将长时任务放置全局并发队列里去执行:

- (void)viewDidLoad{
    [super viewDidLoad];
    
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        //a long time work like download a img from network..
        [self ALongTimeWork];
        
        
        dispatch_sync(dispatch_get_main_queue(), ^{
            //some ui code here..
            [self initUI];

        });

    });
    
}

运行程序,你会发现vc会直接加载完毕,过一会儿图片也会出来。在此期间用户也可以直接操作你的app。这样的体验显然是完胜上面的方案的,这就是GCD的价值!

使用 dispatch_after 延迟处理:

dispatch_after函数可能很不起眼,但是在日常开发中却会经常使用到它。比如以下这种情况:

假设你需要在UITextView中插入一段link并用蓝色标注它们,告诉用户这个地方是可以点击的。显然使用attributedText是个不错的选择。我们可以在用户点击link时更换这段attributedText的背景色为灰色并且跳转,就像大多数网站做的那样:

self.msgDetalTextView.attributedText =  
[self changeNSMutableStringWithStr:jumpStr]; //改变attributedText背景色
NSURL *url = [NSURL URLWithString:_model.messageScheme];

if ([url scheme]) { //跳转动作
    [LocalSchemeHandler performDefaultLocalJump:[url absoluteString]];
}

显然这样是合乎情理的,但是随着使用你就会发现这样的问题:

改变背景色那一瞬间太短了,用户可能根本察觉不到就发生了跳转。这会给用户带来困惑:我点了什么东西导致的跳转?

如果我们能给用户一个良好的反馈表示用户点击了这个link动作生效之后再跳转,就合乎情理多了。于是dispatch_after派上了用场:


self.msgDetalTextView.attributedText =  
[self changeNSMutableStringWithStr:jumpStr]; //改变attributedText背景色
NSURL *url = [NSURL URLWithString:_model.messageScheme];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //这个地方给个延迟反馈下用户的点击
    if ([url scheme]) {
        [LocalSchemeHandler performDefaultLocalJump:[url absoluteString]];
    }
});

现在用户轻点下link会导致link区域变灰,给足0.2秒的时间告诉用户你的点击生效了!,之后再发生跳转。

需要注意的是dispatch_after并不会阻塞当前线程,就算参数中你选择的是dispatch_get_main_queue()。这是因为dispatch_after函数的含义并不是延迟执行,而是延迟提交。dispatch_after的实质是异步将这个任务添加到一个串行队列里去,约定时间结束后,再从串行队列中取出任务添加到你参数中填的那个队列中。比如以下这个例子:

NSLog(@"let's go");
// 设置2秒后执行block
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"This is my");
    
});
    
NSLog(@"let's go");

我们会在控制台看到两个“let's go”的输出,然后过两秒后才输出“This is my”。跟具体的可以看这篇文章

用dispatch_once实现单例:

单例是iOS开发中并不少见的设计模式,在不使用GCD的情况下,我们可能会这样写:

+ (instancetype)shareASingleManager{
    static ASingleManager *shareManager = nil;
    if (!shareManager) {
        shareManager = [[ASingleManager alloc] init];
        
        //some init code here...
        
    }
        
    return shareManager;
}

在了解了线程的概念你就会发现这样其实是有问题的,if语句可不是线程安全!

比如代码运行到第3行时,此时内存中并没有生成过shareManager,于是进入if条件中。但是由于线程切换,该任务暂时不再运行了。在新开辟的线程中,可能再次想要获取该单例,在之前的线程中虽然进入了if,但还没来及生成shareManager,于是造成了两个“单粒”的情况。这显然是不合乎情理的。

dispatch_once函数即可解决这个问题,我们将上述代码改为:

+ (instancetype)shareASingleManager{
    
    static ASingleManager *shareManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //这里只会来一次 直接生成对象即可
        
        shareManager = [[ASingleManager alloc] init];
        
        //some inti code here ...
    });
    
    return shareManager;
}

🍎爸爸的discussion

This function is useful for initialization of global data (singletons) in an application. Always call this function before using or testing any variables that are initialized by the block.

If called simultaneously from multiple threads, this function waits synchronously until the block has completed.

The predicate must point to a variable stored in global or static scope. The result of using a predicate with automatic or dynamic storage (including Objective-C instance variables) is undefined.

Executes a block object once and only once for the lifetime of an application.

The End

关于第二部分:

第二部分将介绍一些更深层次的GCDAPI使用,例如dispatch_barrier_asyncdispatch_group_async等。

此文的第二部分👇
iOS中的多线程编程:重温GCD(二)

码字不易,各位看官看的喜欢烦请点个赞吧!以示鼓励啊😢

参考:

Grand Central Dispatch In-Depth: Part 1/2

Grand Central Dispatch (GCD) Reference

low-level-concurrency-apis

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

推荐阅读更多精彩内容