2021-02-04 多线程

为啥要并发

  • 让多个任务同时执行
  • 提高app运行的性能,保证App实时响应
    1因为UI界面运行在主线程之上,它是一个串行线程。如果将所有代码都放在主线程上运行,那么主线程将承担网络请求,数据处理,图像渲染等操作,无论是GPU还是计算机内存,都会性能耗尽,从而影响用户体验。

线程和进程的区别

同步和异步,串行和并行。多任务和阻塞

异步:异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情。实现异步可以采用多线程技术或则交给另外的进程来处理。
Sync Async Serial Concurrent

  • Serial/Concurrent声明队列的属性是串行的还是并行的。串行队列(Serial Queue)指在同一时间内,队列中只能执行一个任务,当前任务执行完后才能执行下一个任务。
    在串行队列中只有一个线程。
    并行队列(Concurrent Queue)允许多个任务在同一个时间同时进行,在并行队列中有多个线程。串行队列的任务一定是按开始的顺序结束。而并行队列的任务并不一定会按照开始的顺序结束。

任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:『同步执行』 和 『异步执行』。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

同步执行(sync):
同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async):
异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
可以在新的线程中执行任务,具备开启新线程的能力。
串行队列(Serial Dispatch Queue):
每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Dispatch Queue):
可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
注意:并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。

  • Sync/Async声明任务是同步还是异步执行的。
    同步(Sync)会把当前的任务加到队列中,等到任务执行完成,线程才会返回继续运行。
    也就是说,同步会阻塞线程。
    异步(Async)也会把当前的任务添加到队列中,但它会立刻返回,无须等任务执行完成,也就是说异步不会阻塞线程。
    无论是串行队列还是并行队列,都可以执行异步或者同步操作。
    注意,在串行队列中执行同步操作容易造成死锁。
    在并行队列中则不用担心这个问题。异步操作无论是在串行队列中执行还是在并行队列中执行,都可能出现竞态问题;
    同时,异步操作经常与逃逸闭包一起出现在API的设计中

串行队列的代码实战

// 串行同步
serialQueue.sync {
print(1);
}
print(2);
serialQueue.sync {
print(3);
}
print(4);
1
2
3
4
串行队列中同步操作时打印1,2,3,4

// 串行异步
serialQueue.async {
print(1);
}
print(2);
serialQueue.asyn {
print(3);
}
print(4);
serialQueue mainqueue
1 2不用等1
3等1 3等2 4等2
1 不用等我执行完 12 21
2 主队列,同步 1243 1234 2134 2143 2413
3 不用等我执行完
4 主队列,同步

同步

同步执行:比如这里的dispatch_sync,这个函数会把一个block加入到指定的队列中,而且会一直等到执行完blcok,这个函数才返回。因此在block执行完之前,调用dispatch_sync方法的线程是阻塞的。

异步

异步执行:一般使用dispatch_async,这个函数也会把一个block加入到指定的队列中,但是和同步执行不同的是,这个函数把block加入队列后不等block的执行就立刻返回了。

// 串行异步嵌套同步 1
外边异步块 2
5 3阻塞线程等3完成
print(1) 4
serialQueue.async {
print(2)
serialQueue.sync {
print(3)
}
print(4)
}
print(5)
dispatch_sync() 同步执行,完成了它预定的任务后才返回,阻塞当前线程
dispatch_async() 异步执行,会立即返回,预定的任务会完成但不会等它完成,不阻塞当前线程
mainqueue serialQueue不同等
1 2 不同等
5 3 3进行的条件是整个block块结束,3等块结束
4 4等3结束
1,2

串行队列+同步任务:不会开启新的线程,任务逐步完成。
不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。

串行队列+异步任务:开启新的线程,任务逐步完成。
并发队列+同步任务:不会开启新的线程,任务逐步完成。

  • 任务按顺序执行的。按顺序执行的原因:虽然 并发队列 可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务 不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务 需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。

并发队列+异步任务:可以开启多个线程,任务交替(同时)执行。
|
串 主
1 2
3 4

1一定在3之前被打印出来,因为1在3之前派发,串行队列一次只能执行一个任务。所以一旦派发完就执行任务。
2一定在4之前被打印出来
2一定在3之前被打印出来

多线程的实现方式?区别?每种方式的适用场景

1Pthread

pthread 是一套通用的多线程的 API,可以在Unix / Linux / Windows 等系统跨平台使用,使用 C 语言编写,需要程序员自己管理线程的生命周期,使用难度较大,我们在 iOS 开发中几乎不使用 pthread,但是还是来可以了解一下的。
场景 获取堆栈信息
https://blog.csdn.net/wxs0124/article/details/104961809
/_np 是指 not POSIX ,这里的 POSIX 是指操作系统的一个标准,特别是与 Unix 兼容的操作系统。np 表示与标准不兼容
pthread_t pt = pthread_from_mach_thread_np(list[i]);
获取当前调用栈的信息

#pragma mark - Interface
+ (NSString *)callStackWithType:(SMCallStackType)type {
// 所有线程
if (type == SMCallStackTypeAll) {
thread_act_array_t threads; //int 组成的数组比如 thread[1] = 5635

2NSThread

最大限度的掌控每个线程的生命周期。但是,也需要开发者手动管理所有的进程活动,
比如创建、同步、暂停、取消,其中手动加锁操作的挑战性很大。
NSThread总体使用场景很小,基本是在开发底层的开源软件或是测试时使用。
还有就是获取一些线程信息,如当前线程,主线程,线程名称。

NSThread *nsthread = [NSThread currentThread]; //当前执行的指令

3GCD

4Operation

// A、B、C、D、E、F六个任务
// A、B、C 并发执行
// D--->A,B
// E--->B,C
// F--->D、E
// 1A、B、C没有依赖关系

NSOperation实现

 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    queue.maxConcurrentOperationCount = 6;
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"A任务");
        }
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"B任务");
        }
    }];
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"C任务");
        }
    }];
    NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"D任务");
        }
    }];
    NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"E任务");
        }
    }];
    NSBlockOperation *op6 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"F任务");
        }
    }];
    [op4 addDependency:op1];
    [op4 addDependency:op2];
    [op5 addDependency:op2];
    [op5 addDependency:op3];
    [op6 addDependency:op4];
    [op6 addDependency:op5];
    [queue addOperation:op1];
    [queue addOperation:op2];
    [queue addOperation:op3];
    [queue addOperation:op4];
    [queue addOperation:op5];
    [queue addOperation:op6];

dispatch实现

/ D--->A,B
    dispatch_group_t g1 = dispatch_group_create();
    // E--->B,C
    dispatch_group_t g2 = dispatch_group_create();
    // F--->D、E
    dispatch_group_t g3 = dispatch_group_create();
    dispatch_group_enter(g1);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"A任务");
        }
        dispatch_group_leave(g1);
    });
    dispatch_group_enter(g1);
    dispatch_group_enter(g2);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"B任务");
        }
        dispatch_group_leave(g1);
        dispatch_group_leave(g2);
    });
    dispatch_group_enter(g2);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"C任务");
        }
        dispatch_group_leave(g2);
    });
    dispatch_group_enter(g3);
    dispatch_group_notify(g1, dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"D任务");
        }
        dispatch_group_leave(g3);
    });
    dispatch_group_enter(g3);
    dispatch_group_notify(g2, dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"E任务");
        }
        dispatch_group_leave(g3);
    });
    dispatch_group_notify(g3, dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"F任务");
        }
    });

dispatch_group_t底层
底层是一个结构体
类似一个链表的形式存储当前的任务
struct dispatch_group_s {
DISPATCH_SEMAPHORE_HEADER(group, dg)
int_volatile dg_waiters;
struct dispatch_continuation_s * volatitile dg_notify_head;
struct dispatch_continuation_s * volatitile dg_notify_tail;
}
enter函数原子自增,dg_value做加 两次enter之后就是等于2
leve函数原子自减,dg_value做减,每次减1
当dg_value为0的时候,就是执行_dispatch_group_wake(dg, false)
(dispatch源码苹果官网可下载)
enter,leave类似于信号量的标记
标记当前有多少个任务要执行
notify就是等group的任务为0,就是dg_value的任务为0的时候

BlockOperation是基于状态机制实现的
队列的任务是isReady状态时,才会被队列调度
operationqueue是一个队列
adddependency其实就是在底层建立一个依赖关系
比如D任务依赖于A,B其实实现就是,等A任务B任务的状态都编程isFinish后,D任务的状态变成isReady,然后isReady的任务就可以被调度了
所以D需要监听A,B的状态值变化,就是一个KVO
A依赖于B任务,A会存在一个dpendencies数组,里面存放着B
B有downdenpendices数组,存放着B
isReady的判断是根据当前计数器判断
然后最底层还是根据GCD来调度
operation源码在gihub上

多线程和runloop的关系

  1. 一一对应的关系
    -通过k-value的形式存储在一个全局的字典中,来对应当前的线程和
    runloop
    -runloop源码
    TSL是线程局部存储空间
    当前线程访问autoreleasepool
    return os_mac
    pthread_getspecific
    return os_linux
    pthread_getspecific
    return os_win32
    pthread_getspecific
    私有函数,当前线程的局部存储空间只能被当前线程所访问到,其他线程访问不到。
    runloop没有就会创建,放在全局字典里的同时也会在TSL里做一个缓存。
    当前线程的保活
    子线程执行完任务后就会销毁掉,
    线程常驻,就是
    _port = [NSMacPort port];
    _thread = [[LXJThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil]];
    [_thread start];
    @autoreleasepool {
    // port mach_msg 消息队列的收发,决定当前runloop是休眠还是执行任务
    // port是和mac_os内核有关的东西
    // 端口收发消息队列
    // 先获取当前子线程的runloop
    NSRunloop *runloop = [NSRunloop currentRunloop];
    // 向当前的子线程runloop add port
    // port用来收发消息
    [self registerobserver]
    [[runloop addPort:port forMode:nsrunloopdefaultnmode]];
    [runloop run];
    线程常驻后如何销毁子线程
    子线程能存活就是依赖与port事件源
    是否可以移除事件源头,然后解除
    // runloop不会退出 [[runloop remove:port forMode:nsrunloopdefaultnmode]];
    如果控制器销毁,runloop会不会退出
    // runloop不会退出,还会有野指针问题
    // 因为移除当前线程的事件源时,并不能保证系统不往线程里添加一些额外的事件源。所以没有办法通过移除自己事件源来移除常驻线程。
    // 当前直接去退出当前线程
    // [NSThread exit];线程无法响应,当前线程销毁,野指针。程序崩溃,线程销毁了,runloop不销毁还会造成内存泄漏。
    // 看源码CFRunloop怎么stop的
    // 判断CFRunloop finished是通过
    // CFRunloop isFinished
    // 如果当前runloop的mode是空的或者runloopmode是空,isfinish就是true。没有runloop mode 就finish了但是对现在没有用,因为当前runloop run 肯定有mode
    // 如果mode name == null或者是commonmode(commomode只是一个标记)
    // runloop run起来本质就是一个while循环
    // 循环结束依赖于。。。
    // _cfrunllop stop就相当于有了退出标记
    // CFRunloopStop([CFRunloop current])当前的runloop还是退出不了
    }
    // 仿照源码
    runloop runmode:nsdefaultmode beforedate: [snadate distantfuture]
  1. 有序列表第二项

# iOS 并发编程中的三大问题
* 竞态条件
  两个或两个以上线程对共享的数据进行读写操作时,最终的数据结果不确定的情况。
* 优先倒置
  低优先级的任务会因为各种原因先于高优先级的任务执行。
* 死锁问题
  两个或者两个以上的线程,它们之间互相等待彼此停止执行,以获得某种资源,但是没有一方会提前退出的情况。在iOS开发中,**有一个经典的例子就是两个Operation互相依赖。
在对同一个串行队列中进行异步、同步嵌套时:
#如何Debug并发编程问题
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容