一. 多线程开发中常见问题

多线程死锁(Deadlock):如果两个(有时候多个)线程都在等着对方完成任务才能执行自己的任务时,这种情况我们成为死锁。第一个线程要等到第二个线程执行完才能执行,而第二个线程则在等着第一个线程,所以两个都永远无法执行。

线程安全(Thread Safe):多个线程在执行任务过程中,因访问同一块资源,这个资源可以是同一个对象、同一个变量、同一个文件和同一个方法等,从而导致数据错误及数据不安全等问题。没有线程安全的代码同一时间内只能被在一个上下文中运行,线程安全的代码可以被多个线程或者并发任务安全地调用而不会引发任何问题。

上下文切换(Context Switch):一次上下文切换是指在一个单核处理器中,保存和恢复不同线程的执行状态的过程。在编写多任务应用的时候,这个切换是很常见的,但是这种切换也会带来额外的开销。

临界区(Critical Section):临界区值是不能被并发执行的代码块,也即不能被两个及以上的线程同时访问。这通常是因为这段代码访问了一份共享资源,而且这个资源在被访问的过程中不能中断。

竞争条件(Race Condition):当软件系统的行为依赖于无法控制的事件的执行顺序时(比如程序并发任务的执行顺序),我们称这种情况为竞争条件。竞争条件会产生无法预测的结果,通常代码走查也没法发现明显的问题。

二. 线程死锁分析

串行与并行针对的是队列,队列特性 FIFO 原则执行任务,串行队列一个个任务取出让任务一个接着一个地执行,并发队列也是FIFO 原则取出任务,但它取出来一个就会放到一个线程,然后再取出来一个又放到另一个的线程,取的动作快就感觉是一起并发执行的;

同步与异步针对的是线程,最大的区别在于,同步线程要阻塞当前线程,必须要等待同步线程中的任务执行完,返回以后,才能继续执行下一任务;而异步线程则是不用等待。

使用同步 sync 函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁);

死锁场景一(同步执行 + 串行队列):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 同步执行 + 串行队列 (主线程调用此方法)
- (void)syncAndSerialQueue {
dispatch_queue_t queue = dispatch_queue_create("com.zerocc.serialQueue",DISPATCH_QUEUE_SERIAL);

NSLog(@"1---%@",[NSThread currentThread]);

dispatch_async(queue,^{ // block块任务 A
NSLog(@"2---%@",[NSThread currentThread]);

dispatch_sync(queue,^{ // block块任务 B
NSLog(@"3---%@",[NSThread currentThread]);
});

NSLog(@"4---%@",[NSThread currentThread]);
});

NSLog(@"5---%@",[NSThread currentThread]);
}
1
2
3
4
5
// 代码执行结果 
... CCBlogCode[51579:8397296] 1---<NSThread: 0x6000020753c0>{number = 1, name = main}
... CCBlogCode[51579:8397296] 5---<NSThread: 0x6000020753c0>{number = 1, name = main}
... CCBlogCode[51579:8397379] 2---<NSThread: 0x6000020fd340>{number = 3, name = (null)}
(lldb)

分析:执行完到 2 后崩溃Thread 2: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

  • 1 和 5 是主线程中的任务,block块任务 A 是异步 async 不会阻塞当前线程主线程,所以 1 和 5 执行没问题;
  • 异步线程中,block块任务 A 加入到串行队列中。因为是异步执行所以 2 也能正常执行,且 5 和 2 执行顺序不定;
  • 2 执行完以后,遇到同步的 block块任务 B 也添加到同一队列中,队列的特点是遵循 FIFO 原则执行任务,其中 任务 A 是先加入到队列中,任务 B 是后加入的,也就是说 3 的执行要等待 2 和 4 都执行完才会从队列中取出执行,但是这里 4 的执行又要等待 3 执行完,因为 sync 是同步执行他会阻塞当前线程,然后就 3 和 4 的执行就互相等待了;

死锁场景二(同步执行 + 串行队列):

1
2
3
4
5
6
7
8
9
10
11
12
// 同步执行 + 主队列 (主线程调用此方法) 死锁
- (void)syncAndMainQueue{

NSLog(@"1");

dispatch_sync(dispatch_get_main_queue(),^{ // block块任务 A
NSLog(@"2");
});

NSLog(@"3");
// 死锁
}
1
2
3
// 执行结果
... CCBlogCode[60058:10806609] 1
(lldb)

分析:执行完 1 后崩溃 Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

  • syncAndMainQueue 方法主线程调用说明 1 和 3 是在主队列中执行,也就是说 syncAndMainQueue 方法执行的代码块是先添加在主队列的任务, 主队列有一定隐蔽性特殊;这里 1 打印没有异议;
  • block块任务 A 中代码 2 的执行也是添加在主队列,任务 A 是在 syncAndMainQueue 方法执行任务之后添加进主队列的,那么 2 的执行需等 syncAndMainQueue 方法执行完,也就是 1 和 3 执行完,所以 2 的执行需等待 3 执行完;
  • 但是 任务 A 又是通过 sync 函数同步执行方式添加入队列,那么他就会阻塞当前线程-主线程,需等 2 执行完后才能继续往下执行 3;这样 2 和 3 的执行就互相等待了;

三. 任务执行顺序分析

1. 子线程中调用 sync 函数并将任务添加入另一个串行队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 同步执行 + 另一个串行队列
- (void)syncAndOtherSerialQueue {
dispatch_queue_t queue = dispatch_queue_create("com.zerocc.serialQueue",DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.zerocc.serialQueue3",DISPATCH_QUEUE_SERIAL);

NSLog(@"1---%@",[NSThread currentThread]);

dispatch_async(queue,^{ // block块任务 A
NSLog(@"2---%@",[NSThread currentThread]);

// 同步执行阻塞当前线程, sync 不具备开启新线程,所以一定是先执行 3 再 4
dispatch_sync(queue2,^{ // block块任务 B
sleep(3);
NSLog(@"3---%@",[NSThread currentThread]);
});

NSLog(@"4---%@",[NSThread currentThread]);
});

NSLog(@"5---%@",[NSThread currentThread]);
// 执行结果 15234
}
1
2
3
4
5
6
// 执行打印结果
... CCBlogCode[61800:11440133] 1---<NSThread: 0x600001401400>{number = 1, name = main}
... CCBlogCode[61800:11440133] 5---<NSThread: 0x600001401400>{number = 1, name = main}
... CCBlogCode[61800:11440370] 2---<NSThread: 0x60000145b480>{number = 3, name = (null)}
... CCBlogCode[61800:11440370] 3---<NSThread: 0x60000145b480>{number = 3, name = (null)}
... CCBlogCode[61800:11440370] 4---<NSThread: 0x60000145b480>{number = 3, name = (null)}

执行结果分析:152执行顺序,5 和 2顺序不定;同步执行函数任务B添加到另一个串行队列,因为是不同队列不存在 3 等 4执行完再执行,但是 sync 函数是不具备开启新线程在当前线程同步执行那么就一定是先执行 3 再执行 4;如果这里将queue2 改为并发队列执行结果也一样,

2. 子线程中调用 async 函数并将任务添加入同一个串行队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 异步执行 + 串行队列 (主线程调用此方法) 同一队列 FIFO
- (void)asyncAndSerialQueue {

dispatch_queue_t queue = dispatch_queue_create("com.zerocc.serialQueue",DISPATCH_QUEUE_SERIAL);

NSLog(@"1---%@",[NSThread currentThread]);

dispatch_async(queue,^{ // block块任务 A
NSLog(@"2---%@",[NSThread currentThread]);

// 同一串行队列 FIFO,先执行完任务 A 中的代码再执行B,所以先4 后 3
dispatch_async(queue,^{ // block块任务 B
NSLog(@"3---%@",[NSThread currentThread]);
});

sleep(3);
NSLog(@"4---%@",[NSThread currentThread]);
});

NSLog(@"5---%@",[NSThread currentThread]);
// 执行结果 15243
}
1
2
3
4
5
6
// 代码执行结果
... CCBlogCode[53073:8457204] 1---<NSThread: 0x6000005dc2c0>{number = 1, name = main}
... CCBlogCode[53073:8457204] 5---<NSThread: 0x6000005dc2c0>{number = 1, name = main}
... CCBlogCode[53073:8457256] 2---<NSThread: 0x600000550b00>{number = 3, name = (null)}
... CCBlogCode[53073:8457256] 4---<NSThread: 0x600000550b00>{number = 3, name = (null)}
... CCBlogCode[53073:8457256] 3---<NSThread: 0x600000550b00>{number = 3, name = (null)}

执行结果分析:152执行顺序,5 和 2顺序不定;因为是同一个串行队列,那么他只能开启一个线程,尽管调用了 async 异步执行函数,任务 B 还是在当前线程执行,然后同一队列 3 是最后添加到队列中的所以3 等待 4执行完再执行。如果这个同一队列是并发队列,那就是并发执行了 3 4 谁快谁先执行,同一并发队列又是 async 会开启新线程, 虽然也FIFO,但是会将任务从并发队列中取出仍到多个线程中执行,所以 3 4就不定了。

四. 线程安全分析

一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件和同一个方法等。因此当多个线程访问同一块资源时,很容易会发生数据错误及数据不安全等问题。因此要避免这些问题,我们需要使用线程同步技术。先看示例,下篇再进行分析线程锁及其运用细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 存取钱场景示例
- (void)moneySaveAndDrawScene {

self.money = 100;

dispatch_queue_t queue = dispatch_queue_create("com.zerocc.money", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self moneySave];
}
});

dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self moneyDraw];
}
});
}

// 存钱 10 元
- (void)moneySave
{
int oldMoney = self.money;
sleep(1);
oldMoney += 10;
self.money = oldMoney;

NSLog(@"存10,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}

// 取钱 10 元
- (void)moneyDraw
{
int oldMoney = self.money;
sleep(1);
oldMoney -= 10;
self.money = oldMoney;

NSLog(@"取10,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
1
2
3
4
5
6
7
8
9
10
11
// 执行结果打印
... CCBlogCode[65160:11838714] 存10,还剩110元 - <NSThread: 0x60000180da80>{number = 4, name = (null)}
... CCBlogCode[65160:11838717] 取10,还剩90元 - <NSThread: 0x60000180db00>{number = 3, name = (null)}
... CCBlogCode[65160:11838717] 取10,还剩100元 - <NSThread: 0x60000180db00>{number = 3, name = (null)}
... CCBlogCode[65160:11838714] 存10,还剩120元 - <NSThread: 0x60000180da80>{number = 4, name = (null)}
... CCBlogCode[65160:11838717] 取10,还剩110元 - <NSThread: 0x60000180db00>{number = 3, name = (null)}
... CCBlogCode[65160:11838714] 存10,还剩130元 - <NSThread: 0x60000180da80>{number = 4, name = (null)}
... CCBlogCode[65160:11838714] 存10,还剩120元 - <NSThread: 0x60000180da80>{number = 4, name = (null)}
... CCBlogCode[65160:11838717] 取10,还剩100元 - <NSThread: 0x60000180db00>{number = 3, name = (null)}
... CCBlogCode[65160:11838717] 取10,还剩90元 - <NSThread: 0x60000180db00>{number = 3, name = (null)}
... CCBlogCode[65160:11838714] 存10,还剩110元 - <NSThread: 0x60000180da80>{number = 4, name = (null)}

示例分析:显然总共 100 元,存5次取5次 10元正确结果应该还是100,但是最后结果却是 110 元,显然数据出错。至于如何解决下篇文章继续。

多线程开发——线程安全

raywenderlich grand-central-dispatch

枫影 GCD 翻译文章