iOS多线程起底
一、三个人物:进程、线程、任务、
1.1 进程(process)
指一个正在运行中的可执行文件,每个进程包含独立的内存空间、系统资源以及端口权限,且至少包含一个主线程和任意数量的辅助线程。另外,当一个进程的主线程退出,该线程也退出。
1.2 线程(thread)
一个独立的代码的执行路径
1.3 任务(task)
我们需要执行的工作,一种抽象概念,就是一段执行代码
1.4 总结
参考了阮大神的文章,打个比方,cpu好比一个工厂,而进程就是工厂里的一个个车间,线程就是车间里的工人,他们共享车间里的空间、设备。
这图实在太形象了,出自阮大神
二、两个操作:互斥锁、信号量
2.1 互斥锁(mutex)
接着上面,每个厂房的大小不同,有的只能容纳一人,比如厕所🚾,里面有人的时候,其他人就进不去了。这代表一个线程使用某些内存时,其他线程必须等他结束,才能使用这块内存。
这时,在厕所门口加一把锁🔐,先到的人上锁🔐,后面的人看到上锁,就在门口排队,等锁打开再进来。互斥锁,就是防止多个线程同时使用同一块内存。
2.2 信号量(semaphore)
继续接着上面,有的车间能容纳n个人,比如厨房,当人数大于n人,外面的人只能在外面等着。怎么办😰?这时,就在原来一把锁🔐基础上挂n把钥匙。进去的人就取一把钥匙,出来时候再把钥匙挂上面。后面的人发现钥匙空了,就只能等着咯。这种就叫信号量
2.3 两者之间联系
image.png2.4 iOS的几种常见锁
- @synchronized
- NSLock 对象锁
- NSRecursiveLock 递归锁
- NSConditionLock 条件锁
- pthread_mutex 互斥锁(C语言)
- dispatch_semaphore 信号量实现加锁(GCD)
- OSSpinLock (暂不建议使用,原因参见这里)
如果不加锁,会出现如下
- (void)viewDidLoad {
[super viewDidLoad];
_concurrentQueue = dispatch_queue_create("com.will.queue(concurrent)", DISPATCH_QUEUE_CONCURRENT);
_tickets = 5;
_lock = [[NSLock alloc] init];
}
- (IBAction)onclickRun:(id)sender {
//售票窗口1
dispatch_async(_concurrentQueue, ^{
[self sellTickets];
});
//售票窗口2
dispatch_async(_concurrentQueue, ^{
[self sellTickets];
});
}
- (void)sellTickets{
while (1) {
[NSThread sleepForTimeInterval:1];
if (_tickets > 0) {
_tickets --;
NSLog(@"还剩%ld张票,Thread:%@", (long)_tickets, [NSThread currentThread]);
}else{
NSLog(@"票卖完了,Thread:%@", [NSThread currentThread]);
break;
}
}
}
打印如下,发生错乱
image.png
2.4.1 使出@synchronized
将sellTickets
方法做如下改变
- (void)sellTickets{
while (1) {
@synchronized(self){
[NSThread sleepForTimeInterval:1];
if (_tickets > 0) {
_tickets --;
NSLog(@"还剩%ld张票,Thread:%@", (long)_tickets, [NSThread currentThread]);
}else{
NSLog(@"票卖完了,Thread:%@", [NSThread currentThread]);
break;
}
}
}
}
2.4.2 使出NSLock
while (1) {
[_lock lock];
[NSThread sleepForTimeInterval:1];
if (_tickets > 0) {
_tickets --;
NSLog(@"还剩%ld张票,Thread:%@", (long)_tickets, [NSThread currentThread]);
}else{
NSLog(@"票卖完了,Thread:%@", [NSThread currentThread]);
break;
}
[_lock unlock];
}
三、队列
3.1 队列vs进程vs线程
image.png3.2 串行队列、并行队列、同步、异步
从线程的时效性,分为同步(synchronization)和异步(asynchronization)
从线程的执行,分为串行(serial)和并行(concurrency)
3.2.1 串行队列
队列里的线程是一个个执行,直到结束
3.2.2 并行队列
队列里的线程是同时结束,所有线程执行完,该队列结束
3.2.3 同步
就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,程序也不会接着往下执行
3.2.4 异步
当一个异步功能调用发出后,调用者不能立刻得到结果,可以继续干其他事情
3.2.5 之间联系
image.png同步 | 异步 | |
---|---|---|
串行队列 | image.png | image.png |
并行队列 | image.png | image.png |
3.3 全局队列、主队列
名称 | 定义 | 注意事项 |
---|---|---|
全局队列 | 属于并行队列 | 不要与barrier使用,barrier只能与自定义队列使用,可在全局队列里用异步方法执行耗时操作 |
主队列 | 属于串行队列,在主线程运行 | 不能与同步方法搭配使用,会阻塞主线程,造成死锁 |
3.3.1 全局队列里搭配异步
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 10; i ++) {
dispatch_async(q, ^{
NSLog(@"%@ %d", [NSThread currentThread], i);
});
}
打印如下
2017-11-14 17:12:08.641708+0800 tableView[14688:316052] <NSThread: 0x600000266d40>{number = 3, name = (null)} 2
2017-11-14 17:12:08.641736+0800 tableView[14688:316055] <NSThread: 0x600000267000>{number = 6, name = (null)} 0
2017-11-14 17:12:08.641743+0800 tableView[14688:316054] <NSThread: 0x600000266d80>{number = 5, name = (null)} 1
2017-11-14 17:12:08.641763+0800 tableView[14688:316053] <NSThread: 0x60400047e080>{number = 4, name = (null)} 3
2017-11-14 17:12:08.641769+0800 tableView[14688:316079] <NSThread: 0x60400047cb00>{number = 7, name = (null)} 4
2017-11-14 17:12:09.593578+0800 tableView[14688:316052] <NSThread: 0x600000266d40>{number = 3, name = (null)} 6
2017-11-14 17:12:09.593623+0800 tableView[14688:316053] <NSThread: 0x60400047e080>{number = 4, name = (null)} 8
2017-11-14 17:12:09.593647+0800 tableView[14688:316054] <NSThread: 0x600000266d80>{number = 5, name = (null)} 7
2017-11-14 17:12:09.593667+0800 tableView[14688:316055] <NSThread: 0x600000267000>{number = 6, name = (null)} 5
2017-11-14 17:12:09.593687+0800 tableView[14688:316079] <NSThread: 0x60400047cb00>{number = 7, name = (null)} 9
可见,全局队列同时进行多个操作
image.png
3.3.2 全局队列里搭配同步
dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 10; i ++) {
dispatch_sync(q, ^{
NSLog(@"%@ %d", [NSThread currentThread], i);
});
}
打印如下
2017-11-14 17:13:39.821569+0800 tableView[14740:317904] <NSThread: 0x600000067e80>{number = 1, name = main} 0
2017-11-14 17:13:39.821867+0800 tableView[14740:317904] <NSThread: 0x600000067e80>{number = 1, name = main} 1
2017-11-14 17:13:39.822292+0800 tableView[14740:317904] <NSThread: 0x600000067e80>{number = 1, name = main} 2
2017-11-14 17:13:39.822417+0800 tableView[14740:317904] <NSThread: 0x600000067e80>{number = 1, name = main} 3
2017-11-14 17:13:39.822524+0800 tableView[14740:317904] <NSThread: 0x600000067e80>{number = 1, name = main} 4
2017-11-14 17:13:39.822689+0800 tableView[14740:317904] <NSThread: 0x600000067e80>{number = 1, name = main} 5
2017-11-14 17:13:39.822805+0800 tableView[14740:317904] <NSThread: 0x600000067e80>{number = 1, name = main} 6
2017-11-14 17:13:39.822910+0800 tableView[14740:317904] <NSThread: 0x600000067e80>{number = 1, name = main} 7
2017-11-14 17:13:39.823008+0800 tableView[14740:317904] <NSThread: 0x600000067e80>{number = 1, name = main} 8
2017-11-14 17:13:39.823109+0800 tableView[14740:317904] <NSThread: 0x600000067e80>{number = 1, name = main} 9
所以操作都是在全局队列下进行的
image.png
可见是一个一个执行的
3.3.3 主队列里搭配异步
dispatch_queue_t q = dispatch_get_main_queue();
for (int i = 0; i < 10; i ++) {
dispatch_async(q, ^{
NSLog(@"%@ %d", [NSThread currentThread], i);
});
}
因为主队列是串行队列,即使是异步的,也是一个一个完成
image.png
打印如下
2017-11-14 17:26:49.806347+0800 tableView[15142:330532] <NSThread: 0x60400007ff00>{number = 1, name = main} 0
2017-11-14 17:26:52.051380+0800 tableView[15142:330532] <NSThread: 0x60400007ff00>{number = 1, name = main} 1
2017-11-14 17:26:53.822800+0800 tableView[15142:330532] <NSThread: 0x60400007ff00>{number = 1, name = main} 2
2017-11-14 17:26:55.782335+0800 tableView[15142:330532] <NSThread: 0x60400007ff00>{number = 1, name = main} 3
2017-11-14 17:26:56.851570+0800 tableView[15142:330532] <NSThread: 0x60400007ff00>{number = 1, name = main} 4
2017-11-14 17:27:11.047209+0800 tableView[15142:330532] <NSThread: 0x60400007ff00>{number = 1, name = main} 5
2017-11-14 17:27:19.167697+0800 tableView[15142:330532] <NSThread: 0x60400007ff00>{number = 1, name = main} 6
2017-11-14 17:27:21.188779+0800 tableView[15142:330532] <NSThread: 0x60400007ff00>{number = 1, name = main} 7
2017-11-14 17:27:22.871865+0800 tableView[15142:330532] <NSThread: 0x60400007ff00>{number = 1, name = main} 8
2017-11-14 17:27:24.287835+0800 tableView[15142:330532] <NSThread: 0x60400007ff00>{number = 1, name = main} 9
3.3.4 主队列里搭配同步(会崩溃)
- (IBAction)onclickRun:(id)sender {
dispatch_queue_t q = dispatch_get_main_queue();
for (int i = 0; i < 10; i ++) {
dispatch_sync(q, ^{
NSLog(@"%@ %d", [NSThread currentThread], i);
});
}
}
同步任务需要马上执行,但主队列(串行队列)正在执行onclickRun:
,所以需要等待onclickRun:
执行完成。而onclickRun:
需要等待主队列的同步任务执行完成,相互等待造成主线程阻塞,造成死锁。
如果我们这里自定义一个串行队列,则不会造成死锁,因为onclickRun:
只需等待自定义的串行队列完成即可
- (IBAction)onclickRun:(id)sender {
dispatch_queue_t q = dispatch_queue_create("com.will.image.decode", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 10; i ++) {
dispatch_sync(q, ^{
NSLog(@"%@ %d", [NSThread currentThread], i);
});
}
}
image.png
四、生命周期
1499394752139363.png 1499394732413995.png由此可见:
- NSThread创建的线程需要手动控制线程的销毁
[NSThread exit];
- GCD和NSOperation都是在线程里任务执行完毕,自动销毁,需要注意,线程的任务多大量创建了临时变量,需要用
@autoreleasepool
来释放这些临时变量,比如:
dispatch_queue_t serialQueue = dispatch_queue_create("me.tutuge.test.autoreleasepool", DISPATCH_QUEUE_SERIAL);
//Run loop with autoReleasePool
dispatch_sync(serialQueue, ^{
for (int i = 0; i < kIterationCount; i++) {
@autoreleasepool {
NSNumber *num = [NSNumber numberWithInt:i];
NSString *str = [NSString stringWithFormat:@"%d ", i];
NSLog(@"%@", [NSString stringWithFormat:@"%@%@", num, str]);
}
}
});
//Run loop without autoReleasePool
dispatch_sync(serialQueue, ^{
for (int i = 0; i < kIterationCount; i++) {
NSNumber *num = [NSNumber numberWithInt:i];
NSString *str = [NSString stringWithFormat:@"%d ", i];
NSLog(@"%@", [NSString stringWithFormat:@"%@%@", num, str]);
}
});
使用了@autoreleasepool
减少了约20M内存