iOS开发中的11种锁整理
2018-08-09 本文已影响15人
百草纪
-
本文节选自成长手册
-
文章推荐和参考
多线程编程被普遍认为复杂,主要是因为多线程给程序引入了一定的不可预知性,要控制这些不可预知性,就需要使用各种锁各种同步机制,不同的情况就应该使用不同的锁不同的机制。
什么事情一旦放到多线程环境,要考虑的问题立刻就上升了好几个量级。多线程编程带来的好处不可胜数,然而工程师只要一不小心,就很容易让你的程序失去控制,所以你得用各种锁各种机制管住它。
要解决好这些问题,工程师们就要充分了解这些锁机制,分析不同的场景,选择合适的解决方案。
- 临界区:指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
- 自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
-
互斥锁(
Mutex
):是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。 - 读写锁:是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
-
信号量(
semaphore
):是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。 - 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。
- 死锁:指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去,这些永远在互相等待的进程称为死锁进程。
-
轮询(Polling):一种
CPU
决策如何提供周边设备服务的方式,又称“程控输出入”。轮询法的概念是,由CPU
定时发出询问,依序询问每一个周边设备是否需要其服务,有即给予服务,服务结束后再问下一个周边,接着不断周而复始。
//10000000
OSSpinLock: 112.38 ms
dispatch_semaphore: 160.37 ms
os_unfair_lock: 208.87 ms
pthread_mutex: 302.07 ms
NSCondition: 320.11 ms
NSLock: 331.80 ms
pthread_rwlock: 360.81 ms
pthread_mutex(recursive): 512.17 ms
NSRecursiveLock: 667.55 ms
NSConditionLock: 999.91 ms
@synchronized: 1654.92 ms
//1000
OSSpinLock: 0.02 ms
dispatch_semaphore: 0.03 ms
os_unfair_lock: 0.04 ms
pthread_mutex: 0.06 ms
NSLock: 0.06 ms
pthread_rwlock: 0.07 ms
NSCondition: 0.07 ms
pthread_mutex(recursive): 0.09 ms
NSRecursiveLock: 0.12 ms
NSConditionLock: 0.18 ms
@synchronized: 0.33 ms
- 互斥锁
NSLock
pthread_mutex
pthread_mutex(recursive)
递归锁@synchronized
- 自旋锁
OSSpinLock
os_unfair_lock
- 读写锁
pthread_rwlock
- 递归锁
NSRecursiveLock
-
pthread_mutex(recursive)
(见上)
- 条件锁
NSCondition
NSConditionLock
- 信号量
dispatch_semaphore
互斥锁
1、NSLock
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
-
NSLock
遵循NSLocking
协议,lock
方法是加锁,unlock
是解锁,tryLock
是尝试加锁,如果失败的话返回NO
,lockBeforeDate:
是在指定Date
之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO
。
// 主线程中
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];
sleep(1);
NSLog(@"线程1解锁成功");
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
NSLog(@"线程2");
[lock unlock];
});
// 打印:
// 线程1
// 线程2
// 线程1解锁成功
- 如果是三个线程,那么一个线程在加锁的时候,其余请求锁的线程将形成一个等待队列,按先进先出原则
- 注意:要求
NSLock
的lock
和unlock
需要在同一个Thread
下面
2、pthread_mutex
-
pthread_mutex
是C
语言下多线程加互斥锁的方式 - 被这个锁保护的临界区就只允许一个线程进入,其它线程如果没有获得锁权限,那就只能在外面等着。
// 用于静态的mutex的初始化,采用默认的attr。比如: static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
#define PTHREAD_MUTEX_INITIALIZER {_PTHREAD_MUTEX_SIG_init, {0}}
// 动态的初始化一个锁,__restrict 为互斥锁的类型,传 NULL 为默认类型,一共有 4 类型。
int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);
// 请求锁,如果当前mutex已经被锁,那么这个线程就会卡在这儿,直到mutex被释放
int pthread_mutex_lock(pthread_mutex_t *);
// 尝试请求锁,如果当前mutex已经被锁或者不可用,这个函数就直接return了,不会把线程卡住
int pthread_mutex_trylock(pthread_mutex_t *);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *);
// 把mutex锁干掉,并且释放所有它所占有的资源
int pthread_mutex_destroy(pthread_mutex_t *);
int pthread_mutex_setprioceiling(pthread_mutex_t * __restrict, int,
int * __restrict);
int pthread_mutex_getprioceiling(const pthread_mutex_t * __restrict,
int * __restrict);
- 锁类型
PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。
PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。
-
mutex
的初始化分两种,一种是用宏(PTHREAD_MUTEX_INITIALIZER
),一种是用函数(pthread_mutex_init
)。- 如果没有特殊的配置要求的话,使用宏比较好,因为它比较快。只有真的需要配置的时候,才需要用函数。
- 也就是说,凡是
pthread_mutex_init(&mutex, NULL)
的地方都可以使用PTHREAD_MUTEX_INITIALIZER
,因为在pthread_mutex_init
这个函数里的实现其实也是用了PTHREAD_MUTEX_INITIALIZER
:
///////////////////// pthread_src/include/pthread/pthread.h
#define PTHREAD_MUTEX_INITIALIZER __PTHREAD_MUTEX_INITIALIZER
///////////////////// pthread_src/sysdeps/generic/bits/mutex.h
// mutex锁本质上是一个spin lock,空转锁
# define __PTHREAD_MUTEX_INITIALIZER \
{ __PTHREAD_SPIN_LOCK_INITIALIZER, __PTHREAD_SPIN_LOCK_INITIALIZER, 0, 0, 0, 0, 0, 0 }
///////////////////// pthread_src/sysdeps/generic/pt-mutex-init.c
// 你看,这里其实用的也是宏。就这一句是初始化,下面都是在设置属性。
int
_pthread_mutex_init (pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr)
{
*mutex = (pthread_mutex_t) __PTHREAD_MUTEX_INITIALIZER;
if (! attr
|| memcmp (attr, &__pthread_default_mutexattr, sizeof (*attr) == 0))
/* The default attributes. */
return 0;
if (! mutex->attr
|| mutex->attr == __PTHREAD_ERRORCHECK_MUTEXATTR
|| mutex->attr == __PTHREAD_RECURSIVE_MUTEXATTR)
//pthread_mutex_destroy释放的就是这里的资源
mutex->attr = malloc (sizeof *attr);
if (! mutex->attr) return ENOMEM;
*mutex->attr = *attr;
return 0;
}
-
业界有另一种说法是:早年的
POSIX
只支持在static
变量上使用PTHREAD_MUTEX_INITIALIZER
,所以PTHREAD_MUTEX_INITIALIZER
尽量不要到处都用,所以使用的时候你得搞清楚你的pthread
的实现版本是不是比较老的。 -
使用
#import <pthread.h>
static pthread_mutex_t theLock;
- (void)example5 {
pthread_mutex_init(&theLock, NULL);
pthread_t thread;
pthread_create(&thread, NULL, threadMethord1, NULL);
pthread_t thread2;
pthread_create(&thread2, NULL, threadMethord2, NULL);
}
void *threadMethord1() {
pthread_mutex_lock(&theLock);
printf("线程1\n");
sleep(2);
pthread_mutex_unlock(&theLock);
printf("线程1解锁成功\n");
return 0;
}
void *threadMethord2() {
sleep(1);
pthread_mutex_lock(&theLock);
printf("线程2\n");
pthread_mutex_unlock(&theLock);
return 0;
}
// 打印:
// 线程1
// 线程1解锁成功
// 线程2
-
mutex
锁不是万能灵药- 基本上所有的问题都可以用互斥的方案去解决,大不了就是慢点儿,但不要不管什么情况都用互斥,都能采用这种方案不代表都适合采用这种方案。
- 而且这里所说的慢不是说
mutex
的实现方案比较慢,而是互斥方案影响的面比较大,本来不需要通过互斥就能让线程进入临界区,但用了互斥方案之后,就使这样的线程不得不等待互斥锁的释放,所以就慢了。 - 甚至有些场合用互斥就很蛋疼,比如多资源分配,线程步调通知等。 如果是读多写少的场合,就比较适合读写锁,如果临界区比较短,就适合空转锁,后面有介绍
-
预防死锁
- 1、如果要进入一段临界区需要多个
mutex
锁,那么就很容易导致死锁,单个mutex
锁是不会引发死锁的。- 要解决这个问题也很简单,只要申请锁的时候按照固定顺序,或者及时释放不需要的
mutex
锁就可以,尤其是全局mutex
锁的时候,更需要遵守一个约定。 - 我的
mutex
锁的命名规则就是:-
作用
_mutex_
序号,比如LinkListMutex_mutex_1
,OperationQueue_mutex_2
,后面的序号在每次有新锁的时候,就都加一个1
。如果有哪个临界区进入的时候需要获得多个mutex
锁的,我就按照序号的顺序去进行加锁操作,这样就能够保证不会出现死锁了。 - 如果是属于某个
struct
内部的mutex
锁,也一样,只不过序号可以不必跟全局锁挂钩,也可以从1
开始数。
-
作用
- 要解决这个问题也很简单,只要申请锁的时候按照固定顺序,或者及时释放不需要的
- 2、还有另一种方案也非常有效,就是用
pthread_mutex_trylock
函数来申请加锁,这个函数在mutex
锁不可用时,不像pthread_mutex_lock
那样会等待。pthread_mutex_trylock
在申请加锁失败时立刻就会返回错误:EBUSY
(锁尚未解除)或者EINVAL
(锁变量不可用)。- 一旦在
trylock
的时候有错误返回,那就把前面已经拿到的锁全部释放,然后过一段时间再来一遍。 - 当然也可以使用
pthread_mutex_timedlock
这个函数来申请加锁,这个函数跟pthread_mutex_trylock
类似,不同的是,你可以传入一个时间参数,在申请加锁失败之后会阻塞一段时间等解锁,超时之后才返回错误。
- 一旦在
- 这两种方案我更多会使用第一种,原因如下:
- 一般情况下进入临界区需要加的锁数量不会太多,第一种方案能够
hold
住。如果多于2
个,你就要考虑一下是否有些锁是可以合并的了。第一种方案适合锁比较少的情况,因为这不会导致非常大的阻塞延时。但是当你要加的锁非常多,A、B、C、D、E
,你加到D
的时候阻塞了,然而其他线程可能只需要A、B
就可以运行,就也会因为A、B
已经被锁住而阻塞,这时候才会采用第二种方案。如果要加的锁本身就不多,只有A、B
两个,那么阻塞一下也还可以。 - 第二种方案在面临阻塞的时候,要操作的事情太多。当你把所有的锁都释放以后,你的当前线程的处理策略就会导致你的代码复杂度上升:当前线程总不能就此退出吧,你得找个地方把它放起来,让它去等待一段时间之后再去申请锁,如果有多个线程出现了这样的情况,你就需要一个线程池来存放这些等待解锁的线程。如果临界区是嵌套的,你在把这个线程挂起的时候,最好还要把外面的锁也释放掉,要不然也会容易导致死锁,这就需要你在一个地方记录当前线程使用锁的情况。这里要做的事情太多,复杂度比较大,容易出错。
- 一般情况下进入临界区需要加的锁数量不会太多,第一种方案能够
- 所以总而言之,设计的时候尽量减少同一临界区所需要
mutex
锁的数量,然后采用第一种方案。如果确实有需求导致那么多mutex
锁,那么就只能采用第二种方案了,然后老老实实写好周边代码。 - 但是到了
semaphore
情况下的死锁处理方案时,上面两种方案就都不顶用了,后面我会说。另外,还有一种死锁是自己把自己锁死了,这个我在后面也会说。
- 1、如果要进入一段临界区需要多个
pthread_mutex(recursive)递归锁
-
pthread_mutex
锁也支持递归,只需要设置PTHREAD_MUTEX_RECURSIVE
即可 - 这是
pthread_mutex
为了防止在递归的情况下出现死锁而出现的递归锁。作用和NSRecursiveLock
递归锁类似。 - 通过
pthread_mutexattr_t
来设置锁的类型,如下面代码就设置锁为递归锁。
- (void)example5 {
// 定义mutex
pthread_mutex_init(&theLock, NULL);
// 定义mutexattr
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&theLock, &attr);
pthread_mutexattr_destroy(&attr);
pthread_t thread;
pthread_create(&thread, NULL, threadMethord, 5);
}
void *threadMethord(int value) {
pthread_mutex_lock(&theLock);
if (value > 0) {
printf("Value:%i\n", value);
sleep(1);
threadMethord(value - 1);
}
pthread_mutex_unlock(&theLock);
return 0;
}
// Value:5
// Value:4
// Value:3
// Value:2
// Value:1
3、@synchronized
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self) {
sleep(2);
NSLog(@"线程1");
}
sleep(1);
NSLog(@"线程1解锁成功");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(self) {
NSLog(@"线程2");
}
});
// 线程1
// 线程2
// 线程1解锁成功
-
@synchronized(object)
指令使用的object
为该锁的唯一标识,只有当标识相同时,才满足互斥,所以如果线程2
中的@synchronized(self)
改为@synchronized(self.view)
,则线程2
就不会被阻塞 -
@synchronized
指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized
块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。 - 如果在
@sychronized(object){}
内部object
被释放或被设为nil
,从测试的结果来看,的确没有问题,但如果object
一开始就是nil
,则失去了锁的功能。但@synchronized([NSNull null])
是完全可以的。
自旋锁
1、OSSpinLock
-
OSSpinLock
是一种自旋锁,也只有加锁,解锁,尝试加锁三个方法。 - 和
NSLock
不同的是NSLock
请求加锁失败的话,会先轮询,但一秒过后便会使线程进入waiting
状态,等待唤醒。 - 而
OSSpinLock
会一直轮询,等待时会消耗大量CPU
资源,不适用于较长时间的任务。 -
10.0
弃用,使用os_unfair_lock
typedef int32_t OSSpinLock;
//尝试加锁
bool OSSpinLockTry( volatile OSSpinLock *__lock );
//加锁
void OSSpinLockLock( volatile OSSpinLock *__lock );
//解锁
void OSSpinLockUnlock( volatile OSSpinLock *__lock );
- 使用
#import <libkern/OSSpinLockDeprecated.h>
__block OSSpinLock theLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
OSSpinLockUnlock(&theLock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OSSpinLockLock(&theLock);
sleep(1);
NSLog(@"需要线程同步的操作2");
OSSpinLockUnlock(&theLock);
});
// 需要线程同步的操作1 开始
// 需要线程同步的操作1 结束
// 需要线程同步的操作2
os_unfair_lock
-
os_unfair_lock
是苹果官方推荐的替换OSSpinLock
的方案,但是它在iOS10.0
以上的系统才可以调用。
// 简单使用
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
os_unfair_lock_unlock(unfairLock);
读写锁
1、pthread_rwlock
- 前面互斥锁
mutex
有个缺点,就是只要锁住了,不管其他线程要干什么,都不允许进入临界区。- 设想这样一种情况:临界区
foo
变量在被bar1
线程读着,加了个mutex
锁,bar2
线程如果也要读foo
变量,因为被bar1
加了个互斥锁,那就不能读了。 - 但事实情况是,读取数据不影响数据内容本身,所以即便被
1
个线程读着,另外一个线程也应该允许他去读。除非另外一个线程是写操作,为了避免数据不一致的问题,写线程就需要等读线程都结束了再写。
- 设想这样一种情况:临界区
- 因此诞生了读写锁,有的地方也叫共享锁。
- 读写锁的特性是这样的
- 当一个线程加了读锁访问临界区,另外一个线程也想访问临界区读取数据的时候,也可以加一个读锁,这样另外一个线程就能够成功进入临界区进行读操作了。此时读锁线程有两个。
- 当第三个线程需要进行写操作时,它需要加一个写锁,这个写锁只有在读锁的拥有者为
0
时才有效。也就是等前两个读线程都释放读锁之后,第三个线程就能进去写了。- 总结一下就是,读写锁里,读锁能允许多个线程同时去读,但是写锁在同一时刻只允许一个线程去写。
- 这样更精细的控制,就能减少
mutex
导致的阻塞延迟时间。虽然用mutex
也能起作用,但这种场合,明显读写锁更好!
PTHREAD_RWLOCK_INITIALIZER
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//加读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//加写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
// 这个函数在Linux和Mac的man文档里都没有,新版的pthread.h里面也没有,旧版的能找到
int pthread_rwlock_timedrdlock_np(pthread_rwlock_t *rwlock, const struct timespec *deltatime);
// 同上
int pthread_rwlock_timedwrlock_np(pthread_rwlock_t *rwlock, const struct timespec *deltatime);
注意的地方
- 命名
- 跟上面提到的写
muetx
互斥锁的约定一样,操作,类别,序号最好都要有。比如OperationQueue_rwlock_1
。
- 跟上面提到的写
- 认真区分使用场合
- 由于读写锁的性质,在默认情况下是很容易出现写线程饥饿的。因为它必须要等到所有读锁都释放之后,才能成功申请写锁。不过不同系统的实现版本对写线程的优先级实现不同。
Solaris
下面就是写线程优先,其他系统默认读线程优先。 - 比如在写线程阻塞的时候,有很多读线程是可以一个接一个地在那儿插队的(在默认情况下,只要有读锁在,写锁就无法申请,然而读锁可以一直申请成功,就导致所谓的插队现象),那么写线程就不知道什么时候才能申请成功写锁了,然后它就饿死了。
- 为了控制写线程饥饿,必须要在创建读写锁的时候设置
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE
,不要用PTHREAD_RWLOCK_PREFER_WRITER_NP
,这个似乎没什么用,感觉应该是个bug
- 由于读写锁的性质,在默认情况下是很容易出现写线程饥饿的。因为它必须要等到所有读锁都释放之后,才能成功申请写锁。不过不同系统的实现版本对写线程的优先级实现不同。
////////////////////////////// /usr/include/pthread.h
/* Read-write lock types. */
#if defined __USE_UNIX98 || defined __USE_XOPEN2K
enum
{
PTHREAD_RWLOCK_PREFER_READER_NP,
PTHREAD_RWLOCK_PREFER_WRITER_NP, // 妈蛋,没用,一样reader优先
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP,
PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP
};
- 总的来说,这样的锁建立之后一定要设置优先级,不然就容易出现写线程饥饿。而且读写锁适合读多写少的情况,如果读、写一样多,那这时候还是用
mutex
互斥锁比较合理。
递归锁
- 递归锁有一个特点,就是同一个线程可以加锁
N
次而不会引发死锁。
1、NSRecursiveLock
@interface NSRecursiveLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
-
NSRecursiveLock
是递归锁,他和NSLock
的区别在于,NSRecursiveLock
可以在一个线程中重复加锁(反正单线程内任务是按顺序执行的,不会出现资源竞争问题),NSRecursiveLock
会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveBlock)(int);
RecursiveBlock = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value:%d", value);
RecursiveBlock(value - 1);
}
[lock unlock];
};
RecursiveBlock(2);
});
// value:2
// value:1
- 如上面的示例,如果用
NSLock
的话,lock
先锁上了,但未执行解锁的时候,就会进入递归的下一层,而再次请求上锁,阻塞了该线程,线程被阻塞了,自然后面的解锁代码不会执行,而形成了死锁。而NSRecursiveLock
递归锁就是为了解决这个问题。主要是用在循环或递归操作中。 - 这段代码是一个典型的死锁情况。在我们的线程中,
RecursiveMethod
是递归调用的。所以每次进入这个block
时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。
2、pthread_mutex(recursive)
见上文
条件锁
1、NSCondition
@interface NSCondition : NSObject <NSLocking> {
@private
void *_priv;
}
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
- 一种最基本的条件锁。手动控制线程
wait
和signal
。 - 遵循
NSLocking
协议,使用的时候同样是lock
,unlock
加解锁,wait
是傻等,waitUntilDate:
方法是等一会,都会阻塞掉线程,signal
是唤起一个在等待的线程,broadcast
是广播全部唤起。 -
NSCondition
的对象实际上作为一个锁和一个线程检查器,锁上之后其它线程也能上锁,而之后可以根据条件决定是否继续运行线程,即线程是否要进入waiting
状态,经测试,NSCondition
并不会像上文的那些锁一样,先轮询,而是直接进入waiting
状态,当其它线程中的该锁执行signal
或者broadcast
方法时,线程被唤醒,继续运行之后的方法。
NSCondition *lock = [[NSCondition alloc] init];
NSMutableArray *array = [[NSMutableArray alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
while (!array.count) {
[lock wait];
}
[array removeAllObjects];
NSLog(@"array removeAllObjects");
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
[lock lock];
[array addObject:@1];
NSLog(@"array addObject:@1");
[lock signal];
[lock unlock];
});
// array addObject:@1
// array removeAllObjects
- 其中
signal
和broadcast
方法的区别在于,signal
只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用,而broadcast
可以唤醒所有在等待的线程。如果没有等待的线程,这两个方法都没有作用。
2、NSConditionLock
@interface NSConditionLock : NSObject <NSLocking> {
@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;
-
NSConditionLock
和NSLock
类似,都遵循NSLocking
协议,方法都类似,只是多了一个condition
属性,以及每个操作都多了一个关于condition
属性的方法,只有condition
参数与初始化时候的condition
相等,lock
才能正确进行加锁操作。而unlockWithCondition:
并不是当Condition
符合条件时才解锁,而是解锁之后,修改Condition
的值,这个结论可以从下面的例子中得出。
//主线程中
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lockWhenCondition:1];
NSLog(@"线程1");
sleep(2);
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:0]) {
NSLog(@"线程2");
[lock unlockWithCondition:2];
NSLog(@"线程2解锁成功");
} else {
NSLog(@"线程2尝试加锁失败");
}
});
//线程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(2);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"线程3");
[lock unlock];
NSLog(@"线程3解锁成功");
} else {
NSLog(@"线程3尝试加锁失败");
}
});
//线程4
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(3);//以保证让线程2的代码后执行
if ([lock tryLockWhenCondition:2]) {
NSLog(@"线程4");
[lock unlockWithCondition:1];
NSLog(@"线程4解锁成功");
} else {
NSLog(@"线程4尝试加锁失败");
}
});
// 线程2
// 线程2解锁成功
// 线程3
// 线程3解锁成功
// 线程4
// 线程4解锁成功
// 线程1
- 从上面可以得出,
NSConditionLock
还可以实现任务之间的依赖。
信号量
dispatch_semaphore
//传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。
//值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。
dispatch_semaphore_create(long value);
//这个函数会使传入的信号量dsema的值减1;
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
//这个函数会使传入的信号量dsema的值加1;
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
-
dispatch_semaphore
是GCD
用来同步的一种方式,与他相关的只有三个函数,一个是创建信号量,一个是等待信号,一个是发送信号。 - 这个函数的作用是这样的
- 如果
dsema
信号量的值大于0
,该函数所处线程就继续执行下面的语句,并且将信号量的值减1
; - 如果
desema
的值为0
,那么这个函数就阻塞当前线程等待timeout
(注意timeout
的类型为dispatch_time_t
,不能直接传入整形或float
型数) - 如果等待的期间
desema
的值被dispatch_semaphore_signal
函数加1
了,且该函数(即dispatch_semaphore_wait
)所处线程获得了信号量,那么就继续向下执行并将信号量减1
。 - 如果等待期间没有获取到信号量或者信号量的值一直为
0
,那么等到timeout
时,其所处线程自动执行其后语句。
- 如果
-
dispatch_semaphore
是信号量,但当信号总量设为1
时也可以当作锁来。在没有等待情况出现时,它的性能比pthread_mutex
还要高,但一旦有等待情况出现时,性能就会下降许多。相对于OSSpinLock
来说,它的优势在于等待时不会消耗CPU
资源。
// 超时时间overTime设置成>2
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要线程同步的操作1 开始");
sleep(2);
NSLog(@"需要线程同步的操作1 结束");
dispatch_semaphore_signal(signal);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要线程同步的操作2");
dispatch_semaphore_signal(signal);
});
// 需要线程同步的操作1 开始
// 需要线程同步的操作1 结束
// 需要线程同步的操作2
// 超时时间设置为<2s的时候
//...
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC);
//...
// 需要线程同步的操作1 开始
// 需要线程同步的操作2
// 需要线程同步的操作1 结束
- 如上的代码,如果超时时间
overTime
设置成>2
,可完成同步操作。如果overTime<2
的话,在线程1
还没有执行完成的情况下,此时超时了,将自动执行下面的代码。
dispatch_semaphore
和NSCondition
类似,都是一种基于信号的同步方式,但NSCondition
信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效)。而dispatch_semaphore
能保存发送的信号。dispatch_semaphore
的核心是dispatch_semaphore_t
类型的信号量。
-
dispatch_semaphore_create(1)
方法可以创建一个dispatch_semaphore_t
类型的信号量,设定信号量的初始值为1
。注意,这里的传入的参数必须大于或等于0
,否则dispatch_semaphore_create
会返回NULL
。 -
dispatch_semaphore_wait(signal, overTime);
方法会判断signal
的信号值是否大于0
。大于0
不会阻塞线程,消耗掉一个信号,执行后续任务。- 如果信号值为
0
,该线程会和NSCondition
一样直接进入waiting
状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当overTime
时限到了,也会执行后续任务。
- 如果信号值为
-
dispatch_semaphore_signal(signal);
发送信号,如果没有等待的线程接受信号,则使signal
信号值加1
(做到对信号的保存)。 - 和
NSLock
的lock
和unlock
类似,区别只在于有信号量这个参数,lock
unlock
只能同一时间,一个线程访问被保护的临界区,而如果dispatch_semaphore
的信号量初始值为x
,则可以有x
个线程同时访问被保护的临界区。
补充
pthread_cleanup_push()
& pthread_cleanup_pop()
- 线程是允许在退出的时候,调用一些回调方法的。如果你需要做类似的事情,那么就用以下这两种方法:
void pthread_cleanup_push(void (*callback)(void *), void *arg);
void pthread_cleanup_pop(int execute);
- 正如名字所暗示的,它背后有一个
stack
,你可以塞很多个callback
函数进去,然后调用的时候按照先入后出的顺序调用这些callback
。所以你在塞callback
的时候,如果是关心调用顺序的,那就得注意这一点了。 - 但是!你塞进去的callback只有在以下情况下才会被调用:
- 线程通过
pthread_exit()
函数退出 - 线程被
pthread_cancel()
取消 -
pthread_cleanup_pop(int execute)
时,execute
传了一个非0
值
- 线程通过
- 也就是说,如果你的线程函数是这么写的,那在线程结束的时候就不会调到你塞进去的那些
callback
了:
static void * thread_function(void *args)
{
...
...
...
...
return 0; // 线程退出时没有调用pthread_exit()退出,而是直接return,此时是不会调用栈内callback的
}
-
pthread_cleanup_push
塞入的callback
可以用来记录线程结束的点,般不太会在这里执行业务逻辑。在线程结束之后如果要执行业务逻辑,一般用下面提到的pthread_join
。
注意事项:callback
函数是可以传参数的
- 在
pthread_cleanup_push
函数中,第二个参数的值会作为callback
函数的第一个参数,拿来打打日志也不错
void callback(void *callback_arg)
{
printf("arg is : %s\n", (char *)callback_arg);
}
static void * thread_function(void *thread_arg)
{
...
pthread_cleanup_push(callback, "this is a queue thread, and was terminated.");
...
pthread_exit((void *) 0); // 这句不调用,线程结束就不会调用你塞进去的callback函数。
return ((void *) 0);
}
int main ()
{
...
...
error = pthread_create(&tid, NULL, thread_function, (void *)thread_arg)
...
...
return 0;
}
要保持callback栈平衡
pthread_cleanup_pop(0); // 传递参数0,在pop的时候就不会调用对应的callback,如果传递非0值,pop的时候就会调用对应callback了。
pthread_cleanup_pop(0); // push了两次就pop两次,你要是只pop一次就不能编译通过
-
pthread
对于这两个函数是通过宏来实现的,如果没有一一对应,编译器就会报} missing
的错误。其相关实现代码如下:
/* ./include/pthread/pthread.h */
#define pthread_cleanup_push(rt, rtarg) __pthread_cleanup_push(rt, rtarg)
#define pthread_cleanup_pop(execute) __pthread_cleanup_pop(execute)
/* ./sysdeps/generic/bits/cancelation.h */
#define __pthread_cleanup_push(rt, rtarg) \
{ \
struct __pthread_cancelation_handler **__handlers \
= __pthread_get_cleanup_stack (); \
struct __pthread_cancelation_handler __handler = \
{ \
(rt), \
(rtarg), \
*__handlers \
}; \
*__handlers = &__handler;
#define __pthread_cleanup_pop(execute) \
if (execute) \
__handler.handler (__handler.arg); \
*__handlers = __handler.next; \
} \
具体细节请参考推荐博文