Objective-C 用代码揭示多线程
Pthreads
- 其实这个方案不用说的,只是拿来充数,为了让大家了解一下就好了。
- 简单地说,这是一套在很多操作系统上都通用的多线程 API,所以移植性很强(然并卵)。
- 它是需要 C 语言函数,这是比较蛋疼的,更蛋疼的是你需要手动处理线程的各个状态的转换即管理声明周期,比如创建线程,销毁线程。
NSThread
这套方案是经过苹果封装后的,并且完全面向对象的。所以你可以直接操控线程对象,非常直观和方便。但是,它的生命周期还是需要我们手动管理,所以这套方案也是偶尔用用,比如[NSThread currentThread],它可以获取当前线程,你就可以知道当前线程的各种属性,用于调试十分方便。
- 其实,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 的封装,完全面向对象,所以使用起来更好理解。大家可以看到 NSOperation 和 NSOperationQueue 分别对应 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;
}