锁
最底层的锁有两周互斥锁和自旋锁,有很多高级的锁都是基于它们实现的
加锁的目的是保证共享资源的任意时间里,只有一个线程访问,这样就可以避免由于多线程导致的数据错乱的问题。
互斥锁
互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。
对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。
自旋锁是最比较简单的一种锁,一直自旋,利用 CPU 周期,直到锁可用。需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。
如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁。
自旋锁与互斥锁使用层面比较相似,但实现层面上完全不同:当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。
互斥锁中又分为递归锁和非递归锁 下面我们分析常见的几种锁
@synchronized
我们一共有20张票,
然后有好几个窗口在同时卖票,
好了,现在开始卖票
但是我们发现,在票为17张的时候下面显示出18张,这样这个票数便不准确了。
这是因为在同一时刻有多个线程同时操作同一块内存区域,导致数据不准确,我们加一个锁,在同一时刻只有一个线程操作这个票数。
在上面的例子中我们用到了@synchronized锁,它起到一个加锁的效果。有时候我们还会看到下面这种结构。@synchronized是可以实现递归可重入的。那么@synchronized的结构是怎么样子的,是怎么实现加锁的效果的❓。我们来分析源码
@synchronized (p) {
@synchronized (p1) {
}
}
加个断点,看到汇编走到了
找到objc的源码,搜索objc_sync_enter
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;
}
从else看出,如果obj为空,则does nothing
看下objc_sync_exit
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;
}
两者代码很相似,在obj不为空的情况下,分别进行了data->mutex.lock();和bool okay = data->mutex.tryUnlock();操作。
我们看下SyncData* data = id2data(obj, ACQUIRE);主要流程代码
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS
bool fastCacheOccupied = NO;
//#define __PTK_FRAMEWORK_OBJC_KEY1 41
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
//如果从线程里面找到有data则走if里面,线程lockCount++
if(data){...}
SyncCache *cache = fetch_cache(NO);
//Check per-thread cache of already-owned locks for matching object
//在锁退出的时候,如果cache中有,进行lockCount--,threadCount--操作
if (cache) {...}
lockp->lock();
//如果data里面没有listp里面的object==object则threadCount,线程数++
{...}
//创建一个SyncData添加到list
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
*listp = result;
done:
lockp->unlock();
if (result) {
//存入cache 等别的操作
...
}
return result;
数据结构
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
我们看下SyncList, StripedMap
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
...
}
//里面有递归锁,还有多线程,所以比recursive_mutex_t更强大,用的更多一点
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData; //单链表
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
可以看到sDataLists是一个全局的静态哈希表
打印一下,由class StripedMap我们使用的模拟器,所以StripeCount = 64,是下面的结构
(StripedMap<SyncList>::PaddedT [64]) array = {
[0] = {
value = {
data = NULL
lock = {
mLock = (_os_unfair_lock_opaque = 0)
}
}
}
[1] = {
value = {
data = NULL
lock = {
mLock = (_os_unfair_lock_opaque = 0)
}
}
}
[2] = {
value = {
data = NULL
lock = {
mLock = (_os_unfair_lock_opaque = 0)
}
}
}
...
[63] = {
value = {
data = NULL
lock = {
mLock = (_os_unfair_lock_opaque = 0)
}
}
}
}
小结
objc_sync_enter/exit 是对称出现的。封装的是底层的递归锁 支持两种存储:TLS、cache 第一次会创建一个syncData,会进行一个头插法,创建一个链表结构,标记threadCount=1、 第二次进来会判断当前是不是同一个对象进来,如果不是,重新创建,重新标记 如果是同一个对象,TLS可以找到之后,就会多lockCount进行加加,如果TLS找不到,listp里面有这个object则threadCount++ 如果exit,则进行一个lockCount--和threadCount--
为什么Synchronized:具备可重入、递归和多线程 1.有线程空间的保障(TLS),保障了threadCount,有多少条线程对这个锁进行加锁 2.lockCount++进行了多少次
NSLock、NSRecursiveLock
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_testArray = [NSMutableArray array];
});
}
在这种多线程同时对一块资源进行操作的时候,会有线程不安全的情况,上面会出现崩溃,我们给 _testArray = [NSMutableArray array];进行加锁,可以解决这个问题。
NSLock *lock = [[NSLock alloc] init];
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
_testArray = [NSMutableArray array];
[lock unlock];
});
}
我们点进去,看下NSLock,发现它遵循NSLocking协议,这个协议里面有两个方法lock和unlock
for (int i= 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
});
}
对于这个例子我们要怎么加锁呢,如果不加锁会发现输出的数据会存在紊乱的情况。
NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
static void (^testMethod)(int);
testMethod = ^(int value){
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
};
testMethod(10);
[lock unlock];
});
}
如果我们把锁加如下的位置
NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10);
});
}
则只输出10,每次进去进行一个lock,但是没有unlock,所以只输出10
我们知道NSRecursiveLock是递归锁,我们把NSRecursiveLock换成递归锁
输出10 9 8 7 6 5 4 3 2 1 然后崩溃了,NSRecursiveLock只是单线程的递归,多线程会出现问题。我们知道前面的@synchronized支持多线程,那么我们换成@synchronized
则输出正常
for (int i= 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
@synchronized (self) {
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
}
};
testMethod(10);
});
}
我们看到
NSRecursiveLock也是遵循协议
NSCondition
有时候会有一些生产消费问题,消费依赖生产,先生产了才能消费,如果生产为0此时消费应该等着生产,当生产为正,则发送一个消息给消费,可以消费了,然后开始消费,这种情况下,我们可以使用NSCondition
api文档也比较简单,
eg:
- (void)lg_testConditon{
_testCondition = [[NSCondition alloc] init];
//创建生产-消费者
for (int i = 0; i < 50; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self lg_producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self lg_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self lg_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self lg_producer];
});
}
}
- (void)lg_producer{
[_testCondition lock]; // 操作的多线程影响
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",self.ticketCount);
[_testCondition signal]; // 信号
[_testCondition unlock];
}
- (void)lg_consumer{
[_testCondition lock]; // 操作的多线程影响
if (self.ticketCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
[_testCondition wait];
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_testCondition unlock];
}
NSConditionLock
api中一些简单的使用
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@"线程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
sleep(0.1);
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
小结
这些锁底层都是对 pthread_mutex_lock的封装, 对于NSLock的源码
open class NSLock: NSObject, NSLocking {
...
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
}
...
}
对于NSRecursiveLock的源码
open class NSRecursiveLock: NSObject, NSLocking {
public override init() {
super.init()
...
withUnsafeMutablePointer(to: &attrib) { attrs in
pthread_mutexattr_init(attrs)
//比NSLock多一个标记
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
pthread_mutex_init(mutex, attrs)
}
...
}
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
}
}
NSCondition源码
open class NSCondition: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
internal var cond = _ConditionVariablePointer.allocate(capacity: 1)
public override init() {
pthread_mutex_init(mutex, nil)
}
deinit {
pthread_mutex_destroy(mutex)
pthread_cond_destroy(cond)
}
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
}
open func wait() {
pthread_cond_wait(cond, mutex)
}
open func wait(until limit: Date) -> Bool {
guard var timeout = timeSpecFrom(date: limit) else {
return false
}
return pthread_cond_timedwait(cond, mutex, &timeout) == 0
}
open func signal() {
pthread_cond_signal(cond)
}
open func broadcast() {
pthread_cond_broadcast(cond) // wait signal
}
open var name: String?
}
NSConditionLock源码
open class NSConditionLock : NSObject, NSLocking {
internal var _cond = NSCondition()
internal var _value: Int
internal var _thread: _swift_CFThreadRef?
public convenience override init() {
self.init(condition: 0)
}
public init(condition: Int) {
_value = condition
}
open func lock() {
let _ = lock(before: Date.distantFuture)
}
open func unlock() {
_cond.lock()
_thread = nil
}
open var condition: Int {
return _value
}
open func lock(whenCondition condition: Int) {
let _ = lock(whenCondition: condition, before: Date.distantFuture)
}
open func `try`() -> Bool {
return lock(before: Date.distantPast)
}
open func tryLock(whenCondition condition: Int) -> Bool {
return lock(whenCondition: condition, before: Date.distantPast)
}
open func unlock(withCondition condition: Int) {
_cond.lock()
_thread = nil
_value = condition
_cond.broadcast()
_cond.unlock()
}
open func lock(before limit: Date) -> Bool {
_cond.lock()
while _thread != nil {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
_thread = pthread_self()
_cond.unlock()
return true
}
open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
_cond.lock()
while _thread != nil || _value != condition {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
_thread = pthread_self()
_cond.unlock()
return true
}
open var name: String?
}
可见NSConditionLock中有一个value和NSCondition
锁的底层都是对pthread的封装
读写锁
有时候一些场景,比如对文件的操作,我们会要求
- 读写互斥
- 写写互斥
- 可以同时多个读操作
- 写的时候不能阻塞流程
这种情况下要怎么设计呢
对于写我们可以使用
dispatch_barrier_async函数,由多线程的知识知道,dispatch_barrier_async起到在同一个队列上的同步的效果,但是并不阻塞线程,所以可以满足写的时候不能阻塞流程和写写互斥
- (void)setAryWithValue:(NSString *)value time:(int)time {
sleep(time);
dispatch_barrier_async(self.read_write_queue, ^{
[self.dataDic setValue:value forKey:@"name"];
NSLog(@"写情况--%@--%@",self.dataDic[@"name"], [NSThread currentThread]);
});
}
对于读,我们使用的是dispatch_sync函数,这是一个同步函数,保证在这个队列上同步,同时写也是在这个队列上,所以读和写互斥,dispatch_sync虽然是一个同步函数,但是读的时候我们使用多线程来读,dispatch_sync只是在当前线程同步,所以可以实现多读。
有人会疑问,那读使用dispatch_async不就好了,dispatch_async的话异步函数,会导致return value;比value = self.dataDic[@"name"];执行的更快,从而返回的为空
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0 ), ^{
NSLog(@"现在这个值是--%@", [self valueForDataDicWithKey:@"name"]);
});
}
}
- (NSString *)valueForDataDic{
__block NSString *value;
dispatch_sync(self.read_write_queue, ^{
value = self.dataDic[@"name"];
NSLog(@"读情况--%@--%@",self.dataDic[@"name"], [NSThread currentThread]);
});
return value;
}