Objective-C 用代码揭示多线程

Pthreads
  1. 其实这个方案不用说的,只是拿来充数,为了让大家了解一下就好了。
  2. 简单地说,这是一套在很多操作系统上都通用的多线程 API,所以移植性很强(然并卵)。
  3. 它是需要 C 语言函数,这是比较蛋疼的,更蛋疼的是你需要手动处理线程的各个状态的转换即管理声明周期,比如创建线程,销毁线程。
NSThread

这套方案是经过苹果封装后的,并且完全面向对象的。所以你可以直接操控线程对象,非常直观和方便。但是,它的生命周期还是需要我们手动管理,所以这套方案也是偶尔用用,比如[NSThread currentThread],它可以获取当前线程,你就可以知道当前线程的各种属性,用于调试十分方便。

  1. 其实,NSThread 用起来也挺简单的,因为它就那几种方法。同时,我们也只有在一些非常简单的场景才会用 NSThread,毕竟它还不够智能,不能优雅地处理多线程中的其他高级概念。
GCD

Grand Central Dispatch,听名字就霸气,他是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的 CPU 内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理,我们只需要告诉干什么就行。同时它使用的也是 C 语言,不过由于使用了 Block(Swift 里面叫闭包),使得使用起来非常方便,而且灵活。所以基本上大家都是用 GCD 这套方案,老少咸宜,实在是居家旅行、杀人灭口,必备良药。
核心概念1. dispatch_async 有创建新线程的能力(不一定创建),不会阻塞当前线程。
核心概念2. dispatch_sync 没有创建新线程的能力,会阻塞当前线程,直到 Block 任务完成。

代码练习

   // 获取: 主队列:这是一个特殊的串行队列.什么是主队列,大家都知道吧,它用户刷新 UI,任何需要刷新 UI 的工作都要在主队列执行,所以一般耗时的任务都要放到别的线程执行.
   dispatch_queue_t queue = dispatch_get_main_queue();
   NSLog(@"%@",queue); // <OS_dispatch_queue_main: com.apple.main-thread>
   
   // 创建: 串行队列
   dispatch_queue_t serialQueue1 = dispatch_queue_create("com.baidu.searialQueue1", NULL);
   NSLog(@"%@",serialQueue1); // <OS_dispatch_queue_serial: com.baidu.searialQueue1>
   dispatch_queue_t serialQueue2 = dispatch_queue_create("com.baidu.searialQueue2", DISPATCH_QUEUE_SERIAL);
   NSLog(@"%@",serialQueue2); // <OS_dispatch_queue_serial: com.baidu.searialQueue2>
   
   // 创建: 并行队列
   dispatch_queue_t concurrentQueue = dispatch_queue_create("com.baidu.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
   NSLog(@"%@",concurrentQueue); // <OS_dispatch_queue_concurrent: com.baidu.concurrentQueue>
   
   // 获取: 全局并发队列
   dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
   NSLog(@"%@", globalConcurrentQueue); // <OS_dispatch_queue_global: com.apple.root.default-qos>

示例一
以下代码在主线程调用,结果是什么?

    NSLog(@"之前 - %@", [NSThread currentThread]);
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"sync - %@", [NSThread currentThread]);
    });
    NSLog(@"之后 - %@", [NSThread currentThread]);

答案
只会打印出一句 之前 - <NSThread: 0x60000138c800>{number = 1, name = main} ,然后主线程就卡死了,你可以在界面上放一个按钮,你就会发现点不了了。
解释:
同步任务会阻塞当前线程,然后把 Block 中的任务放到指定的队列中运行,只有等 Block 中的任务完成后才会让当前线程继续运行下去。
那么这里的步骤就是:打印完第一句话之后, dispatch_sync 立即会阻塞当前的主线程,然后把 Block 中的任务放到 main_queue 中,可是 main_queue 中的任务会被取出来放到主线程中执行,但是主线程这个时候已经被阻塞了,所以 Block 中的任务就不能完成,它不完成, dispatch_sync 就会一直阻塞主线程,这就是死锁现象。导致主线程一直卡死。

示例二
以下代码在主线程调用,结果是什么?

 NSLog(@"之前 - %@", [NSThread currentThread]);
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"sync sleepForTimeInterval - %@", [NSThread currentThread]);
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"sync - %@", [NSThread currentThread]);
    });
    NSLog(@"之后 - %@", [NSThread currentThread]);

答案
先打印 之前 - <NSThread: 0x600001109240>{number = 1, name = main}
接着打印 sync sleepForTimeInterval - <NSThread: 0x60000276a0c0>{number = 1, name = main}
接着过两秒打印 sync - <NSThread: 0x60000276a0c0>{number = 1, name = main}
最后打印 之后 - <NSThread: 0x60000276a0c0>{number = 1, name = main}
解释:
同步任务会阻塞当前线程,等待 Block 中的任务放到指定队列中执行,只有等到 Block 中的任务完成才会让当前线程继续往下运行。
这时候的 Block 被加入到了全局并发队列,全局并发队列不需要等待其他任务的完成,所以不会造成相互等待阻塞的情况。所以会直接运行 Block 里面的函数。

示例三
以下代码打印的结果是什么?

   dispatch_queue_t queue = dispatch_queue_create("MyQueue", DISPATCH_QUEUE_SERIAL);
   NSLog(@"之前 - %@", [NSThread currentThread]);
   dispatch_async(queue, ^{
       NSLog(@"async之前 - %@", [NSThread currentThread]);
       dispatch_sync(queue, ^{
           NSLog(@"sync - %@", [NSThread currentThread]);
       });
       NSLog(@"async之后 - %@", [NSThread currentThread]);
   });
   NSLog(@"之后 - %@", [NSThread currentThread]);  

答案

2019-10-17 17:17:23.580434+0800 EOCTestBlock[37032:5987893] 之前 - <NSThread: 0x60000040ca80>{number = 1, name = main}
2019-10-17 17:17:23.580602+0800 EOCTestBlock[37032:5987893] 之后 - <NSThread: 0x60000040ca80>{number = 1, name = main}
2019-10-17 17:17:23.580641+0800 EOCTestBlock[37032:5988071] async之前 - <NSThread: 0x60000045a2c0>{number = 7, name = (null)}

解释:
后面不打印的原因参考示例一,相同的原理。

队列组
队列组可以将很多队列添加到一个组里,这样做的好处是,当这个组所有的任务都执行完了,队列组会通过一个方法通知我们.下面是使用方法,这是一个很实用的功能.

    //1.创建队列组
    dispatch_group_t group = dispatch_group_create();
    //2.获取队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //3.多次使用队列组的方法执行任务,只有异步方法.
    //3.1.执行 3 次循环
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 3; i++) {
            NSLog(@"group-01 - %@", [NSThread currentThread]);
        }
    });
    
    //3.2.主队列执行 8 次
    dispatch_group_async(group, dispatch_get_main_queue(), ^{
        for (int i = 0; i < 8; i++) {
            NSLog(@"group-02 - %@", [NSThread currentThread]);
        }
    });
    
    //3.3.执行 5 次循环
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"group-03 - %@", [NSThread currentThread]);
        }
    });
    
    //4.都完成后会自动通知
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"完成 - %@", [NSThread currentThread]);
    });

结果:

2019-10-17 17:36:58.284466+0800 EOCTestBlock[37258:6010751] group-01 - <NSThread: 0x6000030c8900>{number = 6, name = (null)}
2019-10-17 17:36:58.284486+0800 EOCTestBlock[37258:6010746] group-03 - <NSThread: 0x6000030a0b40>{number = 7, name = (null)}
2019-10-17 17:36:58.284604+0800 EOCTestBlock[37258:6010751] group-01 - <NSThread: 0x6000030c8900>{number = 6, name = (null)}
2019-10-17 17:36:58.284620+0800 EOCTestBlock[37258:6010746] group-03 - <NSThread: 0x6000030a0b40>{number = 7, name = (null)}
2019-10-17 17:36:58.284714+0800 EOCTestBlock[37258:6010751] group-01 - <NSThread: 0x6000030c8900>{number = 6, name = (null)}
2019-10-17 17:36:58.284760+0800 EOCTestBlock[37258:6010746] group-03 - <NSThread: 0x6000030a0b40>{number = 7, name = (null)}
2019-10-17 17:36:58.284880+0800 EOCTestBlock[37258:6010746] group-03 - <NSThread: 0x6000030a0b40>{number = 7, name = (null)}
2019-10-17 17:36:58.284989+0800 EOCTestBlock[37258:6010746] group-03 - <NSThread: 0x6000030a0b40>{number = 7, name = (null)}
2019-10-17 17:36:58.305175+0800 EOCTestBlock[37258:6010586] group-02 - <NSThread: 0x6000030c21c0>{number = 1, name = main}
2019-10-17 17:36:58.305338+0800 EOCTestBlock[37258:6010586] group-02 - <NSThread: 0x6000030c21c0>{number = 1, name = main}
2019-10-17 17:36:58.305453+0800 EOCTestBlock[37258:6010586] group-02 - <NSThread: 0x6000030c21c0>{number = 1, name = main}
2019-10-17 17:36:58.305604+0800 EOCTestBlock[37258:6010586] group-02 - <NSThread: 0x6000030c21c0>{number = 1, name = main}
2019-10-17 17:36:58.305721+0800 EOCTestBlock[37258:6010586] group-02 - <NSThread: 0x6000030c21c0>{number = 1, name = main}
2019-10-17 17:36:58.305916+0800 EOCTestBlock[37258:6010586] group-02 - <NSThread: 0x6000030c21c0>{number = 1, name = main}
2019-10-17 17:36:58.306032+0800 EOCTestBlock[37258:6010586] group-02 - <NSThread: 0x6000030c21c0>{number = 1, name = main}
2019-10-17 17:36:58.306149+0800 EOCTestBlock[37258:6010586] group-02 - <NSThread: 0x6000030c21c0>{number = 1, name = main}
2019-10-17 17:36:58.307594+0800 EOCTestBlock[37258:6010586] 完成 - <NSThread: 0x6000030c21c0>{number = 1, name = main}

栅栏 Barrier
栅栏函数(dispatch_barrier_async):会阻塞自定义创建的并发队列,使得排在它前面的任务都执行完了,才执行后面的任务。特别强调:1. 队列必须是通过dispatch_queue_create自定义创建的并发队列。2.阻塞的是 queue 并不是线程。
栅栏函数(dispatch_barrier_sync):这个方法的使用和上一个一样,传入 自定义的并发队列(DISPATCH_QUEUE_CONCURRENT),它和上一个方法一样的阻塞 queue,不同的是 这个方法还会阻塞当前线程

示例代码

    // 创建一个自定义并行队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.baidu.current", DISPATCH_QUEUE_CONCURRENT); // 必须是自定义的并发队列
    // dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 非自定义的将无效果
    
    // 并行操作
    void (^blk1)(void) = ^(void) {
        NSLog(@"1");
    };
    
    void (^blk2)(void) = ^(void) {
        NSLog(@"2");
    };
    
    void (^blk3)(void) = ^(void) {
        NSLog(@"3");
    };
    
    void (^blk4)(void) = ^(void) {
        NSLog(@"4");
    };
    
    void (^blk5)(void) = ^(void) {
        NSLog(@"5");
    };
    
    void (^blk6)(void) = ^(void) {
        NSLog(@"6");
    };
    
    // 栅栏函数执行操作
    void (^barrierBlk)(void) = ^(void) {
        NSLog(@"Barrier!");
    };
    
    // 执行所有操作
    dispatch_async(concurrentQueue, blk1);
    dispatch_async(concurrentQueue, blk2);
    dispatch_async(concurrentQueue, blk3);
    
    dispatch_barrier_async(concurrentQueue, barrierBlk);
    
    dispatch_async(concurrentQueue, blk4);
    dispatch_async(concurrentQueue, blk5);
    dispatch_async(concurrentQueue, blk6);
    
NSOperation 和 NSOperationQueue

NSOperation 是苹果公司对 GCD 的封装,完全面向对象,所以使用起来更好理解。大家可以看到 NSOperationNSOperationQueue 分别对应 GCD 的 任务队列。操作步骤也很好理解:
1.将要执行的任务封装到 NSOperation 对象中。
2.将此任务添加到一个 NSOperationQueue 对象中。

   //1.获取主队列(NSOperationQueue 里面只有主队列和其他队列两种队列名称)
//    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
   
   //2.获取其他队列(因为只有两种队列,其他队列就不需要名字了)
   NSOperationQueue *otherQueue = [[NSOperationQueue alloc] init];
   

   //3.创建一个Operation(任务)
   NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
       NSLog(@"%@",[NSThread currentThread]);
   }];
   
   //1.1.添加多个任务 Block
   for (int i = 0; i < 5; i++) {
       [operation addExecutionBlock:^{
           [NSThread sleepForTimeInterval:1.5];
           NSLog(@"第%i次: %@", i, [NSThread currentThread]);
       }];
   }
   
   //4.如果没有串行和并行概念,怎么控制队列执行呢?通过配置(不懂为什么,无效丫,xcode11.1 上)
//    mainQueue.maxConcurrentOperationCount = 1;
   otherQueue.maxConcurrentOperationCount = 1;
   
   
   //5.添加 Operation 到 Queue 就会自动执行
//    [mainQueue addOperation:operation];
   [otherQueue addOperation:operation];

以上代码,实际运行效果好些和我理解的不一样,关于 NSOPeration 后续再更新吧,说到底还是 GCD 更加灵活好用一些。

其他用法

在这部分,我会说一些和多线程知识相关的案例,可能有些很简单,大家早都知道,不过因为这篇文章讲的是多线程嘛,所以应该尽可能的全面。还有就是,我会尽可能的使用多种方法实现,让大家看看其中的区别。

线程同步

所谓线程同步就是繁殖多个线程抢夺一个资源造成的数据安全问题,所采取的一种措施。当然也有很多方法实现,请往下看:

  • 互斥锁:给需要同步的代码块加一个互斥锁,就可以保证每次只有一个线程访问此代码块。
// EOCAPI.m
- (NSString *)synchronizedData {
    _synchronizedData = @"empty";
    @synchronized (self) {
        // 模拟耗时操作
        [NSThread sleepForTimeInterval:2.0];
        _flag ++;
        _synchronizedData = [NSString stringWithFormat:@"data : %i",_flag];
    }
    return _synchronizedData;
}

// 函数调用,思考下面的输出情况。
    EOCAPI *api = [[EOCAPI alloc] init];
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_async(queue, ^{
        NSLog(@"enter here 1");
        NSLog(@"%@",api.synchronizedData);
    });
    dispatch_async(queue, ^{
        NSLog(@"enter here 2");
        NSLog(@"%@",api.synchronizedData);
    });
    dispatch_async(queue, ^{
        NSLog(@"enter here 3");
        NSLog(@"%@",api.synchronizedData);
    });

输出结果:

2019-10-18 13:56:50.269079+0800 EOCTestBlock[44733:6358343] enter here 1
2019-10-18 13:56:50.269130+0800 EOCTestBlock[44733:6358344] enter here 2
2019-10-18 13:56:50.269141+0800 EOCTestBlock[44733:6358346] enter here 3
2019-10-18 13:56:52.272210+0800 EOCTestBlock[44733:6358343] data
2019-10-18 13:56:54.276316+0800 EOCTestBlock[44733:6358346] data
2019-10-18 13:56:56.281184+0800 EOCTestBlock[44733:6358344] data
  • 同步执行:我们可以使用多线程的知识,把多个线程都要执行次段代码添加到同一个串行队列,这样就实现了线程同步的概念。
    __block int index = 0;
    dispatch_queue_t queue = dispatch_queue_create("com.carrot.concurrent", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        NSLog(@"enter here 1");
        [NSThread sleepForTimeInterval:1.0];
        index ++;
        NSLog(@"%i - %@", index,[NSThread currentThread]);
    });
    
    dispatch_sync(queue, ^{
        NSLog(@"enter here 2");
        [NSThread sleepForTimeInterval:1.0];
        index ++;
        NSLog(@"%i - %@", index,[NSThread currentThread]);
    });
    
    dispatch_sync(queue, ^{
       NSLog(@"enter here 2");
       [NSThread sleepForTimeInterval:1.0];
       index ++;
       NSLog(@"%i - %@", index,[NSThread currentThread]);
   });

结果:

2019-10-18 14:21:20.219193+0800 EOCTestBlock[45070:6388419] enter here 1
2019-10-18 14:21:21.219513+0800 EOCTestBlock[45070:6388419] 1 - <NSThread: 0x600002805e40>{number = 1, name = main}
2019-10-18 14:21:21.219859+0800 EOCTestBlock[45070:6388419] enter here 2
2019-10-18 14:21:22.220427+0800 EOCTestBlock[45070:6388419] 2 - <NSThread: 0x600002805e40>{number = 1, name = main}
2019-10-18 14:21:22.220661+0800 EOCTestBlock[45070:6388419] enter here 2
2019-10-18 14:21:23.221115+0800 EOCTestBlock[45070:6388419] 3 - <NSThread: 0x600002805e40>{number = 1, name = main}
延迟执行

所谓延迟执行就是延时一段时间再执行某段代码。下面说一些常用方法。

  • perform
[self performSelector:@selector(run:) withObject:@"abc" afterDelay:3.0];
  • GCD
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",[NSThread currentThread]);
    });
  • NSTimer
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%@",[NSThread currentThread]);
    }];
单例模式

至于什么是单例模式,我也不多说,我只说说一般怎么实现。在 Objective-C 中,实现单例的方法已经很具体了,虽然有别的方法,但是一般都是用一个标准方法。

static id _instance;

+ (instancetype)sharedTool {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[Tool alloc] init];
    });

    return _instance;
}