iOS 多线程

iOS 多线程有几种方式

  • GCD
  • NSOpeartion
  • NSThread
  • phread

多线程

GCD
  1. dispatch_once_t

    + (TestModel *)shared {
        static TestModel *model;
        static dispatch_once_t once;
        dispatch_once(&once, ^{
            model = [[TestModel alloc] init];
        });
        return model;
    }
    
  2. dispatch_group

    - (void)dispatchGroup {
        dispatch_queue_t queue = dispatch_queue_create("groupTest", DISPATCH_QUEUE_CONCURRENT);
        dispatch_group_t group = dispatch_group_create();
    
        dispatch_group_async(group, queue, ^{
            NSLog(@"任务1 ready");
            sleep(2);
            NSLog(@"任务1 完成");
        });
         dispatch_group_enter(group);
        dispatch_async(queue, ^{
            NSLog(@"任务2 ready");
            sleep(4);
            NSLog(@"任务2 完成");
            dispatch_group_leave(group);
        });
        dispatch_group_notify(group, queue, ^{
            NSLog(@"group 任务完成");
        });
    }
    
    • dispatch_group_notify 可以添加多次,并会多次调用
    • 只要 group 中的任务没有完成,group 完成的监听就不会被调用,即使是后追加的任务
    • notify 方法中第二个 queue 的参数决定了 callBack 将会在那个队列执行
  3. dispatch_apply 多线程快速遍历

    • 本质是 dispatch_syncdispatch_group 关联的 api,因为该方法会等待内部所有操作都结束再返回,内部操作是否同步依赖传入的queue,外部必定是同步的。如果有需要,需将该方法放到一个异步的并行队列中

    • 如果传入多线程,输出的下标未必按照顺序执行

    - (void)dispatchApply {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_apply(10, queue, ^(size_t index) {
            NSLog(@"%@------%d", NSThread.currentThread, index);
            sleep(1);
        });
         NSLog(@"111111");
    }
    // "111111" 必定出现在最后
    
  4. 定时器 DispatchSourceTimer

    不同于基于 Runloop 的 NSTimerDispatchSourceTimer 不会因为子线程没有正在运行的 Runloop 而失效,也不会有循环引用、计时不准(每次 runloop 循环才会检查定时器是否需要被执行)等问题。但有几点需要注意:

    • suspend 与 resume 一定要成对使用,否则会 crash
    • timer 最好被持有,否则在 suspend 时可能 crash
    var timer: DispatchSourceTimer?
    var index: Int = 0
    func startTimer() {
        timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.global())
        timer?.schedule(deadline: .now(), repeating: 0.1)
        timer?.setEventHandler(handler: {
            print("index: \(index)")
            index += 1
            if index == 10 {
                cancelTimer()
            }
        })
        timer?.resume()
    }
    
    private func cancelTimer() {
        timer?.cancel()
        timer = nil
    }
    
  5. 信号量 DispatchSemaphore

    信号量类似于锁,信号量为 0 则阻塞线程,大于 0 则不会阻塞。因此可以通过改变信号量的值来控制是否阻塞线程

    • DispatchSemaphore(value: 0) 初始化
    • semaphore.signal() // 信号量 +1
    • semaphore.wait() 在信号量大于 0 的前提下,信号量 -1,如果信号量本来为 0,则线程休眠,加入到等待这个信号的线程队列当中。当信号量大于 0 时,就会唤醒这个等待队列中靠前的线程,继续线程后面的代码且对信号量减 1,也就确保了信号量大于 0 才减 1,所以不存在信号量小于 0 的情况(除非在初始化时设置为负数,不过这样做应用程序会 crash)。
    // 下载两个图片后执行操作 A
    // 1. 初始化信号量为 0
    var seamphore = DispatchSemaphore(value: -0)
    // 2. 操作 A 前添加两个 wait 操作
    seamphore.wait()
    seamphore.wait()
    // 3. 在每个下载结果回调中添加
    seamphore.signal()
    
  6. 线程栅栏 dispatch_barrier

    线程栅栏可以阻塞某个 queue(必须是自定义的并行 queue,如果是 global ,则不会有预期效果)中任务的执行直到 queue 中栅栏之前的任务执行完毕

    - (void)dispatchBarrier {
        dispatch_queue_t queue = dispatch_queue_create("aaa", DISPATCH_QUEUE_CONCURRENT);
        for (int i = 1; i <= 3; i ++) {
            dispatch_async(queue, ^{
                sleep(1);
                NSLog(@"%d 任务结束", i);
            });
        }
        NSLog(@"栅栏前面");
        dispatch_barrier_sync(queue, ^{
            sleep(3);
            NSLog(@"栅栏结束");
        });
        NSLog(@"栅栏后面");
        for (int i = 4; i <= 6; i ++) {
            dispatch_async(queue, ^{
                sleep(1);
                NSLog(@"%d 任务结束", i);
            });
        }
        NSLog(@"代码结束");
    }
    

    运行结果

    dispatch_barrier_sync 运行结果
    2020-07-24 20:17:09.905062+0800 ObjcTest[13685:969878] 栅栏前面
    2020-07-24 20:17:10.907255+0800 ObjcTest[13685:969907] 2 任务结束
    2020-07-24 20:17:10.907310+0800 ObjcTest[13685:969908] 1 任务结束
    2020-07-24 20:17:10.907579+0800 ObjcTest[13685:969906] 3 任务结束
    2020-07-24 20:17:13.907888+0800 ObjcTest[13685:969878] 栅栏结束
    2020-07-24 20:17:13.908349+0800 ObjcTest[13685:969878] 栅栏后面
    2020-07-24 20:17:13.908954+0800 ObjcTest[13685:969878] 代码结束
    2020-07-24 20:17:14.913851+0800 ObjcTest[13685:969906] 4 任务结束
    2020-07-24 20:17:14.914364+0800 ObjcTest[13685:969908] 5 任务结束
    2020-07-24 20:17:14.914628+0800 ObjcTest[13685:969907] 6 任务结束
    dispatch_barrier_async 运行结果
    2020-07-24 20:18:16.955921+0800 ObjcTest[13691:970322] 栅栏前面
    2020-07-24 20:18:16.956002+0800 ObjcTest[13691:970322] 栅栏后面
    2020-07-24 20:18:16.956040+0800 ObjcTest[13691:970322] 代码结束
    2020-07-24 20:18:17.961793+0800 ObjcTest[13691:970352] 1 任务结束
    2020-07-24 20:18:17.962022+0800 ObjcTest[13691:970353] 2 任务结束
    2020-07-24 20:18:17.963561+0800 ObjcTest[13691:970357] 3 任务结束
    2020-07-24 20:18:20.967381+0800 ObjcTest[13691:970357] 栅栏结束
    2020-07-24 20:18:21.974364+0800 ObjcTest[13691:970357] 4 任务结束
    2020-07-24 20:18:21.974901+0800 ObjcTest[13691:970353] 5 任务结束
    2020-07-24 20:18:21.975174+0800 ObjcTest[13691:970352] 6 任务结束
    

    dispatch_barrier_syncdispatch_barrier_async 区别为

    • 同步栅栏会阻塞之后的普通代码的执行,异步栅栏则不会

    应用线程栅栏的特性,可以更好的做一些线程同步。例如

    任务 1、2、3结束后需要执行任务 4、5、6

    如果用 dispatch_group ,则将任务 1、2、3 添加到 group 中,在 dispatch_group_notify 中执行任务 4、5、6

NSOperation

NSOperation 是苹果 GCD 面向对象的封装

优点:

  • 添加操作间的依赖关系,控制执行顺序

  • 可以方便的取消一个操作的执行

  • 设定操作的优先级

  • 使用 KVO 观察操作执行状态的更改

  1. NSOperation 执行的方式

    • 添加到 NSOperationQueue,系统会自动将 NSOperationQueue 中的 NSOperation 取出来,在线程中执行操作
    • 如果不显式将 NSOperation 添加到 NSOperationQueue 中,也可以调用 operation.start() 方法,操作会在当前线程中执行

    备注:使用 start 方法时,如果 NSBlockOperation 调用 addExecutionBlock:方法,添加额外的操作,包括 blockOperationWithBlock 中的操作在内的这些操作可能在不同的线程中并发执行,具体由系统决定

        NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
            for (int i = 0; i < 3; i ++) {
                sleep(2);
                NSLog(@"1---%@", NSThread.currentThread);
            }
        }];
        [op addExecutionBlock:^{
            for (int i = 0; i < 3; i ++) {
                sleep(2);
                NSLog(@"2---%@", NSThread.currentThread);
            }
        }];
        [op addExecutionBlock:^{
            for (int i = 0; i < 3; i ++) {
                sleep(2);
                NSLog(@"3---%@", NSThread.currentThread);
            }
        }];
        [op addExecutionBlock:^{
            for (int i = 0; i < 3; i ++) {
                sleep(2);
                NSLog(@"4---%@", NSThread.currentThread);
            }
        }];
        [op addExecutionBlock:^{
            for (int i = 0; i < 3; i ++) {
                sleep(2);
                NSLog(@"5---%@", NSThread.currentThread);
            }
        }];
        [op addExecutionBlock:^{
            for (int i = 0; i < 3; i ++) {
                sleep(2);
                NSLog(@"6---%@", NSThread.currentThread);
            }
        }];
        [op start];
    
    2020-07-27 14:19:25.237520+0800 ObjcTest[19476:1527007] 1---<NSThread: 0x282870ac0>{number = 5, name = (null)}
    2020-07-27 14:19:25.237532+0800 ObjcTest[19476:1526976] 2---<NSThread: 0x28283da80>{number = 1, name = main}
    2020-07-27 14:19:27.239170+0800 ObjcTest[19476:1526976] 2---<NSThread: 0x28283da80>{number = 1, name = main}
    2020-07-27 14:19:27.239164+0800 ObjcTest[19476:1527007] 1---<NSThread: 0x282870ac0>{number = 5, name = (null)}
    2020-07-27 14:19:29.240627+0800 ObjcTest[19476:1526976] 2---<NSThread: 0x28283da80>{number = 1, name = main}
    2020-07-27 14:19:29.240620+0800 ObjcTest[19476:1527007] 1---<NSThread: 0x282870ac0>{number = 5, name = (null)}
    2020-07-27 14:19:31.242202+0800 ObjcTest[19476:1527007] 4---<NSThread: 0x282870ac0>{number = 5, name = (null)}
    

    从结果中可以看出 blockOperationWithBlock 中的代码不是在主线程中执行的,并且系统一共开启了两个线程执行该 Operation

  2. NSOperationQueue 控制串行、并发

    maxConcurrentOperationCount 属性可以控制一个队列中可以有多少个操作同时参与并发执行

    注意:这里的 maxConcurrentOperationCount 控制的不是并发线程的数量,而是一个队列中同时能并发执行的最大操作数。而且一个操作也并非只能在一个线程中运行。

  3. NSOperation 操作依赖

    NSOperation 可以添加操作之间的依赖。因此我们可以很方便的控制操作之间的执行先后顺序。并且一个操作可以添加多个依赖。

    例如我们有如下需求:A、B两个操作,A 执行完毕,B 才能执行操作。

    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    // 2.创建操作
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
             [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
             NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
        }
    }];
    
    // 3.添加依赖
    [op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2
    
    // 4.添加操作到队列中
    [queue addOperation:op1];
    [queue addOperation:op2];
    

    输出如下

    2020-07-27 14:43:35.998300+0800 ObjcTest[19617:1533985] 1---<NSThread: 0x281496200>{number = 6, name = (null)}
    2020-07-27 14:43:38.000611+0800 ObjcTest[19617:1533985] 1---<NSThread: 0x281496200>{number = 6, name = (null)}
    2020-07-27 14:43:40.007131+0800 ObjcTest[19617:1533985] 2---<NSThread: 0x281496200>{number = 6, name = (null)}
    2020-07-27 14:43:42.021944+0800 ObjcTest[19617:1533985] 2---<NSThread: 0x281496200>{number = 6, name = (null)}
    

    可以看出,op1 先执行完毕后,op2 才开始执行

  4. NSOperation 优先级

    优先级只体现在两个时间点

    • 依赖任务处理完成、队列对后续任务的调度
    • 依赖队列从暂停转变为重新启动、后续任务的调度
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"1--- 开始执行, %@", NSThread.currentThread);
            sleep(2);
            NSLog(@"1--- 执行完毕");
        }];
        op1.queuePriority = NSOperationQueuePriorityLow;
    
        NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"2--- 开始执行, %@", NSThread.currentThread);
            sleep(2);
            NSLog(@"2--- 执行完毕");
        }];
        op2.queuePriority = NSOperationQueuePriorityHigh;
    
        NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"依赖--- 开始执行, %@", NSThread.currentThread);
            sleep(2);
            NSLog(@"依赖--- 执行完毕");
        }];
        [op1 addDependency:op3];
        [op2 addDependency:op3];
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        queue.maxConcurrentOperationCount = 1;
        [queue addOperation:op1];
        [queue addOperation:op2];
        [queue addOperation:op3];
    

    结果如下

    2020-07-27 15:13:05.623639+0800 ObjcTest[19750:1540177] 依赖--- 开始执行, <NSThread: 0x281561c00>{number = 5, name = (null)}
    2020-07-27 15:13:07.629611+0800 ObjcTest[19750:1540177] 依赖--- 执行完毕
    2020-07-27 15:13:07.630799+0800 ObjcTest[19750:1540177] 2--- 开始执行, <NSThread: 0x281561c00>{number = 5, name = (null)}
    2020-07-27 15:13:09.631681+0800 ObjcTest[19750:1540177] 2--- 执行完毕
    2020-07-27 15:13:09.632164+0800 ObjcTest[19750:1540176] 1--- 开始执行, <NSThread: 0x281561380>{number = 3, name = (null)}
    2020-07-27 15:13:11.637458+0800 ObjcTest[19750:1540176] 1--- 执行完毕
    
  5. NSOperationQueue 暂停和取消

    注意:

    1. 这里的暂停和取消(包括操作的取消和队列的取消)并不代表可以将当前的操作立即取消,而是当当前的操作执行完毕后不再执行新的操作
    2. 暂停和取消的区别在于:暂停之后还可以恢复操作,继续向下执行;而取消操作之后,所有的操作就清空了,不再执行剩下的操作
NSThread

NSThread 在代码中偶尔会使用,例如 [NSThread currentThread]

  1. 创建、启动线程

    - (void)threadTest {
        NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
        [thread start];
      // 创建线程后自动启动线程
      //[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
    
      // 隐式创建并启动线程
      //[self performSelectorInBackground:@selector(run) withObject:nil];
    }
    
    - (void)run {
        NSLog(@"currentThread: %@", NSThread.currentThread);
    }
    
  2. 线程状态控制

    + (void)sleepUntilDate:(NSDate *)date;
    + (void)sleepForTimeInterval:(NSTimeInterval)ti;// 线程进入阻塞状态
    + (void)exit; // 线程 kill
    
  3. 线程间通信

    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
    
  4. 与 runloop 关系

    - (void)run {
        NSLog(@"currentThread: %@", NSThread.currentThread);
        [self performSelector:@selector(threadAfter) withObject:nil afterDelay:1];
    }
    
    - (void)threadAfter {
        NSLog(@"1111111");
    }
    

    上述代码默认不会起作用,‘1111111’ 不会被打印出来。 performSelector: withObject: afterDelay: 方法默认会创建一个 timer 添加到当前 runloop 中,而子线程默认不开启 runloop,因此上述代码不起作用。
    解决方法是在 performSelector 方法后面添加[[NSRunLoop currentRunLoop] run];
    如果将 runloop 启动的代码放到前面,仍然不会起作用,原因是 runloop 启动后没有可执行的代码,会立刻退出,此时再添加 timer 也没有什么作用。

pthread

pthread 是一套通用的多线程 API,使用 C 语言编写,需要程序员自己管理线程的生命周期,使用难度较大,很少使用

  • pthread_create()创建一个线程
  • pthread_exit()终止当前线程
  • pthread_cancel()中断另外一个线程的运行
  • pthread_join()阻塞当前的线程,直到另外一个线程运行结束
  • pthread_attr_init()初始化线程的属性
  • pthread_attr_setdetachstate()设置脱离状态的属性(决定这个线程在终止时是否可以被结合)
  • pthread_attr_getdetachstate()获取脱离状态的属性
  • pthread_attr_destroy()删除线程的属性
  • pthread_kill()向线程发送一个信号

问题

  1. 子线程同时执行 ABC 三个同步任务,全部执行完毕后再在自线程执行三个同步任务 EDF,应该怎样做?
    • 可以使用 GCD 的group 或者 NSOperation 的 依赖
    • 可以使用 dispatch_barrier 稍简单一些
  2. 将问题 1 中的 ABC 三个任务改为异步任务如 AFN 网络请求,全部回调成功后进行数据整合,应该怎样做?
    1. 使用信号量
    2. 使用 GCD 的 group 也可以。dispatch_group_enterdispatch_group_leave 搭配使用
  3. 线程的生命周期是怎样的?
    可参考 线程的生命周期以及常驻线程

参考

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