正文
抛砖引玉
说到锁不得不提线程安全,说到线程安全,作为iOS程序员又不得不提
nonatomic
与
atomic
-
nonatomic
不会对生成的
getter
、
setter
方法加同步锁(非原子性)
-
atomic
会对生成的
getter
、
setter
加同步锁(原子性)
setter
/
getter
被
atomic
修饰的属性时,该属性是读写安全的。然而读写安全并不代表线程安全。
线程安全概念(thread safety)
-
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
-
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
验证
atomic
非线程安全
#import "ViewController.h"
@interface ViewController ()
@property (strong, atomic) NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//atomic非线程安全验证
//Jack
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
self.name = @"Jack";
NSLog(@"Jack is %@", self.name);
}
});
//Rose
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
self.name = @"Rose";
NSLog(@"Rose is %@", self.name);
}
});
}
2017-11-29 11:21:27.713446+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.713487+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.713638+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.713659+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.713840+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.714050+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.714205+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.718069+0800 LockDemo[42637:1199499] Rose is Rose
2017-11-29 11:21:27.718069+0800 LockDemo[42637:1199500] Jack is Rose
2017-11-29 11:21:27.718199+0800 LockDemo[42637:1199500] Jack is Jack
2017-11-29 11:21:27.718199+0800 LockDemo[42637:1199499] Rose is Jack
最后一行和倒数第三行可以看到,
atomic
非线程安全验证完毕。
-
也就是说
atomic
只能做到读写安全并不能做到线程安全,若要实现线程安全还需要采用更为深层的锁定机制才行。
-
iOS开发时一般都会使用
nonatomic
属性,因为在iOS中使用同步锁的开销较大,这会带来性能问题,但是在Mac OS X程序时,使用
atomic
属性通常都不会有性能瓶颈。
锁的概念
在计算机科学中,锁是一种同步机制,用于在存在多线程的环境中实施对资源的访问限制。
锁的作用
-
通俗来讲:就是为了防止在多线程的情况下对共享资源的脏读或者脏写。
-
也可以理解为:执行多线程时用于强行限制资源访问的同步机制,即并发控制中保证互斥的要求。
iOS开发中常用的锁
-
@synchronized
-
NSLock
对象锁
-
NSRecursiveLock
递归锁
-
NSConditionLock
条件锁
-
pthread_mutex
互斥锁(C语言)
-
dispatch_semaphore
信号量实现加锁(
GCD
)
-
OSSpinLock
自旋锁
性能图来源:ibireme
@synchronized
@synchronized
其实是一个 OC 层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读性。
@synchronized
是我们平常使用最多的但是性能最差的。
OC写法:
@synchronized(self) {
//需要执行的代码块
}
swift写法:
objc_sync_enter(self)
//需要执行的代码块
objc_sync_exit(self)
代码示例:
//线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized(self) {
NSLog(@"第一个线程同步操作开始");
sleep(3);
NSLog(@"第一个线程同步操作结束");
}
});
//线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
@synchronized(self) {
NSLog(@"第二个线程同步操作");
}
});
结果:
2017-11-29 14:36:52.056457+0800 LockDemo[46145:1306472] 第一个线程同步操作开始
2017-11-29 14:36:55.056868+0800 LockDemo[46145:1306472] 第一个线程同步操作结束
2017-11-29 14:36:55.057261+0800 LockDemo[46145:1306473] 第二个线程同步操作
-
@synchronized(self)
指令使用的
self
为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的
self
改成其它对象,线程2就不会被阻塞。
NSString *s = [NSString string];
//线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized(self) {
NSLog(@"第一个线程同步操作开始");
sleep(3);
NSLog(@"第一个线程同步操作结束");
}
});
//线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
@synchronized(s) {
NSLog(@"第二个线程同步操作");
}
});
2017-11-29 14:43:54.930414+0800 LockDemo[46287:1312173] 第一个线程同步操作开始
2017-11-29 14:43:55.930761+0800 LockDemo[46287:1312158] 第二个线程同步操作
2017-11-29 14:43:57.932287+0800 LockDemo[46287:1312173] 第一个线程同步操作结束
-
@synchronized
指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,
@synchronized
块会隐式的添加一个异常处理来保护代码,该处理会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。
NSLock
-
NSLock
中实现了一个简单的互斥锁。通过
NSLocking
协议定义了
lock
和
unlock
方法。
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
举个栗子卖冰棍儿
- (void)nslockTest {
//设置冰棍儿的数量为5
_count = 5;
//创建锁
_lock = [[NSLock alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleIceCream];
});
//线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleIceCream];
});
}
- (void)saleIceCream
{
while (1) {
sleep(1);
//加锁
[_lock lock];
if (_count > 0) {
_count--;
NSLog(@"剩余冰棍儿数= %ld, Thread - %@", _count, [NSThread currentThread]);
} else {
NSLog(@"冰棍儿卖光光 Thread - %@",[NSThread currentThread]);
break;
}
//解锁
[_lock unlock];
}
}
加锁结果:
2017-11-29 16:21:29.728198+0800 LockDemo[55262:1411318] 剩余冰棍儿数= 4, Thread - <NSThread: 0x604000475dc0>{number = 3, name = (null)}
2017-11-29 16:21:29.728428+0800 LockDemo[55262:1411319] 剩余冰棍儿数= 3, Thread - <NSThread: 0x604000475e00>{number = 4, name = (null)}
2017-11-29 16:21:30.729009+0800 LockDemo[55262:1411318] 剩余冰棍儿数= 2, Thread - <NSThread: 0x604000475dc0>{number = 3, name = (null)}
2017-11-29 16:21:30.729378+0800 LockDemo[55262:1411319] 剩余冰棍儿数= 1, Thread - <NSThread: 0x604000475e00>{number = 4, name = (null)}
2017-11-29 16:21:31.733061+0800 LockDemo[55262:1411318] 剩余冰棍儿数= 0, Thread - <NSThread: 0x604000475dc0>{number = 3, name = (null)}
2017-11-29 16:21:31.733454+0800 LockDemo[55262:1411319] 冰棍儿卖光光 Thread - <NSThread: 0x604000475e00>{number = 4, name = (null)}
不加锁结果:
2017-11-29 16:23:38.702352+0800 LockDemo[55316:1412917] 剩余冰棍儿数= 3, Thread - <NSThread: 0x604000270b80>{number = 3, name = (null)}
2017-11-29 16:23:38.702352+0800 LockDemo[55316:1412919] 剩余冰棍儿数= 4, Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
2017-11-29 16:23:39.705096+0800 LockDemo[55316:1412919] 剩余冰棍儿数= 2, Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
2017-11-29 16:23:39.705099+0800 LockDemo[55316:1412917] 剩余冰棍儿数= 1, Thread - <NSThread: 0x604000270b80>{number = 3, name = (null)}
2017-11-29 16:23:40.709617+0800 LockDemo[55316:1412919] 剩余冰棍儿数= 0, Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
2017-11-29 16:23:40.709617+0800 LockDemo[55316:1412917] 冰棍儿卖光光 Thread - <NSThread: 0x604000270b80>{number = 3, name = (null)}
2017-11-29 16:23:41.714002+0800 LockDemo[55316:1412919] 冰棍儿卖光光 Thread - <NSThread: 0x604000271040>{number = 4, name = (null)}
-
NSLock
类还增加了
tryLock
和
lockBeforeDate:
方法
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
tryLock
试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程,相反,它只是返回NO。
lockBeforeDate:
方法试图获取一个锁,但是如果锁没有在规定的时间内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回NO)。
NSRecursiveLock 递归锁
有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁。
- (void)recursiveLockTest {
//创建锁
_lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void(^TestMethod)(int);
TestMethod = ^(int value)
{
[_lock lock];
if (value > 0)
{
[NSThread sleepForTimeInterval:1];
value--;
TestMethod(value);
}
[_lock unlock];
};
TestMethod(5);
NSLog(@"结束");
});
}
我们发现 "结束" 永远不会被打印出来,这个时候可以使用递归锁来解决。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。
- (void)recursiveLockTest {
//创建锁
_recursiveLock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void(^TestMethod)(int);
TestMethod = ^(int value)
{
[_recursiveLock lock];
if (value > 0)
{
[NSThread sleepForTimeInterval:1];
value--;
TestMethod(value);
}
[_recursiveLock unlock];
};
TestMethod(5);
NSLog(@"结束");
});
}
此时 "结束" 5秒后会被打印出来。
NSConditionLock 条件锁
NSCoditionLock
做多线程之间的任务等待调用,而且是线程安全的。
- (void)conditionLockTest {
NSInteger HAS_DATA = 1;
NSInteger NO_DATA = 0;
_conditionLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
NSMutableArray *products = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
[_conditionLock lockWhenCondition:NO_DATA];
[products addObject:[[NSObject alloc] init]];
NSLog(@"生产");
[_conditionLock unlockWithCondition:HAS_DATA];
sleep(5);
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (1) {
NSLog(@"等待");
[_conditionLock lockWhenCondition:HAS_DATA];
[products removeObjectAtIndex:0];
NSLog(@"售卖");
[_conditionLock unlockWithCondition:NO_DATA];
}
});
}
NSConditionLock
也跟其它的锁一样,是需要
lock
与
unlock
对应的,只是
lock
,
lockWhenCondition:
与
unlock
,
unlockWithCondition:
是可以随意组合的,当然这是与需求相关的。
POSIX(pthread_mutex)
-
C语言定义下多线程加锁方式。 pthread_mutex 和
dispatch_semaphore_t
很像,但是完全不同。pthread_mutex 是Unix/Linux平台上提供的一套条件互斥锁的API。
-
新建一个简单的 pthread_mutex 互斥锁,引入头文件
#import <pthread.h>
声明并初始化一个
pthread_mutex_t
的结构。使用
pthread_mutex_lock
和
pthread_mutex_unlock
函数。调用
pthread_mutex_destroy
来释放该锁的数据结构。
使用:
#import <pthread.h>
- (void)pthreadTest {
__block pthread_mutex_t theLock;
pthread_mutex_init(&theLock, NULL);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
pthread_mutex_lock(&theLock);
NSLog(@"第一个线程同步操作开始");
sleep(3);
NSLog(@"第一个线程同步操作结束");
pthread_mutex_unlock(&theLock);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(1);
pthread_mutex_lock(&theLock);
NSLog(@"第二个线程同步操作");
pthread_mutex_unlock(&theLock);
});
}
执行结果:
2017-11-29 17:51:11.901064+0800 LockDemo[56729:1466788] 第一个线程同步操作开始
2017-11-29 17:51:14.904834+0800 LockDemo[56729:1466788] 第一个线程同步操作结束
2017-11-29 17:51:14.905195+0800 LockDemo[56729:1466789] 第二个线程同步操作
-
pthread_mutex 还可以创建条件锁,提供了和
NSCondition
一样的条件控制,初始化互斥锁同时使用
pthread_cond_init
来初始化条件数据结构
// 初始化
int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
// 等待(会阻塞)
int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
// 定时等待
int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
// 唤醒
int pthread_cond_signal (pthread_cond_t *cond);
// 广播唤醒
int pthread_cond_broadcast (pthread_cond_t *cond);
// 销毁
int pthread_cond_destroy (pthread_cond_t *cond);
pthread_mutex 还提供了很多函数,有一套完整的API,包含
Pthreads
线程的创建控制等等,非常底层,可以手动处理线程的各个状态的转换即管理生命周期,甚至可以实现一套自己的多线程,感兴趣的可以继续深入了解。