多线程

一、线程的生命周期

首先创建线程,然后调用线程的start,此时线程进入runable就绪状态,等待CPU的调度。CPU调度该线程后,线程进入running状态,CPU去调度其他的线程时,该线程会进入runabel状态。当线程出现以下情况:调用了sleep、等待同步锁或者从可调度线程池移除,就会进入Blocked阻塞状态。当sleep到时,获得同步锁或者重新加入可调度线程池时,重新进入runable状态。任务执行完毕或者强制性退出,线程进入Dead状态。


线程生命周期

二、线程池原理

使用线程执行任务的时候,需要到线程池中去取线程进行任务分配。首先判断线程池大小是否小于核心线程池大小,如果小于的话,创建新的线程执行任务;如果当前小城池大小大于了核心线程池大小,然后开始判断工作队列是否已满,如果没满,将任务提交到工作队列。如果工作队列已满,判断线程池的线程是否都在工作,如果有空闲线程没有在工作,就交给它去执行任务。而如果线程池中的线程都在工作,那么就交给饱和策略去执行。

线程池

饱和策略分为下面四种:

  • AbortPolicy 直接抛出RejectedExecutionExeception 异常来阻止系统正常运行;
  • CallerRunsPolicy 将任务回退到调用者;
  • DisOldestPolicy 丢掉等待最久的任务‘;
  • DisCardPolicy 直接丢弃任务。

线程之间的通信
线程之间的通信除了我们常用的直接同步或者异步向任务队列添加任务外,还可以通过NSPort端口的形式进行发送消息,实现不同的线程间的通信。使用的时候注意需要将NSPort加入的线程的RunLoop中去。

三、GCD

GCD的特点:自动管理线程的生命周期(创建线程、调度任务、销毁线程),程序员只需要告诉GCD需要执行什么任务,而不必关心任何线程管理代码。
GCD分为串行任务队列和并发任务队列,同步函数和异步函数。
注意:在串行队列中同步的往该串行队列中添加任务,会导致死锁。
下面我们来看下GCD相关的题目

  • 题目1、下面代码的打印结果是什么?
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

答案:1 5 2 4 3
1 和 5 都是在主线程打印的,因为调用dispatch_async进行异步调用,需要耗费一些时间,而在主线程直接打印是很快的。所以1打印后,接着打印出了5,然后才会执行异步任务。接着就是2、4、3。

  • 题目2、下面代码的打印结果是什么?从ABCD选中中选择所有的可能。
    A: 1230789
    B: 1237890
    C: 3120798
    D: 2137890
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    // sleep(2);
    NSLog(@"1");
});
dispatch_async(queue, ^{
    NSLog(@"2");
});
// 堵塞 - 护犊子
dispatch_sync(queue, ^{
    NSLog(@"3");
});
// **********************
NSLog(@"0");

dispatch_async(queue, ^{
    NSLog(@"7");
});
dispatch_async(queue, ^{
    NSLog(@"8");
});
dispatch_async(queue, ^{
    NSLog(@"9");
});

答案是:A和C。
因为我们再打印3的时候使用的同步函数。所以一定是先执行3,然后执行0,然后(789)再执行所以3、0、(789)这个顺序是可以确定的,所以A和C都可能出现。

  • 题目3、下面代码的打印结果是什么?
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

答案:1、5、2 然后就会死锁卡死。因为在串行队列中同步往该队列中添加了任务,会导致添加的任务和后面的任务互相等待。

四、GCD的使用

barrier的使用

barrier的特点:堵塞指定的queue队列;只能使用自定义的并发队列,不能使用global队列。
题目一、下面代码的打印结果是什么?

dispatch_queue_t concurrentQueue = dispatch_queue_create("rzf", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
    sleep(1);
    NSLog(@"123");
});
dispatch_barrier_async(concurrentQueue, ^{
    NSLog(@"----------------- 分割------");
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"456");
});
NSLog(@"我在主线程");

结果如下

2020-02-26 13:22:20.571436+0800 004--GCD进阶使用[1696:82355] 我在主线程
2020-02-26 13:22:21.571781+0800 004--GCD进阶使用[1696:82445] 123
2020-02-26 13:22:21.572109+0800 004--GCD进阶使用[1696:82445] ----------------- 分割------
2020-02-26 13:22:21.572305+0800 004--GCD进阶使用[1696:82445] 456

barrier栅栏函数,可以堵塞指定的queue任务队列,而对其他的queue没有影响。queue中凡是在barrier任务添加之添加的任务,会在barrier任务之执行;凡是在barrier任务添加之添加的任务,会在barrier任务之执行。barrier就像在一个并发队列中放了一个闸口,将前面添加的任务和后面添加的任务完全分割来。
如果我们将上面的并发队列换成系统的global并发队列可以吗?我们看下结果:

2020-02-26 13:37:00.735816+0800 004--GCD进阶使用[3560:94316] 我在主线程
2020-02-26 13:37:00.735821+0800 004--GCD进阶使用[3560:94366] ----------------- 分割------
2020-02-26 13:37:00.735830+0800 004--GCD进阶使用[3560:94368] 456
2020-02-26 13:37:01.738121+0800 004--GCD进阶使用[3560:94365] 123

答案是不可以,系统的global不允许使用栅栏函数进行堵塞。如果换成global的话结果如下,显然不起作用了,而且还有可能崩溃。

题目二、下面代码可以正产运行吗?

dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);

for (int i = 0; i<2000; i++) {
    dispatch_async(concurrentQueue, ^{
        NSString *imageName = [NSString stringWithFormat:@"%d.jpg", (i % 10)];
        NSURL *url = [[NSBundle mainBundle] URLForResource:imageName withExtension:nil];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];
        
        [self.mArray addObject:image];
    });
}

答案是不可以,因为在for缓存中创建了好多线程去对self.mArray进行操作,显然self.mArray是线程不安全的。最后的结果有有可能和预期的不一样,也有可能崩溃。所以我们需要进行加锁处理。我们可以通过synchronized来保护。

@synchronized (self) {
    [self.mArray addObject:image];
}

也可以通过栅栏来实现

dispatch_barrier_async(concurrentQueue, ^{
    [self.mArray addObject:image];
});

这样就可以保证得到正确的结果了,避免了多线程的数据竞争。

dispatch_group的使用

创建一个group,以及两个任务队列queue和queue1。然后通过dispatch_group_async分别往两个队列中添加block任务。最后通过dispatch_group_notify配置组任务完成后的回调。这样我们就可以是实现多个任务全部完成后的一个汇总操作,而且不同的任务可以不在一个队列中。

//创建调度组
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_queue_t queue1 = dispatch_queue_create("com.lg.cn", DISPATCH_QUEUE_CONCURRENT);

// SIGNAL
dispatch_group_async(group, queue, ^{
    NSString *logoStr = @"http://p.qpic.cn/qqcourse/QFzQYCgCrxlq7n5Jats36WGb4wxzJIYmFplnUUgdxk4gy66xicbmGCDM5DXDuVNQP/";
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr]];
    UIImage *image = [UIImage imageWithData:data];
    [self.mArray addObject:image];
});

dispatch_group_async(group, queue1, ^{
    sleep(2);
    NSString *logoStr = @"https://www.baidu.com/img/baidu_resultlogo@2.png";
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr]];
    UIImage *image = [UIImage imageWithData:data];
    [self.mArray addObject:image];
});

__block UIImage *newImage = nil;
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"数组个数:%ld",self.mArray.count);
    for (int i = 0; i<self.mArray.count; i++) {
        UIImage *waterImage = self.mArray[i];
        newImage = [KC_ImageTool kc_WaterImageWithWaterImage:waterImage backImage:newImage waterImageRect:CGRectMake(20, 100*(i+1), 100, 40)];
    }
    self.imageView.image = newImage;
});

我们还可以通过dispatch_group_enterdispatch_group_leave向group添加任务。注意enter和leave要成对出现,如果 dispatch_group_enterdispatch_group_leave多,dispatch_group_notify不会被调用;如果dispatch_group_enterdispatch_group_leave 少,就会奔溃。

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
dispatch_async(queue, ^{
    NSLog(@"第一个走完了");
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
    NSLog(@"第二个走完了");
    dispatch_group_leave(group);
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"所有任务完成,可以更新UI");
});

dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)同步等待group任务的完成。会堵塞当前线程。可以设置等待的时间,当超过设置的时间如果任务还没有完成,就会直接返回非0值。如果任务在设置的时间内完成就会返回0。

long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW,1 * NSEC_PER_SEC));
if (timeout == 0) {
    NSLog(@"回来了");
}else{
    NSLog(@"等待中 -- 转菊花");
}
dispatch_semaphore的使用

我们可以通过semaphore信号量实现控制并发队列中任务的并发数,就像NSOperationQueue中的maxConcurrentOperationCount一样。下面代码设置任务的并发数为3。

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 信号量 -- gcd控制并发数
// 同步
//总结:由于设定的信号值为3,先执行三个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过3
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);

//任务1
dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"执行任务1");
    sleep(1);
    NSLog(@"任务1完成");
    dispatch_semaphore_signal(semaphore);
});

//任务2
dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"执行任务2");
    sleep(1);
    NSLog(@"任务2完成");
    dispatch_semaphore_signal(semaphore);
});

//任务3
dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"执行任务3");
    sleep(1);
    NSLog(@"任务3完成");
    dispatch_semaphore_signal(semaphore);
});
dispatch_source的使用

dispatch_source的特点:效率高,不依赖于runloop。使用dispatch_source创建的time相比于NSTimer可以非常的精确。
dispatch_source相关的api:
dispatch_source_create 创建source
dispatch_source_set_event_handler 设置事件的回调
dispatch_source_merge_data 添加数据,会触发DISPATCH_SOURCE_TYPE_DATA_ADD 和DISPATCH_SOURCE_TYPE_DATA_OR类型source的事件回调。
dispatch_source_get_data 获取上面merge_data添加的数据。
dispatch_resume 启动
dispatch_suspend 挂起

下面我们来演示创建一个DISPATCH_SOURCE_TYPE_DATA_ADD类型的source。该类型的source可以根据添加数据进行响应。首先我们创建source,然后设置source的句柄,也就是回调代码块,在回调中获取传递过来的data,我们这里的data是一个进度。最后resume这个source。
我们还可以使用DISPATCH_SOURCE_TYPE_TIMER这个type来实现定时器的功能。

//创建source
self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());

// 封装我们需要回调的触发函数 -- 响应
dispatch_source_set_event_handler(self.source, ^{
    
    NSUInteger value = dispatch_source_get_data(self.source); // 取回来值 1 响应式
    self.totalComplete += value;
    NSLog(@"进度:%.2f", self.totalComplete/100.0);
    self.progressView.progress = self.totalComplete/100.0;
});

//启动source
dispatch_resume(self.source);

上面我们在回调中处理了数据,下面我们需要使用dispatch_source_merge_data传输数据,触发回调句柄。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    for (NSUInteger index = 0; index < 100; index++) {
        dispatch_async(self.queue, ^{
            sleep(2);
            dispatch_source_merge_data(self.source, 1); // source 值响应
        });
    }
}

我们在touchesBegan方法中,循环merge_data。每次传输一个1过去。然后回调句柄那可以收到该数据1。
我们还可以在一个按钮的点击方法中控制该source的暂停和继续:

- (IBAction)didClickStartOrPauseAction:(id)sender {
    if (self.isRunning) {// 正在跑就暂停
        dispatch_suspend(self.source);
        dispatch_suspend(self.queue);
        [sender setTitle:@"暂停中..." forState:UIControlStateNormal];
    }else{
        dispatch_resume(self.source);
        dispatch_resume(self.queue);
        [sender setTitle:@"加载中..." forState:UIControlStateNormal];
    }
}

因为self.queue是用来为source传输数据用的,所以暂停source后,queue也对应的挂起。

五、GCD的源码实现

  • 1、主队列是个全局的静态变量,所以才可以全局直接调用。
struct dispatch_queue_static_s _dispatch_main_q = {
    DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
#if !DISPATCH_USE_RESOLVERS
    .do_targetq = _dispatch_get_default_queue(true),
#endif
    .dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
            DISPATCH_QUEUE_ROLE_BASE_ANON,
    .dq_label = "com.apple.main-thread",
    .dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
    .dq_serialnum = 1,
};
  • 2、自定义队列以及global队列都是模式化创建的,
    我们调用create方法进行创建队列。
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)

creat方法会调用下面的init方法,进行构造queue。

// 构造方法
_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
            DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
            (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));
......
dq->do_targetq = tq;

我们看到上面方法构造的dq的属性do_targetq=tq,然后我们来看下tq的创建。

tq = _dispatch_get_root_queue(
        qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos, // 4
        overcommit == _dispatch_queue_attr_overcommit_enabled)->_as_dq; // 0 1
if (unlikely(!tq)) {
    DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
}

然后我们跟踪_dispatch_get_root_queue这个方法。

static inline dispatch_queue_global_t
_dispatch_get_root_queue(dispatch_qos_t qos, bool overcommit)
{
    if (unlikely(qos < DISPATCH_QOS_MIN || qos > DISPATCH_QOS_MAX)) {
        DISPATCH_CLIENT_CRASH(qos, "Corrupted priority");
    }
    // 4-1= 3
    // 2*3+0/1 = 6/7
    return &_dispatch_root_queues[2 * (qos - 1) + overcommit];
}

通过上面的方法我们看到,_dispatch_get_root_queue方法实际上就是去_dispatch_root_queues这个数组里面去值。至此我们可以总结:自定义队列和global队列都是通过_dispatch_root_queues这个数组来进行初始化创建的。
_dispatch_root_queues这个数组是在dispatch_init方法中初始化创建的。

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

推荐阅读更多精彩内容

  • 文章目录GCD简介任务和队列GCD的使用步骤队列的创建方法任务的创建方法GCD的基本使用并行队列 + 同步执行并行...
    lusen_b阅读 247评论 0 1
  • 一、前言 上一篇文章iOS多线程浅汇-原理篇中整理了一些有关多线程的基本概念。本篇博文介绍的是iOS中常用的几个多...
    nuclear阅读 2,053评论 6 18
  • 本文用来介绍 iOS 多线程中 GCD 的相关知识以及使用方法。这大概是史上最详细、清晰的关于 GCD 的详细讲...
    花花世界的孤独行者阅读 505评论 0 1
  • NSThread 第一种:通过NSThread的对象方法 NSThread *thread = [[NSThrea...
    攻城狮GG阅读 803评论 0 3
  • 161130 本月计划要写的2篇读书笔记,一直没有下笔,最后一天,没法给自己交代,完成一篇是一篇。 村上大叔每年都...
    行_素阅读 1,129评论 0 1