必备知识架构-线程与网络-①锁

[toc]

知识架构

iOS知识库

Android知识库

四、线程安全问题

< 返回目录

1、线程安全

当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。就好比几个人在同一时修改同一个表格,造成数据的错乱。

参考:iOS中保证线程安全的几种方式与性能对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
性能对比
对以上各个锁进行1000000此的加锁解锁的空操作时间如下:
OSSpinLock: 46.15 ms
dispatch_semaphore: 56.50 ms
pthread_mutex: 178.28 ms
NSCondition: 193.38 ms
NSLock: 175.02 ms
pthread_mutex(recursive): 172.56 ms
NSRecursiveLock: 157.44 ms
NSConditionLock: 490.04 ms
@synchronized: 371.17 ms

总的来说:

OSSpinLock 自旋锁(性能最高的锁)和 dispatch_semaphore 信号量的效率远远高于其他。
@synchronized和NSConditionLock效率较差。

dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。
OSSpinLock 自旋锁,性能最高的锁。原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。 不过最近YY大神在自己的博客不再安全的 OSSpinLock中说明了OSSpinLock已经不再安全,请大家谨慎使用。


鉴于OSSpinLock的不安全,所以我们在开发中如果考虑性能的话,建议使用dispatch_semaphore。
如果不考虑性能,只是图个方便的话,那就使用@synchronized。

NSDateFormatter在iOS7之后(包括iOS7)才是线程安全的

2、死锁

死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局(Deadly-Embrace) ) ,若无外力作用,这些进程(线程)都将无法向前推进。

死锁的4个必要条件

一、@synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSObject *obj = [[NSObject alloc] init];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(obj) {
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
}
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
@synchronized(obj) {
NSLog(@"需要线程同步的操作2");
}
});

@synchronized(obj)指令使用的obj为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的@synchronized(obj)改为@synchronized(self),刚线程2就不会被阻塞,

二、NSLock

1
2
3
4
5
6
7
8
9
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock]; // 加锁
NSLog(@"需要线程同步的操作1 开始");
sleep(2);
NSLog(@"需要线程同步的操作1 结束");
[lock unlock]; // 解锁

});

三、NSRecursiveLock递归锁的使用

我们先写一个典型的死锁情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
NSLock *lock = [[NSLock alloc] init];	// 此锁在本代码中会造成死锁
//NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; //要想下面的递归调用不会造成死锁,只要这里将锁改成递归锁NSRecursiveLock就可以了。NSRecursiveLock递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

static void (^RecursiveMethod)(int);

RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1); // 此时lock还没解锁,就有执行这个block,而block里又给加了次锁,从而造成了死锁
}
[lock unlock];
};

RecursiveMethod(5);
});

在是跟你面的代码中,在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSLock *lock = [[NSLock alloc] init];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

static void (^RecursiveMethod)(int);

RecursiveMethod = ^(int value) {
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1);
}
};

[lock lock];
RecursiveMethod(5);
[lock unlock];
});

四、NSCondition & NSConditionLock 条件锁

wait方法是傻等;

waitUntilDate:方法是等一会;

signal是唤起一个在等待的线程。

通过wait()waitUntilDate(limit: NSDate) -> Bool这两个方法都可以实现线程阻塞即线程睡眠。

不同之处在于

wait()会使线程一直处于休眠状态,直到收到signal()为止;

waitUntilDate(limit: NSDate) -> Bool在使线程睡眠的同时会设置睡眠的终止时间。

如果在终止时间前收到了signal()就会唤醒线程;
当到达终止时间的时候,即使没有收到signal(),也会直接唤醒线程,而不会像wait()方法那样一直睡眠下去。

1、NSCondition

基本的条件锁,手动的控制。wait

2、NSConditionLock 条件锁

条件锁,这里的条件并不是bool表达式中的条件,而是一个特定的int值。

条件锁,不是简单的加锁/解锁, 而是需要根据一定条件满足后进行 加锁/解锁.

以一个生产中与消费者的例子,介绍条件锁的用法。

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
static NSInteger CONDITION_NO_DATA        //条件一: 没有数据
static NSInteger CONDITION_HAS_DATA //条件二: 有数据

// 初始化锁时,指定一个默认的条件
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:CONDITION_NO_DATA];

//生产者,加锁与解锁的过程
while (YES) {

//1. 当满足 【没有数据的条件时】进行加锁
[lock lockWhenCondition:CONDITION_NO_DATA];

//2. 生产者生成数据
//.....

//3. 解锁,并设置新的条件,已经有数据了
[locker unlockWithCondition:CONDITION_HAS_DATA];
}


//消费者,加锁与解锁的过程
while (YES) {

//1. 当满足 【有数据的条件时】进行加锁
[lock lockWhenCondition:CONDITION_HAS_DATA];

//2. 消费者消费数据
//.....

//3. 解锁,并设置新的条件,没有数据了
[locker unlockWithCondition:CONDITION_NO_DATA];
}

五、OSSpinLock自旋锁 & os_unfair_lock

由于OSSpinLock不再安全,所以这里我们就直接说一下os_unfair_lock,这个是苹果用于代替OSSpinLock的。效率很高用法很简单如下

1
2
3
4
5
os_unfair_lock_t lock = OS_UNFAIR_LOCK_INIT;

os_unfair_lock_lock(&lock);
NSLog(@"os_unfair_lock_t");
os_unfair_lock_unlock(&lock);
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
42
43
44
45
46
47
48
49
50
51
52
53
#import "ViewController.h"
#import <libkern/OSAtomic.h>
@interface ViewController ()
@property (nonatomic,assign) int ticket;
//@property (nonatomic,assign) OSSpinLock lock;
@end

@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// self.lock = OS_SPINLOCK_INIT;
self.ticket=50;
[self ticketsTest];
// Do any additional setup after loading the view.
}

-(void)ticketsTest{
// 这里我们开两个线程处理同一件事
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i =0; i<5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i =0; i<5; i++) {
[self saleTicket];
}
});
}

-(void)saleTicket{
//静态创建、则不需要新建属性
static OSSpinLock lock = OS_SPINLOCK_INIT;
//若后面是个函数、则需
/*
static OSSpinLock lock = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = OS_SPINLOCK_INIT;
});
*/
//其他线程执行到这里时,发现锁被加锁了 就会再这排队等待、直到这个锁被打开
//加锁
OSSpinLockLock(&lock);
int ticket = self.ticket;
sleep(.2);
ticket--;
self.ticket=ticket;
NSLog(@"%d",self.ticket);
//解锁
OSSpinLockUnlock(&lock);
}

iOS - 互斥锁&&自旋锁 多线程安全隐患

iOS - 互斥锁&&自旋锁 多线程安全隐患

一、多线程安全隐患

资源共享
一块资源可能会被多个线程共享,也就是多个线程可能会访问到一块资源
比如多个线程访问同一个对象,同一个变量,同一个文件。
当多线程访问同一块资源的时候,很容易引发数据错乱和数据安全问题
二、原子和非原子属性
1>OC 在定义属性的时候有nonatomic和atomic两种选择
* atomic:原子属性,为 setter 方法加锁
* nonatomic:非原子属性,不会为 setter 方法加锁
普通情况下都是在主线程做操作,所以一般都不会加锁。
对比:
* atomic:线程安全,需要消耗大量的资源
* nonatomic:非线程安全,适合内存小的移动设备
2>synchronized 与 atomic
* synchronized:互斥锁
* atomic:自旋锁
共同点:都能保证同一时刻只能有一个线程操作锁住的代码
区别
互斥锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会. 自动唤醒然后执行任务。
自旋锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
自旋锁应用场景:比较适合做一些不耗时的操作
三、互斥锁
· 注意点:
- 如果多线程访问同一个资源,那么必须使用同一把锁才能锁住
- 在开发中,尽量不要加锁,能在服务端做尽量在服务端做,如果必须要加锁,一定要记住,锁的范围不能太大,哪里有安全隐患就加在哪里。
技巧:因为必须使用同一把锁,开发中如果需要加锁,直接使用 self 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@synchronized(self) {
//线程1进来之后,锁住,2和3都再外面等待
//1、查询剩余的票数 NSUInteger count = self.totalCount;
//2、判断是否还有余票
//2.1卖票
//3 、提示客户,没有票了
if (count>0) {
[NSThread sleepForTimeInterval:0.1];
self.totalCount = count-1;
NSLog(@"%@卖了一张票,还剩%zd票",[NSThread currentThread].name,self.totalCount);
}
else
{
NSLog(@"没票了");
break;
}
}
//解锁

四、自旋锁
注意点:
只会给 setter 方法加锁,并不会给getter方法加锁。

ObjC 多线程简析(一)-多线程简述和线程锁的基本应用

在iOS10之后apple废弃了OSSpinLock自旋锁,使用os_unfair_lock互斥锁来替代。

在iOS10之后apple已经不再建议使用OSSpinLock自旋锁了,它的替代方案是一个互斥锁,所以一般情况下我们使用互斥锁来解决线程同步的问题才是比较合理的。

1
2
3
4
5
6
7
8
9
10
11
// 初始化OSSpinLock
_osspinlock = OS_SPINLOCK_INIT;

// 加锁
OSSpinLockLock(&_osspinlock);

// 操作数据
// ...

// 解锁
OSSpinLockUnlock(&_osspinlock);
1
2
3
4
5
6
7
8
9
10
11
// 初始化os_unfair_lock
_osunfairLock = OS_UNFAIR_LOCK_INIT;

// 加锁
os_unfair_lock_lock(&(_osunfairLock));

// 操作数据
// ...

// 解锁
os_unfair_lock_unlock(&(_osunfairLock));

iOS中的线程同步方案

自旋锁、互斥锁比较:

a, 什么情况使用自旋锁比较划算?

预计线程等待锁的时间很短

加锁的代码(临界区)经常被调用,但竞争情况很少发生

CPU资源不紧张

多核处理器

b,什么情况使用互斥锁比较划算?

预计线程等待锁的时间较长

单核处理器

临界区有IO操作

临界区代码复杂或者循环量大

临界区竞争非常激烈

(4)、自己延伸的问题:什么队列能同时存在同步任务和异步任务?(并发队列)

eg:主线程的主队列是串行队列,不可能在该队列上添加同步任务,否则会造成死锁。所以,同时存在同步任务和异步任务的队列应该只能是并发队列。

最终的执行顺序,还是看这些各式各样的任务所在的队列是什么队列,串行的还是并发的。

1、死锁探究:GCD死锁及报错提示(EXC_BAD_INSTRUCTION)

(1)、死锁举例:通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
- (IBAction)testDeadlock1:(id)sender {
//测试死锁
dispatch_queue_t q1 = dispatch_queue_create("serial_queue_1", DISPATCH_QUEUE_SERIAL);
dispatch_async(q1, ^{
NSLog(@"1(属任务①).验证通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁%@", [NSThread currentThread]);
dispatch_sync(q1, ^{ // 添加的同步任务和外面的任务是同一个串行队列
sleep(2);
NSLog(@"2(属任务②).验证通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁%@", [NSThread currentThread]);
});
NSLog(@"3(属任务①).验证通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁%@", [NSThread currentThread]);
});
}
(2)、死锁结论:往串行队列里添加的同步任务不能卡住该串行队列,否则会造成死锁(这句话非常重要)
(3)、死锁原因分析:

要了解通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁,或者说往串行队列里添加的同步任务卡住该串行队列的时候会发生死锁的原因,我们首先需要知道几个概念:

知识点①:创建同步任务的操作,在创建完之后,需要立马执行所创建的同步任务。即创建完同步任务的操作后,只有连其所创建的同步任务也结束了才能继续执行下去;

知识点②:而创建异步任务的操作,在创建完之后,即可继续执行下去,不需要等待所创建的异步任务结束。

附:在线程组的使用中,我们会遇到需要监听多个子线程中的所有任务都结束后,才去执行最后的更新UI的操作。假设最后更新UI的操作,除了需要子线程的任务结束,还需要子线程中的异步线程,如网络请求也结束后,才能更新UI,那么为了不让子线程提前退出,而是等到网络请求也结束后才退出,我们通常会通过使用gcd的enter leave或者信号量来等待。

知识点③:串行队列里的任务是一个执行完,才接着执行下一个;

所以,由以上①②③知识点,我们就能分析出其造成死锁的原因如下:

首先,对于标记123,先只看是同步任务还是异步任务,而不管是串行队列,还是并行队列,或者说是哪个队列。

从代码上我们很容易看出其是同步任务,所以由以上同步的知识点②,可以知道,最终标记输出的正确顺序应该依次是标记1、标记2、标记3

即我们的关注点是标记2能否在标记1之后输出。

答,在上述代码中,标记2处的同步任务是被添加到串行队列的,而且还是当前的串行队列。

我们知道串行队列里的任务是一个执行完,才接着执行下一个的,也就是说,往串行队列里添加的任务要执行的条件一定是在所添加的新任务之前的所有任务都已经全部执行完了后,才会执行到这一个的。

这里我们依次往串行队列里添加了第一个任务块和第二个任务块。

要完成第一个任务块需要同时完成任务1+第二个任务块2+任务3

第二个任务块要执行,根据串行队列的性质,我们知道第二个任务块要等待第一个任务块结束才会执行。(如果两个任务即使都是添加到串行队列,但是他们是不同串行队列的时候就不会需要等待)

由此死锁。

显然这里任务②需要等到任务①真正完成,而任务①的真正完成需要等任务②完成,这样的一个互相等待也就构成了一个死锁,导致我们 EXC_BAD_INSTRUCTION的崩溃了。

那么以上死锁的问题,怎么解决呢?

答:其实只要解决标记2即任务②,可以在标记1执行之后执行就可以了。
解决方法有:将任务②改成异步任务,或者将任务②这个同步任务添加到非本串行队列下,可以是其他串行队列,也可以是其他并行队列都可以。即以下这种修改方案是能够解决死锁的。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (IBAction)testDeadlock2:(id)sender {
//测试死锁
dispatch_queue_t q1 = dispatch_queue_create("serial_queue_1", DISPATCH_QUEUE_SERIAL);
dispatch_async(q1, ^{
​ NSLog(@"1(属任务①).验证通过串行队列里的任务,往其他串行队列里添加同步任务,不会造成死锁%@", [NSThread currentThread]);
​ dispatch_queue_t q2 = dispatch_queue_create("serial_queue_2", DISPATCH_QUEUE_SERIAL);
​ dispatch_sync(q2, ^{
​ sleep(2);
​ NSLog(@"2(属任务②).验证通过串行队列里的任务,往其他串行队列里添加同步任务,不会造成死锁%@", [NSThread currentThread]);
​ });
​ NSLog(@"3(属任务①).验证通过串行队列里的任务,往其他串行队列里添加同步任务,不会造成死锁%@", [NSThread currentThread]);
});
}

2、主队列中的死锁:在主队列开启同步任务,一定为什么会阻塞线程(看同步任务是不是加在主队列里去了)?

回答本问题前,我们需要先了解的知识点是:

知识点①:主线程和主队列的关系:

主队列是主线程中的一个串行队列。每一个应用程序只有唯一的一个主队列用来update UI。所有的和UI的操作(刷新或者点击按钮)都必须在主线程中的主队列中去执行,否则无法更新UI。

因为主线程是一个串行队列,所以往主队列里添加同步任务(如果不是往主队列添加同步任务就不会)是很有可能发生死锁卡死的。如以下代码就会发生死锁。代码如下:

1
2
3
4
5
6
7
8
9
10
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"打印1");

dispatch_sync(dispatch_get_main_queue(), ^{
​ NSLog(@"打印2");
});
NSLog(@"打印3");
}

由引述,我们已经知道往串行队列里添加的同步任务,如果卡住的是该串行队列,则会发生死锁,所以显然即执行往这个串行队列里添加同步任务的该任务也是在这个串行队列里的话,那么由于相互等待会造成死锁。

最简单的解决方法:将sync同步方法,替换成异步方法

1
2
3
4
5
6
7
8
9
10
11
12
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"标记1");

dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"标记3");
});

NSLog(@"标记5");
}
//输出结果为 标记1、标记5、标记3

其他修改方法:将同步任务卡住的队列改成并发队列。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)testDeadLock {
NSLog(@"标记1");
dispatch_queue_t other_queue = dispatch_queue_create("other_queue", DISPATCH_QUEUE_SERIAL);//不管是串行队列还是并发队列,都能解决这个死锁问题,因为它同步方法没有卡住这个other_queue。
dispatch_async(other_queue, ^{
​ NSLog(@"标记2");
​ dispatch_sync(dispatch_get_main_queue(), ^{
​ NSLog(@"标记3");
​ });
​ NSLog(@"标记4");
});
sleep(2);
NSLog(@"标记5");
}
//输出结果为 标记1、标记2、标记5、标记3、标记4。

有了这些基础,你再看以下文章中的例子时,就能轻松判断是否会造成死锁了 。

####

iOS中自旋锁与互斥锁的区别

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。(附:获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active活跃状态的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快)

其他参考文章:iOS中自旋锁与互斥锁的区别

pthread_mutex 表示互斥锁。互斥锁可以传入不同参数,实现递归锁pthread_mutex(recursive)。

NSLock,NSCondition,NSRecursiveLock,NSConditionLock都是内部封装的pthread_mutex,即都属于互斥锁。@synchronized是NSLock的一种封装,牺牲了效率,简洁了语法。

OSSpinLock 表示自旋锁,从上图可以看到自旋锁的效率最高,但是现在的iOS因为优先级反转的问题,已经不安全,所以推荐使用pthread_mutex或者dispatch_semaphore。

总结
  自旋锁会忙等: 所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。
  互斥锁会休眠: 所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁。此时会唤醒休眠线程。

优缺点
  自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,cpu时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
  缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。