一,前言
线程安全是iOS开发中避免了的话题,随着多线程的使用,对于资源的竞争以及数据的操作都可能存在风险,所以有必要在操作时保证线程安全。
二,为什么要使用锁?
由于一个进程中不可避免的存在多线程,所以不可避免的存在多个线程访问同一个数据的情况。但是为了数据的安全性,当一个线程访问数据的时候,其它的线程不能对其访问。简单来讲就是在同一时刻,对同一个数据操作的线程只有一个。只有确保了这样,才能使数据不会被其他线程影响。而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。例如,一个内存单元存储着一个可读,可写的变量数据10,我们想取到10时,另外一个线程把它改成11,就会造成我们取到的数据,并不是我们想要的。再比如,写文件和读文件,当一个线程在写文件的时候,理论上来说,如果这个时候另一个线程来直接读取的话,那么得到的结果可能是你无法预料的。
示例:我们定义一个person类,创建一个NSInterge age的属性,开辟两个线程去改变age的值。
- (void)withoutLock { __block Person *p = [[Person alloc]init]; [NSThread detachNewThreadWithBlock:^{ //开辟一个新线程 for (int i = 0; i<1000; i++) { p.age ++; } NSLog(@"%zd\n",p.age); }]; [NSThread detachNewThreadWithBlock:^{ //开辟一个新线程 for (int i = 0; i<1000; i++) { p.age ++; } NSLog(@"%zd\n",p.age); }]; }
打印结果:
2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339611] 1893 2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339610] 1012
分析结果:
按正常的理想情况,打印的结果应该为1000,2000; 造成这个问题的主要原因就是我们开辟的两个线程都去访问age的内存单元,造成数据混乱。
假如我们加上锁以后:
- (void)useLock { __block Person *p = [[Person alloc]init]; NSLock *myLock = [[NSLock alloc]init]; NSLog(@"begin:"); [NSThread detachNewThreadWithBlock:^{ for (int i = 0; i<1000; i++) { [myLock lock]; //加锁 p.age ++; [myLock unlock]; //解锁 } NSLog(@"%zd\n",p.age); }]; [NSThread detachNewThreadWithBlock:^{ for (int i = 0; i<1000; i++) { [myLock lock]; //加锁 p.age ++; [myLock unlock]; //解锁 } NSLog(@"%zd\n",p.age); }]; }
打印结果:
2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339611] 1000 2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339610] 2000
三,怎么保证线程安全
通常我们使用锁的机制来保证线程安全,即确保同一时刻只有同一个线程来对同一个数据源进行访问。
四,常用的锁有哪些
- NSLock
- Synchronized 同步锁
- Atomic 自旋锁
- Recursivelock 递归锁
- Dispatch_semaphore 信号量
- NSConditionLock和NSCondition 条件锁
五,常用锁的使用
- NSLock
* 系统API:
@protocol NSLocking lock 方法 - (void)lock //获得锁 unlock 方法 - (void)unlock //释放锁
@interface NSLock : NSObject <NSLocking> { @private void *_priv; } - (BOOL)tryLock; //试图得到一个锁。YES:成功得到锁;NO:没有得到锁。 - (BOOL)lockBeforeDate:(NSDate *)limit; //在指定的时间以前得到锁。YES:在指定时间之前获得了锁;NO:在指定时间之前没有获得锁。该线程将被阻塞,直到获得了锁,或者指定时间过期。
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); //给锁定义一个Name @end
* NSLock的执行原理:
某个线程A调用lock方法。这样,NSLock将被上锁。可以执行“关键部分”,完成后,调用unlock方法。如果,在线程A 调用unlock方法之前,另一个线程B调用了同一锁对象的lock方法。那么,线程B只有等待。直到线程A调用了unlock。
* 使用方法
//初始化数据锁(主线程中) NSLock *lock =[NSLock alloc]init]; //数据加锁 [lock lock];
//加锁的内容
[object doSomeThine];
//数据解锁 [lock Unlock];
* 使用示例
//主线程中 NSLock *lock = [[NSLock alloc] init]; //线程1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [lock lock]; NSLog(@"线程1"); sleep(2); [lock unlock]; NSLog(@"线程1解锁成功"); }); //线程2 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(1);//以保证让线程2的代码后执行 [lock lock]; NSLog(@"线程2"); [lock unlock]; });
结果:
2018-12-02 14:23:09.659 ThreadLockControlDemo[1754:129663] 线程1 2018-12-02 14:23:11.663 ThreadLockControlDemo[1754:129663] 线程1解锁成功 2018-12-02 14:23:11.665 ThreadLockControlDemo[1754:129659] 线程2
* 注意事项
Warning
* NSLock类使用POSIX(可移植性操作系统接口)线程来实现上锁的特性。当NSLock类收到一个解锁的消息,你必须确定发送源也是来自那个发送上锁的线程。在不同的线程上解锁,会产生不定义行为。
* 你不应该把这个类实现递归锁。如果在同一个线程上调用两次lock方法,将会对这个线程永久上锁。使用NSRecursiveLock类来才可以实现递归锁。
* 解锁一个没有被锁定的锁是一个程序错误,这个地方需要注意。
- Synchronized 同步锁
同步锁是比较常用的,因为其使用方法是所有锁中最简单的,但性能却是最差的,所以对性能要求不高的使用场景Synchronized是一种比较方便的锁。
* 使用示例:
static Config * instance = nil; //方法A +(Config *) Instance { @synchronized(self) { if(nil == instance) { [self new]; } } return instance; } //方法B +(id)allocWithZone:(NSZone *)zone { @synchronized(self) { if(instance == nil){ instance = [super allocWithZone:zone]; return instance; } } return nil; }
* 使用介绍:
@synchronized,代表这个方法加锁, 相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程例如B正在用这个方法,有的话要等正在使用synchronized方法的线程B运行完这个方法后再运行此线程A,没有的话,直接运行。它包括两种用法:synchronized 方法和 synchronized 块。
@synchronized 方法控制对类(一般在IOS中用在单例中)的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法锁方能执行,否则所属就会发生线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类,至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突(只要所有可能访问类的方法均被声明为 synchronized)。
synchronized 块:
@通过 synchronized关键字来声明synchronized 块。语法如下:
@synchronized(syncObject) {
}
synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
* 使用总结:
- 从上可以看出不需要创建锁,一种类似于swift中调用一个含有尾随闭包的函数,就能实现功能。
- synchronized内部实现是对传入的对象,为其分配一个递归锁,存储在哈希表中。
* 使用注意:
@synchronized(){} 小括号里面需要传入一个对象类型,基本数据类型不能作为参数;
@synchronized(){}小括号内的这个对象不能为空,如果为nil,就不能保证其锁的功能。
- Atomic 自旋锁
自旋锁在iOS系统中的实现是OSSpinLock。自旋锁通过一直处于while盲等状态,来实现只有一个线程访问数据。由于一直处于while循环,所以对CPU的占用也比较高的,用CPU的消耗换来的好处就是自旋锁的性能高。
* 使用介绍:
当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(busy-waiting),当上一个线程的任务执行完毕,下一个线程会立即执行。
* 优缺点:
1. 由于自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁
2. 自旋锁会一直占用CPU,也可能会造成死锁
3.自旋锁有bug!不同优先级线程调度算法会有优先级反转问题,比如低优先级获锁访问资源,高优先级尝试访问时会等待,这时低优先级又没法争过高优先级导致任务无法完成lock释放不了
* 原子操作
-
nonatomic
:非原子属性,非线程安全,适合小内存移动设备 -
atomic
:原子属性,default,线程安全(内部使用自旋锁),消耗大量资源 -
单写多读,只为setter方法加锁,不影响getter
-
相关代码如下:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { if (offset == 0) { object_setClass(self, newValue); return; } id oldValue; id *slot = (id*) ((char*)self + offset); if (copy) { newValue = [newValue copyWithZone:nil]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:nil]; } else { if (*slot == newValue) return; newValue = objc_retain(newValue); } if (!atomic) { oldValue = *slot; *slot = newValue; } else { spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } objc_release(oldValue); } void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) { bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY); bool mutableCopy = (shouldCopy == MUTABLE_COPY); reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy); }
-
总结:很容易理解的代码,可变拷贝和不可变拷贝会开辟新的空间,两者皆不是则持有(引用计数+1),相比
nonatomic
只是多了一步锁操作。
* 使用示例
#import "ViewController.h" #import "Person.h" #import <libkern/OSAtomic.h> @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self useLock]; } - (void)withoutLock { __block Person *p = [[Person alloc]init]; [NSThread detachNewThreadWithBlock:^{ for (int i = 0; i<1000; i++) { p.age ++; } NSLog(@"%zd\n",p.age); }]; [NSThread detachNewThreadWithBlock:^{ @synchronized(self){ } for (int i = 0; i<1000; i++) { p.age ++; } NSLog(@"%zd\n",p.age); }]; } - (void)useLock { __block OSSpinLock spinLock = OS_SPINLOCK_INIT; //创建锁 __block Person *p = [[Person alloc]init]; NSLog(@"begin:"); [NSThread detachNewThreadWithBlock:^{ for (int i = 0; i<1000; i++) { OSSpinLockLock(&spinLock); //加锁 p.age ++; OSSpinLockLock(&spinLock); //解锁 } NSLog(@"%zd\n",p.age); }]; [NSThread detachNewThreadWithBlock:^{ for (int i = 0; i<1000; i++) { OSSpinLockLock(&spinLock); //加锁 p.age ++; OSSpinLockLock(&spinLock); //解锁 } NSLog(@"%zd\n",p.age); }]; }
* 使用总结:
1)首先需要#important<libkern/OSAtomic.h> ,因此关于自旋锁的API是在这个文件中声明的。
2)创建自旋锁也是通过一个静态宏,在线程内通过 OSSpinLockLock 和 OSSpinLockUnlock来上锁,解锁。如果不是因为现在的OSSpinLock出现了使用bug,在性能以及使用方面来说,都是很好的使用锁的选择。
* 自旋锁的原理
就是while循环来占用CPU,实际上,当A线程获取到锁时,CPU会处于while死循环,而这个死循环并不是A线程造成的,当A获取到锁,并且B线程也要申请锁时,就会一直while循环询问A线程是否释放了该锁,所以导致了CPU死循环,因此是B线程导致的,这个是“自旋”的由来,正是因为这个一直等待询问,并不类似于互斥锁,互斥锁在申请时处于线程休眠状态,所以才使自旋锁的性能高。举个列子:煮饭吃,你的电饭锅(A线程)正在煮饭(资源),而你本人(B线程)也想煮饭,你有两种方式,第一种,一直在电饭锅前等待着,看着饭好了没;第二种,去忙其它的,每15分钟过来看一次饭好了没。很显然,按照第一种方式肯定是会先吃上饭。
- Recursivelock 递归锁
* 需求场景:
一个锁只是请求一份资源,而在一些开发实际中,往往需要在代码中嵌套锁的使用,也就是在同一个线程中,一个锁还没有解锁就再次加锁。这个时候就用到了递归锁。
* 实现原理:
递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型。NSRecursiveLock 与 NSLock 的区别在于内部封装的 pthread_mutex_t 对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE
* 运用场景:
循环(多张图片循环上传),递归
* 使用示例:
示例一:
//递归锁实例化 NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; static void (^RecursiveMethod)(NSInteger); // 同一线程可多次加锁,不会造成死锁 RecursiveMethod = ^(NSInteger value){ [lock lock];//一进来就要开始加锁 [NetWorkManager requestWithMethod:POST Url:url Parameters:paraDic success:^(id responseObject) { [self reuestForSuccess]; //一旦数据获取成功就要解锁 不然会造成死锁 [lock unlock]; } requestRrror:^(id requestRrror) { //条件没有达到,开始循环操作 if(value > 0){ RecursiveMethod(value-1);//必须-1 循环 } if(value == 0){ //条件 如果 == 0 代表循环的次数条件已经达到 可以做别的操作 } //失败后也要解锁 [lock unlock]; }]; //记得解锁 [lock unlock]; }; //设置递归锁循环次数 自定义 RecursiveMethod(5);
示例二:
- (void)recursiveLock { NSRecursiveLock *theLock = [[NSRecursiveLock alloc]init]; [self MyRecursiveFucntion:5 recursiveLock:theLock]; }
- (void) MyRecursiveFucntion:(NSInteger )value recursiveLock:(NSRecursiveLock *)theLock { [theLock lock]; if (value !=0) { --value; [self MyRecursiveFucntion:value recursiveLock:theLock]; } [theLock unlock]; }
- Dispatch_semaphore 信号量
dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是
dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。
下面我们逐一介绍三个函数:
(1)dispatch_semaphore_create的声明为:
dispatch_semaphore_t dispatch_semaphore_create(long value);
传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。
值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。
(关于信号量,我就不在这里累述了,网上很多介绍这个的。我们这里主要讲一下dispatch_semaphore这三个函数的用法)。
(2)dispatch_semaphore_signal的声明为:
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
这个函数会使传入的信号量dsema的值加1;(至于返回值,待会儿再讲)
(3) dispatch_semaphore_wait的声明为:
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
这个函数会使传入的信号量dsema的值减1;
这个函数的作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;
如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,
不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,
且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。
如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。
(4)dispatch_semaphore_signal的返回值为long类型,当返回值为0时表示当前并没有线程等待其处理的信号量,其处理
的信号量的值加1即可。当返回值不为0时,表示其当前有(一个或多个)线程等待其处理的信号量,并且该函数唤醒了一
个等待的线程(当线程有优先级时,唤醒优先级最高的线程;否则随机唤醒)。
dispatch_semaphore_wait的返回值也为long型。当其返回0时表示在timeout之前,该函数所处的线程被成功唤醒。
当其返回不为0时,表示timeout发生。
(5)在设置timeout时,比较有用的两个宏:DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER。
DISPATCH_TIME_NOW 表示当前;
DISPATCH_TIME_FOREVER 表示遥远的未来;
一般可以直接设置timeout为这两个宏其中的一个,或者自己创建一个dispatch_time_t类型的变量。
创建dispatch_time_t类型的变量有两种方法,dispatch_time和dispatch_walltime。
利用创建dispatch_time创建dispatch_time_t类型变量的时候一般也会用到这两个变量。
dispatch_time的声明如下:
dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
其参数when需传入一个dispatch_time_t类型的变量,和一个delta值。表示when加delta时间就是timeout的时间。
例如:dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW, 1*1000*1000*1000);
表示当前时间向后延时一秒为timeout的时间。
(6)关于信号量,一般可以用停车来比喻。
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait函数就相当于来了一辆车,dispatch_semaphore_signal
就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),
调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;
当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主
没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,
所以就一直等下去。
(7)代码举简单示例如下:
|
dispatch_semaphore_t signal; signal = dispatch_semaphore_create(1);
__block long x = 0;
NSLog (@ "0_x:%ld" ,x);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
NSLog (@ "waiting" );
x = dispatch_semaphore_signal(signal);
NSLog (@ "1_x:%ld" ,x);
sleep(2);
NSLog (@ "waking" );
x = dispatch_semaphore_signal(signal);
NSLog (@ "2_x:%ld" ,x);
});
// dispatch_time_t duration = dispatch_time(DISPATCH_TIME_NOW, 1*1000*1000*1000); //超时1秒 // dispatch_semaphore_wait(signal, duration); x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
NSLog (@ "3_x:%ld" ,x);
x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
NSLog (@ "wait 2" );
NSLog (@ "4_x:%ld" ,x);
x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
NSLog (@ "wait 3" );
NSLog (@ "5_x:%ld" ,x);
|
最终打印的结果为:
|
2018-12-02 22:51:54.734 LHTest[15700:70b] 0_x:0 2018-12-02 22:51:54.737 LHTest[15700:70b] 3_x:0 2018-12-02 22:51:55.738 LHTest[15700:f03] waiting 2018-12-02 22:51:55.739 LHTest[15700:70b] wait 2 2018-12-02 22:51:55.739 LHTest[15700:f03] 1_x:1 2018-12-02 22:51:55.739 LHTest[15700:70b] 4_x:0 2018-12-02 22:51:57.741 LHTest[15700:f03] waking 2018-12-02 22:51:57.742 LHTest[15700:f03] 2_x:1 2018-12-02 22:51:57.742 LHTest[15700:70b] wait 3 2018-12-02 22:51:57.742 LHTest[15700:70b] 5_x:0 |
- NSConditionLock和NSCondition 条件锁
* 使用介绍:
NSConditionLock好处是可以设置条件,条件符合时获得锁。设置时间,指定时间之前获取锁。缺点是加锁和解锁需要在同一线程中执行,否则控制台会报错,虽然不影响程序运行。(but好像会影响进程释放,因为多次执行后进程到了80多,程序卡了还是崩溃了,忘了。只是猜测。)
* 使用举例:
NSConditionLock * conditionLock = [[NSConditionLockalloc] init]; //当条件符合时获得锁 [conditionLock lockWhenCondition:1]; //在指定时间前尝试获取锁,若成功则返回YES 否则返回NO BOOL isLock = [conditionLock lockBeforeDate:date1]; //在指定时间前尝试获取锁,且条件必须符合 BOOL isLock = [conditionLock lockWhenCondition:1 beforeDate:date1]; //解锁并设置条件为2 [conditionLock unlockWithCondition:2];