iOS 线程锁

概念

  1. 自旋锁
    1.1 OSSpinLock
    1.2 os_unfair_lock
    1.3 atomic
  2. 互斥锁
    2.1 pthread_mutex_t
    2.2 NSLock
    2.3 NSRecursiveLock
    2.4 @synchronized
    2.5 dispatch_semaphore_t
    2.6 NSCondition
    2.7 NSConditionLock
  3. 读写锁

性能对比
参考


概念

什么是锁
  • 锁是一种同步的机制。
  • 锁是一个对象。
  • 锁是为了保证某一个资源在同一时间,不被多个“潜在调用者”持有,保证资源在同一时间不会因抢夺而出现错误。
  • 锁是对资源的访问限制。


死锁

当两个及两个以上的运算单元在等待对方停止运行,从而获取对方持有的系统资源,但又没有一方提前退出争夺的时候,就会产生死锁。


死锁

如果线程1和线程2,谁都不先释放自己对已拥有的锁对象的持有权,那么就会陷入互相等待对方先松手的状态,这就是死锁。

死锁产生的必要条件
  1. 互斥条件 :
    资源只能在同一时间分配给某一个运算单元,如果其他的运算单元也要请求同一资源,则只能等待持有资源的运算单元使用完毕。
  2. 持有和等待 :
    某运算单元已经持有一个或多个资源,又请求了其他被占用的资源,这个运算单元并不释放自己已有的资源,持有资源进行新资源的等待。
  3. 不可剥夺条件 :
    指运算单元已经获得了资源,在没有使用完成该资源的情况下,该资源不可以被剥夺,只能等待运算单元使用完毕后自己释放。
  4. 循环等待 :
    指一组集合中有很多运算单元,它们互相持有其他运算单元的资源。

1. 自旋锁

线程反复检查锁变量是否可用,在此过程中线程一直处于执行状态。适用于预期持有锁时间很短的操作,此时因为阻塞线程和唤醒涉及上下文切换和线程数据结构的更新,在cpu资源宽裕且锁预期等待时间很短的情况下,轮询通常比阻塞线程更有效。

1.1 OSSpinLock(iOS 10 以后废弃)

OSSpinLock spinLock = OS_SPINKLOCK_INIT;
OSSpinLockLock(&spinLock);
//code
OSSpinLockUnlock(&spinLock);

OSSSpinLock 存在优先级反转问题。如果一个低优先级的线程 A 获得锁并访问共享资源,这时如果另一个高优先级的线程 B 也尝试获得这个锁,线程 B 会处于忙等状态,由于线程 B 是一个高优先级线程,因此 CPU 会尽量将执行的资源分配给线程B,从而导致线程 B 占用大量 CPU 而线程 A 由于得到的执行资源少而迟迟无法解锁。

1.2 os_unfair_lock

//头文件
@import Darwin.os.lock;

os_unfair_lock_t lock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(lock);
// code
os_unfair_lock_unlock(lock);

多个线程同时等待锁时,先请求获取锁的线程不一定会先获取锁,锁的获取与请求锁的先后顺序无关,例如最后请求获取锁的线程可能先获得锁。
锁的获取和释放基于原子操作。
只包含一个指针大小的内存空间,性能开销小。

1.3 atomic

@protocol(atomic, assign) NSInteger count;

编译器自动生成 getter / setter 内部会调用 objc_getProperty / reallySetProperty 方法,方法内部根据是否为原子属性执行不同的代码

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}
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);
}

原子性修饰的属性会进行 spinlock 加锁处理,spinlock由于优先级反转问题已经被 os_unfair_lock 代替

using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
    os_unfair_lock mLock;
    ...
}

对于原子性修饰的属性,只能保证 getter / setter 的线程安全,无法保证属性在使用过程中的线程安全。例如可变数组在多个线程中 removeObjectAtIndex:


2. 互斥锁

是一种用于多线程编程中,防止多条线程同时对同一公共资源(比如全局变量)进行读写的机制。它通过将代码切片成一个一个的临界区域达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。
互斥锁不会出现忙碌等待,仅仅是线程阻塞。

2.1 pthread_mutex_t

// 导入头文件
#import <pthread/pthread.h>

pthread_mutex_t lock;
pthread_mutexattr_t attr;
// 初始化属性
pthread_mutexattr_init(&attr);
/*
 * Mutex type attributes
#define PTHREAD_MUTEX_NORMAL        0  // 普通
#define PTHREAD_MUTEX_ERRORCHECK    1  // 此类型互斥量会自动检测死锁。检查错误、提供错误提示,需要消耗一定的性能
#define PTHREAD_MUTEX_RECURSIVE     2  // 递归
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL
 */
// 设置类型为递归
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁,如果不需要设置属性则第二个参数传入NULL,使用默认属性
pthread_mutex_init(&lock, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 加锁
pthread_mutex_lock(&lock);
// code
// 解锁
pthread_mutex_unlock(&lock);

// 使用完成后需要在合适的时机对锁进行销毁
pthread_mutex_destroy(&lock);

pthread_mutex 是 iOS 中,多种类型的锁的底层实现,例如 NSLock、NSRecursiveLock、@ synchronized 。
pthread_mutex底层有实现一个阻塞队列,如果当前有其他任务正在执行,则加入到队列中,放弃当前cpu时间片。一旦其他任务执行完,则从队列中取出等待执行的线程对象,恢复上下文重新执行。

2.2 NSLock
NSLock * lock = [[NSLock alloc] init];
[lock lock];
// code
[lock unlock];

NSLock 是对 pthread_mutex 的封装,属性为 PTHREAD_MUTEX_ERRORCHECK,它会自动检测死锁,损失一定性能换来错误提示。
注意:在同一个线程中多次对同一个对象加锁会导致死锁。

2.3 NSRecursiveLock
NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];
[lock lock];
// code 
[lock unlock];

NSRecursiveLock 是对 pthread_mutex 的封装,属性为 PTHREAD_MUTEX_RECURSIVE
递归锁允许在同一线程内对同一个锁对象多次加锁,但是需要注意的是在线程执行完毕后必须在当前线程内进行同样次数的解锁操作,否则会导致其他线程无法获得锁(死锁)。

- (void)NSRecursiveLockTest:(NSRecursiveLock *)lock some:(NSInteger)i {
    [lock lock];
    NSLog(@"%zd", i);
    if (i != 0) {
        [self NSRecursiveLockTest:lock some:--i];
    } else {
        // i == 0 时直接结束递归,不进行解锁
        return;
    }
    [lock unlock];
}

NSRecursiveLock * lock = [[NSRecursiveLock alloc] init];
// 由于首先执行的线程没有解锁,导致后面执行的线程一直在等待解锁
dispatch_async(self.queue, ^{
     [self NSRecursiveLockTest:lock some:3];
});
dispatch_async(self.queue, ^{
     [self NSRecursiveLockTest:lock some:4];
});
2.4 @synchronized
@synchronized (obj) {
// code
}

@synchronized是对pthread_mutex递归锁的封装, @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作。如果 obj 为 nil 则不会进行加锁,不能保证线程安全。

2.5 dispatch_semaphore_t
// 初始化 
dispatch_semaphore_t semaphore_t = dispatch_semaphore_create(0);
// 如果信号计数 <= 0 阻塞当前线程,否则信号计数 - 1 不阻塞当前线程
dispatch_semaphore_wait(semaphore_t,DISPATCH_TIME_FOREVER);
// 信号计数 + 1
dispatch_semaphore_signal(semaphore_t);

GCD信号量是通过对信号量计数和0的对比来进行锁的实现。

  • 当信号量的信号计数 > 0,使信号计数 -1,并不造成线程阻塞。
  • 当信号量的信号计数 <= 0,其所在的线程会被阻塞执行,直到信号计数 > 0 为止。

信号量除了可以作为锁使用还可以用于将异步操作转为同步操作。

- (void)fetchDataComplete:(void(^)(id data, BOOL isSuccess))complete {
    // 模拟网络请求
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), self.queue, ^{
        NSArray * data = @[@"data"];
        complete(data, YES);
    });
}

- (id)syncFetchData {
    // 1.创建信号量,计数为0
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    __block id data = nil;
    [self fetchDataComplete:^(id d, BOOL isSuccess) {
        if (isSuccess) {
            data = d;
        }
        // 3.数据请求完成,发送 signal 信号计数 + 1 继续执行 2
        dispatch_semaphore_signal(semaphore);
    }];
    // 2.计数 <= 0 阻塞当前线程,等待异步请求回调
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    return data;
}
2.6 NSCondition
NSCondition * condition = [[NSCondition alloc] init];
// 加锁
[condition lock];
// 解锁
[condition unlock];

NSLock 基于 mutex 与 POSIX condition 实现,除了基础的 lock / unlock 还支持类似于 信号量 功能:

  • wait 释放互斥量,线程进入休眠状态。
  • waitUntilDate: 释放互斥量,当前线程立即进入休眠,其他线程继续执行任务,直到limit时间点,当前线程再被唤醒。
  • signal 唤醒一个等待的线程
  • broadcast 唤醒所有等待的线程

以上方法必须在 NSCondition 对象 lock 之后调用,例如下面的例子:

// 初始有两张票
static NSInteger ticket = 2;
static NSCondition * condition;
- (void)NSConditionTest {
    condition = [[NSCondition alloc] init];
    for (int i = 0; i < 5; i++) {
        dispatch_async(self.queue, ^{
            [self waitTicket];
        });
    }
    for (int i = 0; i < 3; i++) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * 3 * NSEC_PER_SEC)), self.queue, ^{
            [self signalTicket];
        });
    }
}

- (void)waitTicket {
    // 加锁
    [condition lock];
    while (!ticket) {
        NSLog(@"没有票,线程休眠");
        // 释放互斥量,线程休眠
        [condition wait];
        // 线程唤醒后继续while循环检查票数,如果没有票说明票已经被之前唤醒的线程售出,当前线程再次休眠
        NSLog(@"线程唤醒,检查票数");
    }
    ticket--;
    NSLog(@"卖出了一张票 剩余:%zd", ticket);
    // 解锁
    [condition unlock];
}

- (void)signalTicket {
    // 加锁
    [condition lock];
    ticket++;
    NSLog(@"发行了一张票 剩余:%zd", ticket);
    // 发送信号,通知唤醒一条休眠的线程
    [condition signal];
    // 解锁
    [condition unlock];
}

卖出了一张票 剩余:1
卖出了一张票 剩余:0
没有票,线程休眠
没有票,线程休眠
没有票,线程休眠
发行了一张票 剩余:1
线程唤醒,检查票数
卖出了一张票 剩余:0
发行了一张票 剩余:1
线程唤醒,检查票数
卖出了一张票 剩余:0
发行了一张票 剩余:1
线程唤醒,检查票数
卖出了一张票 剩余:0

如果将以上例子用 mutex 与 POSIX condition 实现如下:

static pthread_mutex_t mutex;
static pthread_cond_t cond;
- (void)mutexAndCondTest {
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);
    for (int i = 0; i < 5; i++) {
        dispatch_async(self.queue, ^{
            [self cond_waitTicket];
        });
    }
    for (int i = 0; i < 3; i++) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(i * 3 * NSEC_PER_SEC)), self.queue, ^{
            [self cond_signalTicket];
        });
    }
}

- (void)cond_waitTicket {
    pthread_mutex_lock(&mutex);
    while (!ticket) {
        NSLog(@"没有票,线程休眠");
        pthread_cond_wait(&cond, &mutex);
        NSLog(@"线程唤醒,检查票数");
    }
    ticket--;
    NSLog(@"卖出了一张票 剩余:%zd", ticket);
    pthread_mutex_unlock(&mutex);
}

- (void)cond_signalTicket {
    pthread_mutex_lock(&mutex);
    ticket++;
    NSLog(@"发行了一张票 剩余:%zd", ticket);
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);
}
2.7 NSConditionLock
// 初始化并设置条件变量
NSConditionLock * conditionLock = [[NSConditionLock alloc] initWithCondition:cond];
// 当条件变量相等且可以获取到锁时加锁,否则阻塞
[conditionLock lockWhenCondition:cond];
// 只要能够获取到锁就加锁,否则阻塞
// [conditionLock lock];
// code
// 解锁并重新设置条件变量
[conditionLock unlockWithCondition:cond];
// 解锁 不会改变当前条件变量
// [conditionLock unlock];

NSConditionLock 是对 NSCondition 的封装,在加锁前会比较等待变量和条件变量是否相等,如果不相等则阻塞线程

    // 初始化条件变量
    NSConditionLock * lock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(self.queue, ^{
        // 条件变量为 1 且能够获取到锁时加锁,否则阻塞
        [lock lockWhenCondition:1];
        NSLog(@"1");
        // 解锁并将条件变量设置为 2
        [lock unlockWithCondition:2];
    });
    dispatch_async(self.queue, ^{
        // 条件变量为 3 且能够获取到锁时时加锁,否则阻塞
        [lock lockWhenCondition:3];
        NSLog(@"2");
        // 解锁并将条件变量设置为 1
        [lock unlockWithCondition:1];
    });
    dispatch_async(self.queue, ^{
        // 无论条件变量为多少只要获取到锁就进行加锁,如果获取不到锁则阻塞线程
        [lock lock];
        NSLog(@"4");
        // 解锁且不改变条件变量
        [lock unlock];
    });
    dispatch_async(self.queue, ^{
        // 条件变量为 2 且能够获取到锁时时加锁,否则阻塞
        [lock lockWhenCondition:2];
        NSLog(@"3");
        // 解锁并将条件变量设置为 3
        [lock unlockWithCondition:3];
    });

执行顺序: 4 -> 3 -> 2 -> 1

3. 读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的CPU数
写者是排他性的,⼀个读写锁同时只能有⼀个写者或多个读者(与CPU数相关),但不能同时既有读者⼜有写者。在读写锁保持期间也是抢占失效的
如果读写锁当前没有读者,也没有写者,那么写者可以⽴刻获得读写锁,否则它必须⾃旋在那⾥,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以⽴即获得该读写锁,否则读者必须⾃旋在那⾥,直到写者释放该读写锁。

pthread_rwlock_t

// 导入头文件
#import <pthread/pthread.h>

pthread_rwlock_t lock;
// 初始化读写锁
pthread_rwlock_init(&lock, NULL);

// 读操作-加锁
pthread_rwlock_rdlock(&lock);
// 读操作-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写操作-加锁
pthread_rwlock_wrlock(&lock);
// 写操作-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁锁
pthread_rwlock_destroy(&lock);

读写锁的三种状态 :

  • 以读的方式占据锁的状态 :
    如果有其他的线程以读的方式请求占据锁,并读取锁内的共享资源,不会造成线程阻塞,允许其他线程进行读取,就像递归锁的可重入一样。
    如果有其他的线程以写的方式请求占据锁,企图更改锁内的共享资源,则会阻塞请求的线程,直到读的操作进行完毕。
    如果有其他多条线程,分别以读和写的不同方式请求占据锁,那么这些多条线程也会被阻塞,并且在当前线程读操作结束后,先让写方式的线程占据锁,避免读模式的锁长期占用资源,而写模式的锁却长期堵塞。
  • 以写的方式占据锁的状态 : 所有其他请求占据锁的线程都会阻塞。
  • 没有线程占据锁的状态 : 按照操作系统的调度顺序,依次调用,调度后要符合上述两种情况。

性能对比

性能

参考

Threading Programming Guide
【iOS】—— iOS中的相关锁
iOS线程安全——锁
iOS 多线程下的不同锁
iOS常用的几种锁详解以及用法
第三十二节—iOS的锁(一)
iOS GCD (四) dispatch_semaphore 信号量

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容