操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。每个线程会被分配一段时间片,通常在 10-100 毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。
前面介绍的多线程方式GCD
和NSOperationQueue
基本都要配合锁一起使用,比如多线程对属性的访问或者同一时间调用一段代码等都会需要锁来保证安全,下面 iOS 八种锁的性能对比图,图片来源 ibireme 的不再安全的 OSSpinLock
下面介绍这几种锁的用法,OSSpinLock
自旋锁因为不再安全,而且 APP 代码基本不会用到,所以不介绍。
dispatch_semaphore
GCD
中的信号量,常用方法如下:
dispatch_semaphore_t dispatch_semaphore_create(long value);
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
create
方法创建一个新的信号量,其中参数value
为信号量的起始值, 不要传递小于零的值,否则返回 NULL
,设为 1 就可以当作锁来用。当不再需要该信号量时,调用dispatch_release
释放信号量
signal
:信号量计数加1.
wait
:信号量计数减1,如果信号量的值大于0,该方法所处线程就继续执行后面的代码,并且将信号量的值减1;否则,阻塞当前线程并等待timeout;如果在timeout 之前信号量的值被dispatch_semaphore_signal
方法加1,那么就继续执行后面的代码并将信号量的值减1。如果等到timeout,其所处线程自动执行其后代码。
使用代码:
_number = 1;
_signal = dispatch_semaphore_create(1);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
[self funOne];
});
dispatch_async(globalQueue, ^{
[self funOne];
});
- (void)funOne {
dispatch_semaphore_wait(_signal, DISPATCH_TIME_FOREVER);
NSLog(@"funOne before run %zi",_number);
[NSThread sleepForTimeInterval:0.5];
NSLog(@"funOne end run %zi",_number);
_number++;
dispatch_semaphore_signal(_signal);
}
执行结果:
2018-11-16 14:42:40.723877+0800 YMultiThreadDemo[1938:261566] funOne before run 1
2018-11-16 14:42:41.224161+0800 YMultiThreadDemo[1938:261566] funOne end run 1
2018-11-16 14:42:41.224385+0800 YMultiThreadDemo[1938:261565] funOne before run 2
2018-11-16 14:42:41.729619+0800 YMultiThreadDemo[1938:261565] funOne end run 2
这样就保证funOne
在多线程调用时,同一时间只在一个线程上执行,其他线程等待
signal
。为什么要保证一段代码同一时间只在一个线程上被执行,下面实验买票过程,一共50张票,定时器模拟多人同时刷屏的情况,代码如下:
_number = 0;
_tickets = 50;
dispatch_queue_t queue = dispatch_queue_create("com.yxw.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("com.yxw.queue2", DISPATCH_QUEUE_CONCURRENT);
__weak typeof(self) wself = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
dispatch_async(queue, ^{
__strong typeof(wself) strongSelf = wself;
[strongSelf buyTicket];
});
}];
_timer2 = [NSTimer scheduledTimerWithTimeInterval:0.15 repeats:YES block:^(NSTimer * _Nonnull timer) {
dispatch_async(queue2, ^{
__strong typeof(wself) strongSelf = wself;
[strongSelf buyTicket];
});
}];
- (void)buyTicket {
// dispatch_semaphore_wait(_signal, DISPATCH_TIME_FOREVER);
if (_tickets <= 0) {
return;
}
NSLog(@"start buy ticket ");
[NSThread sleepForTimeInterval:0.2];
NSLog(@"buy ticket %zi",_tickets);
_tickets --;
if (_tickets <= 0) {
NSLog(@"Tickets has been sold out");
if ([_timer isValid]) {
[_timer invalidate];
[_timer2 invalidate];
}
}
_number++;
NSLog(@"end buy ticket run %zi",_number);
// dispatch_semaphore_signal(_signal);
}
这里是没有加锁的代码,执行部分结果如下:
2018-11-16 16:02:59.620253+0800 YMultiThreadDemo[3207:346466] end buy ticket run 3
2018-11-16 16:02:59.669713+0800 YMultiThreadDemo[3207:346466] start buy ticket
2018-11-16 16:02:59.720036+0800 YMultiThreadDemo[3207:346464] start buy ticket
2018-11-16 16:02:59.720293+0800 YMultiThreadDemo[3207:346467] buy ticket 47
2018-11-16 16:02:59.720296+0800 YMultiThreadDemo[3207:346489] buy ticket 47
2018-11-16 16:02:59.720380+0800 YMultiThreadDemo[3207:346467] end buy ticket run 4
2018-11-16 16:02:59.720387+0800 YMultiThreadDemo[3207:346489] end buy ticket run 5
...
2018-11-16 16:03:02.619423+0800 YMultiThreadDemo[3207:346466] start buy ticket
2018-11-16 16:03:02.625722+0800 YMultiThreadDemo[3207:346464] buy ticket 1
2018-11-16 16:03:02.625892+0800 YMultiThreadDemo[3207:346464] Tickets has been sold out
2018-11-16 16:03:02.626038+0800 YMultiThreadDemo[3207:346464] end buy ticket run 51
2018-11-16 16:03:02.721501+0800 YMultiThreadDemo[3207:346489] buy ticket 0
2018-11-16 16:03:02.721501+0800 YMultiThreadDemo[3207:346467] buy ticket 0
2018-11-16 16:03:02.721702+0800 YMultiThreadDemo[3207:346489] Tickets has been sold out
2018-11-16 16:03:02.721702+0800 YMultiThreadDemo[3207:346467] Tickets has been sold out
2018-11-16 16:03:02.721787+0800 YMultiThreadDemo[3207:346489] end buy ticket run 52
2018-11-16 16:03:02.721822+0800 YMultiThreadDemo[3207:346467] end buy ticket run 53
2018-11-16 16:03:02.820595+0800 YMultiThreadDemo[3207:346466] buy ticket -2
2018-11-16 16:03:02.820792+0800 YMultiThreadDemo[3207:346466] Tickets has been sold out
2018-11-16 16:03:02.820892+0800 YMultiThreadDemo[3207:346466] end buy ticket run 54
会出现一张票卖出多次的情况,而且卖出的总票数多于50张,显然是不符合要求的,加上锁后(把信号量注释去掉)结果:
2018-11-16 16:09:35.791340+0800 YMultiThreadDemo[3275:352290] start buy ticket
2018-11-16 16:09:35.995282+0800 YMultiThreadDemo[3275:352290] buy ticket 50
2018-11-16 16:09:35.995452+0800 YMultiThreadDemo[3275:352290] end buy ticket run 1
2018-11-16 16:09:35.995600+0800 YMultiThreadDemo[3275:352636] start buy ticket
2018-11-16 16:09:36.198430+0800 YMultiThreadDemo[3275:352636] buy ticket 49
2018-11-16 16:09:36.198985+0800 YMultiThreadDemo[3275:352636] end buy ticket run 2
...
2018-11-16 16:09:45.761050+0800 YMultiThreadDemo[3275:352651] buy ticket 2
2018-11-16 16:09:45.761323+0800 YMultiThreadDemo[3275:352651] end buy ticket run 49
2018-11-16 16:09:45.761472+0800 YMultiThreadDemo[3275:352687] start buy ticket
2018-11-16 16:09:45.964220+0800 YMultiThreadDemo[3275:352687] buy ticket 1
2018-11-16 16:09:45.964416+0800 YMultiThreadDemo[3275:352687] Tickets has been sold out
2018-11-16 16:09:45.964561+0800 YMultiThreadDemo[3275:352687] end buy ticket run 50
这样就保证了不卖出相同的票以及多余的票
pthread_mutex
pthread_mutex
表示互斥锁,互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。
int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);
互斥锁的初始化方法,pthread_mutexattr_t
参数用来设置属性,用法如下:
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 定义锁的属性
其中settype
有四个类型:
-
PTHREAD_MUTEX_NORMAL
:默认值,普通锁,当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。如果线程在不解锁的情况下尝试重新锁定该互斥锁,则会产生死锁。尝试解除由其他线程锁定的互斥锁会产生不确定的行为。如果尝试解除锁定的互斥锁未锁定,则会产生不确定的行为。 -
PTHREAD_MUTEX_ERRORCHECK
:检错锁,此类型的互斥锁可提供错误检查,上面NORMAL
类型的三种情况都会返回错误,其他与普通锁类型动作相同。 -
PTHREAD_MUTEX_RECURSIVE
: 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock
解锁。 -
PTHREAD_MUTEX_DEFAULT
:iOS 下等同于PTHREAD_MUTEX_NORMAL
pthread_mutex_destroy ()
用于注销一个互斥锁,要求锁当前处于解锁状态。
用下面代码测试:
static pthread_mutex_t mutex;//静态全局变量
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 定义锁的属性
pthread_mutex_init(&mutex, &attr); // 创建锁
[self funPerform:5];
- (void)funPerform:(NSInteger)num {
NSInteger lockReturn = pthread_mutex_lock(&mutex);
NSLog(@"lock return: %zi",lockReturn);
if (num > 0) {
NSLog(@"fun perform %zi",num);
[self funPerform:num - 1];
}
pthread_mutex_unlock(&mutex);
}
上面代码执行正确,结果如下:
2018-11-19 16:09:15.806866+0800 YMultiThreadDemo[2562:316069] lock return: 0
2018-11-19 16:09:15.806946+0800 YMultiThreadDemo[2562:316069] fun perform 5
2018-11-19 16:09:15.814543+0800 YMultiThreadDemo[2562:316069] lock return: 0
2018-11-19 16:09:15.814908+0800 YMultiThreadDemo[2562:316069] fun perform 4
2018-11-19 16:09:15.815086+0800 YMultiThreadDemo[2562:316069] lock return: 0
2018-11-19 16:09:15.815173+0800 YMultiThreadDemo[2562:316069] fun perform 3
2018-11-19 16:09:15.815267+0800 YMultiThreadDemo[2562:316069] lock return: 0
2018-11-19 16:09:15.815350+0800 YMultiThreadDemo[2562:316069] fun perform 2
2018-11-19 16:09:15.815442+0800 YMultiThreadDemo[2562:316069] lock return: 0
2018-11-19 16:09:15.815528+0800 YMultiThreadDemo[2562:316069] fun perform 1
2018-11-19 16:09:15.815616+0800 YMultiThreadDemo[2562:316069] lock return: 0
如果锁类型改为PTHREAD_MUTEX_NORMAL
,则死锁(如果加锁方法改为pthread_mutex_trylock(&mutex);
,则返回16,表示EBUSY
);如果改为PTHREAD_MUTEX_ERRORCHECK
,能执行,但是pthread_mutex_lock
返回11,表示EDEADLK
,其定义为
#define EDEADLK 11 /* Resource deadlock avoided */
#define EBUSY 16 /* Device / Resource busy */
NSLock
NSLock
是以对象的形式暴露给开发者的一种锁,在内部封装了一个 pthread_mutex
,属性为 PTHREAD_MUTEX_ERRORCHECK
,它会损失一定性能换来错误提示。加解锁需要在同一线程, 从不同的线程加解锁可能导致未定义的行为。其属性和方法如下:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name;
@end
lock
、unlock
、tryLock
方法都与pthread_mutex
中的对应方法类似。
lockBeforeDate
:尝试在给定时间之前获取锁定并返回指示尝试是否成功的布尔值。
NSCondition
NSCondition
的底层是通过条件变量pthread_cond_t
来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程继续执行。NSCondition
属性和方法如下:
@interface NSCondition : NSObject {
@private
void *_priv;
}
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@property (nullable, copy) NSString *name;
@end
wait
:阻塞当前线程,直到condition
发出信号。必须在调用此方法之前加锁。
waitUntilDate
:相对于wait
,多加了个时间限制,如果到达时间但是condition
还尚未发出信号,则唤醒线程,继续执行。
signal
:发出状态信号,唤醒等待它的第一个线程。
broadcast
:广播信号,唤醒等待它的所有线程。如果没有线程在等待该条件,则此方法不执行任何操作。
使用情况:比如一个任务,要等另一个线程保存文件完成后执行,模拟代码如下:
__weak typeof(self) wself = self;
dispatch_queue_t queue3 = dispatch_queue_create("com.yxw.queue3", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue4 = dispatch_queue_create("com.yxw.queue4", DISPATCH_QUEUE_CONCURRENT);
NSCondition *condition = [[NSCondition alloc] init];
dispatch_async(queue3, ^{
[condition lock];
if (!wself.writeCompleted) {
[condition wait];
}
NSLog(@"write completed after");
[condition unlock];
});
dispatch_async(queue4, ^{
NSLog(@"write begin");
[NSThread sleepForTimeInterval:2.];
wself.writeCompleted = YES;
NSLog(@"write completed");
[condition signal];
});
执行结果如下:
2018-11-20 11:34:06.575478+0800 YMultiThreadDemo[1556:174183] write begin
2018-11-20 11:34:08.581873+0800 YMultiThreadDemo[1556:174183] write completed
2018-11-20 11:34:08.582089+0800 YMultiThreadDemo[1556:174182] write completed after
其中,if (!wself.writeCompleted)
语句可以改为while (!self.writeCompleted)
,因为[condition wait]
会阻塞当前线程,所以while
不是一直在运行,不会浪费 CPU 资源;与if
不同的是,为 if
时,其他线程只要收到signal
就直接执行后面的代码,不会再判断self.writeCompleted
的值,while
则会继续判断,条件不满足就继续wait
,看需求使用。
NSRecursiveLock
递归锁,也是通过 pthread_mutex_lock
函数来实现,内部封装的 pthread_mutex_t
对象的类型为PTHREAD_MUTEX_RECURSIVE
。使用方法跟前面pthread_mutex_t
的递归模式一样。
@interface NSRecursiveLock : NSObject {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name;
@end
NSConditionLock
条件锁,可以与特定的用户定义条件相关联的锁,借助 NSCondition
来实现。使用NSConditionLock
对象,可以确保线程只有在满足特定条件时才能获取锁。 一旦获得锁并执行代码的关键部分,线程就可以放弃锁并将相关条件设置为新的。 条件本身是任意的:可以根据应用需要定义它们。NSConditionLock
属性方法如下:
@interface NSConditionLock : NSObject {
@private
void *_priv;
}
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name;
@end
lockWhenCondition
:在锁定操作成功之前,接收器的条件必须等于传入的条件。 此方法阻止线程的执行,直到可以获取锁定。使用此方法加锁代码,由unlock
方法解锁无效。
unlockWithCondition
:解锁并把属性condition
修改为传入的值。与lockWhenCondition
配合使用。
使用情况,比如有三个任务的执行顺序需要三个条件来确定,模拟代码如下:
_conditionNum = 1;
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:1];
dispatch_async(queue3, ^{
[NSThread sleepForTimeInterval:0.5];
[conditionLock lockWhenCondition:1];
NSLog(@"condition lock task: %zi",wself.conditionNum);
[NSThread sleepForTimeInterval:1.];
wself.conditionNum ++;
[conditionLock unlockWithCondition:2];
});
dispatch_async(queue4, ^{
[conditionLock lockWhenCondition:2];
NSLog(@"condition lock task: %zi",wself.conditionNum);
[NSThread sleepForTimeInterval:2.];
wself.conditionNum ++;
[conditionLock unlockWithCondition:3];
});
dispatch_async(globalQueue, ^{
[conditionLock lockWhenCondition:3];
NSLog(@"condition lock task: %zi",wself.conditionNum);
[NSThread sleepForTimeInterval:2.];
wself.conditionNum ++;
[conditionLock unlockWithCondition:4];
});
执行结果如下:
2018-11-20 15:29:37.196991+0800 YMultiThreadDemo[3719:336744] condition lock task: 1
2018-11-20 15:29:38.202500+0800 YMultiThreadDemo[3719:336745] condition lock task: 2
2018-11-20 15:29:40.203211+0800 YMultiThreadDemo[3719:336746] condition lock task: 3
@synchronized
使用最简单,通过牺牲性能换来语法上的简洁与可读。实现上是把这个对象当做锁来使用。这是通过一个哈希表来实现的,OC 在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值来得到对应的互斥锁。
使用代码如下:
dispatch_async(globalQueue, ^{
@synchronized (wself) {
NSLog(@"synchronized run 1 begin");
[NSThread sleepForTimeInterval:2.];
NSLog(@"synchronized run 1 end");
}
});
dispatch_async(globalQueue, ^{
@synchronized (wself) {
NSLog(@"synchronized run 2");
}
});
执行结果如下:
2018-11-20 15:51:14.517625+0800 YMultiThreadDemo[3952:363020] synchronized run 1 begin
2018-11-20 15:51:16.521971+0800 YMultiThreadDemo[3952:363020] synchronized run 1 end
2018-11-20 15:51:16.522215+0800 YMultiThreadDemo[3952:363021] synchronized run 2
参考文章:
不再安全的 OSSpinLock
深入理解 iOS 开发中的锁
关于 @synchronized,这儿比你想知道的还要多