OSSpinLock
OSSpinLock
自旋锁,因为自旋锁一直busy-waiting
忙等待占用cpu,且不会像互斥锁、信号量一样会导致线程休眠,进而引发上下文切换,因此短时间持有自旋锁
性能最高;但适合多线程处理器及持有时间较短(对于单线程处理器会降低cpu效率),并且存在优先级反转问题(不再安全的 OSSpinLock,对于ios线程存在多个优先级且高优先级不会被低优先级抢占,导致低优先级占用自旋锁后,高优先级被执行获取自旋锁,导致死锁等待,见下)
如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。
对于ios跟踪OSSpinLock
汇编,其实现就是while
循环,具体如下:
0x104ba98b1 <+12>: cmpl $-0x1, %eax
0x104ba98b4 <+15>: jne 0x104ba98d1 ; <+44>
0x104ba98b6 <+17>: testl %ecx, %ecx
0x104ba98b8 <+19>: je 0x104bab0f9 ; _OSSpinLockLockYield
0x104ba98be <+25>: pause
0x104ba98c0 <+27>: incl %ecx
0x104ba98c2 <+29>: movl (%rdi), %eax
0x104ba98c4 <+31>: testl %eax, %eax
0x104ba98c6 <+33>: jne 0x104ba98b1 ; <+12>
0x104ba98c8 <+35>: xorl %eax, %eax
0x104ba98ca <+37>: lock
0x104ba98cb <+38>: cmpxchgl %edx, (%rdi)
0x104ba98ce <+41>: jne 0x104ba98b1 ; <+12>
对于linux
实现(linux2.6内核,)如下:
#define spin_lock(lock) _spin_lock(lock) //lock数据类型为*spinlock_t,虽然没有用到-_-。
#define _spin_lock(lock) __LOCK(lock)
#define __LOCK(lock) \
do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)
linux
实现是通过关抢占来避免其他任务抢占来保证原子性(也存在关闭中断抢占),此时若持有自旋锁的线程休眠,引发上下文切换,另一个线程也获取该自旋锁而导致死锁(因关抢占除非主动调度,因此无法释放自旋锁);
使用如下:
OSSpinLock lock = OS_SPINLOCK_INIT;//初始化
OSSpinLockLock(&lock);//上锁
OSSpinLockTry(&lock);//尝试上锁
OSSpinLockUnlock(&lock);//解锁
ios10.0+ mac10.12+已废弃OSSpinLock,替换为os_unfair_lock来解决优先级反转问题;
源码地址:
SpinLocksLoadStoreEx.c
os_unfair_lock
os_unfair_lock
用于取代不安全的OSSpinLock
,从iOS10 macos10.12
开始才支持 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等,需要导入头文件#import <os/lock.h>
//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);
跟踪汇编实现如下:
libsystem_kernel.dylib`__ulock_wait:
0x10701f360 <+0>: movl $0x2000203, %eax ; imm = 0x2000203
0x10701f365 <+5>: movq %rcx, %r10
0x10701f368 <+8>: syscall
0x10701f36a <+10>: jae 0x10701f374 ; <+20>
0x10701f36c <+12>: movq %rax, %rdi
0x10701f36f <+15>: jmp 0x10701ce67 ; cerror_nocancel
0x10701f374 <+20>: retq
0x10701f375 <+21>: nop
0x10701f376 <+22>: nop
0x10701f377 <+23>: nop
其中syscall
执行后线程休眠,执行路径:os_unfair_lock_lock
-> _os_unfair_lock_lock_slow
-> __ulock_wait
pthread_mutex_t pthread_cond_t
互斥锁
及条件变量
为POSIX
接口,见《unix进程间通信.md》
NSLock NSRecursiveLock
NSLock
是对pthread_mutex
普通锁的封装。pthread_mutex_init(mutex, NULL);
默认属性为PTHREAD_MUTEX_NORMAL
NSLock
遵循 NSLocking 协议,使用方法与pthread_mutex
类似,如下:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking>
- (BOOL)tryLock;//tryLock 是尝试加锁,如果失败的话返回 NO
- (BOOL)lockBeforeDate:(NSDate *)limit;//是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO
@end
具体NSLock
实现可参考GNUSetup
, 搜索NSLock.m,找到 initialize 方法
GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍,虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值
(void) initialize
{
static BOOL beenHere = NO;
if (beenHere == NO)
{
beenHere = YES;
/* Initialise attributes for the different types of mutex.
* We do it once, since attributes can be shared between multiple
* mutexes.
* If we had a pthread_mutexattr_t instance for each mutex, we would
* either have to store it as an ivar of our NSLock (or similar), or
* we would potentially leak instances as we couldn't destroy them
* when destroying the NSLock. I don't know if any implementation
* of pthreads actually allocates memory when you call the
* pthread_mutexattr_init function, but they are allowed to do so
* (and deallocate the memory in pthread_mutexattr_destroy).
*/
pthread_mutexattr_init(&attr_normal);
pthread_mutexattr_settype(&attr_normal, PTHREAD_MUTEX_NORMAL);
pthread_mutexattr_init(&attr_reporting);
pthread_mutexattr_settype(&attr_reporting, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutexattr_init(&attr_recursive);
pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);
/* To emulate OSX behavior, we need to be able both to detect deadlocks
* (so we can log them), and also hang the thread when one occurs.
* the simple way to do that is to set up a locked mutex we can
* force a deadlock on.
*/
pthread_mutex_init(&deadlock, &attr_normal);
pthread_mutex_lock(&deadlock);
}
}
NSRecursiveLock
是对pthread_mutex
递归属性下的封装,API与NSLock
一致;
NSCondition
NSCondtion
是对pthread_mutex
和pthread_cond
的封装,具体类源码如下:
+ (void) initialize
{
[NSLock class]; // Ensure mutex attributes are set up.
}
- (id) init
{
if (nil != (self = [super init]))
{
if (0 != pthread_cond_init(&_condition, NULL))
{
DESTROY(self);
}
else if (0 != pthread_mutex_init(&_mutex, &attr_reporting))
{
pthread_cond_destroy(&_condition);
DESTROY(self);
}
}
return self;
}
- (void) signal
{
pthread_cond_signal(&_condition);
}
- (void) wait
{
pthread_cond_wait(&_condition, &_mutex);
}
- (void) broadcast
{
pthread_cond_broadcast(&_conditon);
}
具体使用如下:
@interface NSCondition : NSObject <NSLocking> {
- (void)wait;//等待条件
- (BOOL)waitUntilDate:(NSDate *)limit;//超时等待
- (void)signal;//发送信号
- (void)broadcast;//广播信号
@end
需要注意NSCondition遵循NSLock协议,同pthread_cond_t,需要添加条件变量时加锁,避免信号丢失及条件变量无法获取锁导致一直阻塞线程等待;
NSCondtionLock
NSConditionLock
是对NScondition
的进一步封装,具体如下:
@interface NSConditionLock : NSObject <NSLocking> {
- (instancetype)initWithCondition:(NSInteger)condition;//初始化Condition,并且设置状态值
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;//当状态值为condition且收到等待的条件变量信号时返回,否则一直阻塞等待条件变量信号
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;//修改condition条件判断值并广播条件变量信号
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name;
@end
NSCondition同pthread_cond一样,存在pthread_cond_wait被虚假唤醒的情况,一般都会while(condition != true)循环判断是否条件为真,否则一直pthread_cond_wait阻塞;NSConditionLock就是提供了condition条件判断的逻辑;
具体类实现如下:
- (id) init
{
return [self initWithCondition: 0];
}
- (id) initWithCondition: (NSInteger)value
{
if (nil != (self = [super init]))
{
if (nil == (_condition = [NSCondition new]))
{
DESTROY(self);
}
else
{
_condition_value = value;
}
}
return self;
}
- (void) lockWhenCondition: (NSInteger)value
{
[_condition lock];
while (value != _condition_value)
{
[_condition wait];
}
}
//条件判断值改变并广播信号解锁
- (void) unlockWithCondition: (NSInteger)value
{
_condition_value = value;
[_condition broadcast];
[_condition unlock];
}
synchronized
@synchronized
使用如下:
@synchronized(obj) {
//code
}
上述代码转化为c++,简化如下:
try {
objc_sync_enter(obj);
//code
} finally {
objc_sync_exit(obj);
}
对于objc_sync_enter
及objc_sync_exit
源码如下:
// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}
return result;
}
其实现使用了递归锁
来实现加锁避免多次使用@synchronized
导致死锁,并使用缓存技术通过传入的objc
地址来查找对应的锁,其中缓存是使用了hash map
来缓存;若传入nil
则锁不起作用;若@synchronized
中的block
修改了objc
地址也不影响锁结构;并使用try finally
来捕获异常,避免锁未释放;
使用注意事项
慎用@synchronized(self)
使用self
对于外部可以修改使用的对象地址,容易外部混合使用@synchronized
及其他锁导致死锁问题,如:
//class A
@synchronized (self) {
[_sharedLock lock];
NSLog(@"code in class A");
[_sharedLock unlock];
}
//class B
[_sharedLock lock];
@synchronized (objectA) {
NSLog(@"code in class B");
}
[_sharedLock unlock];
对于类内部数据同步,正确的做法是传入一个类内部维护的NSObject对象,而且这个对象是对外不可见的;
减小粗粒度
对于不同的数据锁同步使用不同的objc
对象来控制,避免无关的对象锁,且block
内部尽量避免函数调用;
正确使用多线程同步锁@synchronized
dispatch_queue(DISPATCH_QUEUE_SERIAL) GCD串行队列
dispatch_queue_t queue = dispatch_queue_create("top.istones.moneyQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(self.moneyQueue, ^{
// 任务
});
任务顺序串行同步执行;
dispatch_semaphore
dispatch_semaphore
信号量,同unix sem
概念相同,具体使用如下:
//表示最多开启5个线程
dispatch_semaphore_create(5);
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
实际封装的
系统库semaphone_xxx
,使用的是基于内存的信号量形式;
但是在资源可用的情况下,使用GCD semaphore将会消耗较少的时间,因为在这种情况下GCD不会调用内核,只有在资源不可用的时候才会调用内核,并且系统需要停在你的线程里,直到线程发出可用信号。
pthread_rwlock 读写锁
pthread_rwlock经常用于文件等数据的读写操作,需要导入头文件#import <pthread.h>
atomic属性
atomic
用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁(使用了自旋锁);
可以参考源码objc4的objc-accessors.mm;
它并不能保证使用属性的过程是线程安全的(eg.一个属性array,atomic的话只能保证在外面set和get的时候线程安全,但是不能保证array addObject、removeObject线程安全);
atomic
属性保证的属性的值修改(包括数值类型及指针类型)线程安全,但不保证指针指向的内存的安全及多部操作的原子性,如上array
;见iOS多线程到底不安全在哪里?
对于property分为三类内存模型:指针、指针指向的内存区域及数值类型;对于指针及指向的内存容易好理解,对于数值类型,64位系统小于等于8位的数值,一个指令周期就可以获取,因此set/get
操作atomic
或noatomic
都可以保证原子性,但对于数值操作,如self.count = self.count + 1;
排除编译器优化的影响,分为读取(load)、加1(add)、赋值(store)三步操作,store前可能存在多次store操作;
建议:尽量避免多线程设计,若不可避免,则尽量不使用atomic
属性,而使用锁机制,保证多条指令执行的原子性;
源码使用了自旋锁来保证属性指针修改或者值修改线程安全,源码见下:
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);
}
void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}
关键代码如下:
//设置noatomic
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {//设置atomic
spinlock_t& slotlock = PropertyLocks[slot];//自旋锁
slotlock.lock();//自旋锁加锁
oldValue = *slot;//保留旧值
*slot = newValue;//修改新值的指针地址
slotlock.unlock();//自旋锁释放锁
}
dispatch_barrier_async dispatch_barrier_sync
这个函数传入的必须是自己通过dispatch_queue_cretate创建的DISPATCH_QUEUE_CONCURRENT并发队列,如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果;
作用是类似栅栏,但相比栅栏能控制汇合的点,即dispatch_barrier_asyn
或者dispatch_barrier_sync
,在这之前添加到队列的所有block
执行完成,才去执行这两个函数中的block
,再去执行后续添加的block
;
主要用于并发读写控制;
dispatch_queue_t queue = dispatch_queue_create("top.istones.rwQueue", DISPATCH_QUEUE_CONCURRENT);
// 读
dispatch_async(queue, ^{
});
// 写
dispatch_barrier_async(queue, ^{
});
两者的相同:
- 等待前面的任务都执行完成才去执行后面的函数添加的任务;
不同:
-
dispatch_barrier_sync
会阻塞调用线程此函数后面的执行(因为同步执行),而dispatch_barrier_async
不会阻塞当前调用线程后面的代码执行;
dispatch_barrier_sync、dispatch_barrier_async的使用
参考资料
Demo
https://github.com/FengyunSky/notes/blob/master/local/code/threadlock.tar