关于 iOS 中各种锁的整理

名词解释

原子:

同一时间只允许一个线程访问

临界区:

指的是一块对公共资源进行访问的代码,并非一种机制或是算法。

自旋锁:

是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

互斥锁(Mutex):

是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。
当线程来到临界区,获取不到锁,就会去睡眠主动让出时间片,让出时间片会导致操作系统切换到另一个线程,这种上下文的切换也耗时

读写锁:

是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。
读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。

信号量(semaphore):

是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。

条件锁:

就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。
当资源被分配到了,条件锁打开,进程继续运行。

递归锁:

递归锁有一个特点,就是同一个线程可以加锁N次而不会引发死锁。


互斥锁 :

互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。

1. NSLock

是 Foundation 框架中以对象形式暴露给开发者的一种锁(Foundation框架同时提供了NSConditionLock,NSRecursiveLock,NSCondition)
NSLock 内部封装了 pthread_mutex 属性为 PTHREAD_MUTEX_ERRORCHECK 它会损失一定的性能来换错误提示。
NSLock 比 pthread_mutex 要慢,因为他还要经过方法调用,但是有缓存多次调用影响不大

NSLock定义如下:

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
@interface NSLock : NSObject  {
@private
    void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end

lock 和 tryLock 方法都会请求加锁, 唯一不同的是 trylock 在没有获得锁的时候可以继续做一些任务和处理,lockBeforeDate方法也比较简单,就是在limit时间点之前获得锁,没有拿到返回NO。

2. pthread_mutex :

pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API,pthread_mutex 表示互斥锁。
互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换,性能不及信号量。

// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(&(_ticketMutex), &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);

/*
* Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL        0
#define PTHREAD_MUTEX_ERRORCHECK    1 // NSLock 使用
#define PTHREAD_MUTEX_RECURSIVE        2 // 递归锁
#define PTHREAD_MUTEX_DEFAULT        PTHREAD_MUTEX_NORMAL

备注:我们可以不初始化属性,在传属性的时候直接传NULL,表示使用默认属性 PTHREAD_MUTEX_NORMALpthread_mutex_init(mutex, NULL);

3. @synchronized :

@synchronized要一个参数,这个参数相当于信号量

// 用在防止多线程访问属性上比较多
- (void)setTestInt:(NSInteger)testInt {
    @synchronized (self) {
        _testInt = testInt;
    }
}

自旋锁 :

实现原理 : 保护临界区只有一个线程可以访问
伪代码 :

do {  
    Acquire Lock  // 获取锁
        Critical section  // 临界区
    Release Lock  // 释放锁        
        Reminder section // 不需要锁保护的代码
}

实现思路很简单,理论上定义一个全局变量,用来表示锁的状态即可

bool lock = false; // 一开始没有锁上,任何线程都可以申请锁  
do {  
    while(lock); // 如果 lock 为 true 就一直死循环,相当于申请锁
    lock = true; // 挂上锁,这样别的线程就无法获得锁
        Critical section  // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
        Reminder section // 不需要锁保护的代码        
}

有一个问题就是一开始有多个线程执行 while 循环, 他们都不会在这里卡住,而是继续执行,这样就无法保证锁的可靠性了,解决思路很简单,就是确保申请锁的过程是原子的。

bool lock = false; // 一开始没有锁上,任何线程都可以申请锁  
do {  
    while(test_and_set(&lock); // test_and_set 是一个原子操作
        Critical section  // 临界区
    lock = false; // 相当于释放锁,这样别的线程可以进入临界区
        Reminder section // 不需要锁保护的代码        
}
如过临界区执行时间过长,使用自旋锁是不合适的。忙等的线程白白占用 CPU 资源。
1. OSSpinLock :

编译器会报警告,大家已经不使用了,在某些场景下已经不安全了,主要是发生在低优先级的线程拿到锁时,高优先级线程进入忙等状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁,这被称为优先级反转

新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。
高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。
这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。

2. os_unfair_lock:

os_unfair_lock 是苹果官方推荐的替换OSSpinLock的方案,但是它在iOS10.0以上的系统才可以调用,解决了优先级反转问题

两种自旋锁的使用
// 需要导入的头文件
#import <libkern/OSAtomic.h>
#import <os/lock.h>
#import <AddressBook/AddressBook.h>
// 自旋锁 实现
- (void)OSSpinLock {
    if (@available(iOS 10.0, *)) {  // iOS 10以后解决了优先级反转问题
        
        os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT);
        NSLog(@"线程1 准备上锁");
        os_unfair_lock_lock(unfairLock);
        sleep(4);
        NSLog(@"线程1执行");
        os_unfair_lock_unlock(unfairLock);
        NSLog(@"线程1 解锁成功");
    } else { // 会造成优先级反转,不建议使用
        __block OSSpinLock oslock = OS_SPINLOCK_INIT;
        
        //线程2
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
            NSLog(@"线程2 befor lock");
            OSSpinLockLock(&oslock);
            NSLog(@"线程2执行");
            sleep(3);
            OSSpinLockUnlock(&oslock);
            NSLog(@"线程2 unlock");
        });
        
        //线程1
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            NSLog(@"线程1 befor lock");
            OSSpinLockLock(&oslock);
            NSLog(@"线程1 sleep");
            sleep(3);
            NSLog(@"线程1执行");
            OSSpinLockUnlock(&oslock);
            NSLog(@"线程1 unlock");
        });
        
        // 可以看出不同的队列优先级,执行的顺序不同,优先级越高,越早被执行
    }
}

读写锁:

上文有说到,读写锁又称共享-互斥锁

1. pthread_rwlock:

pthread_rwlock经常用于文件等数据的读写操作,需要导入头文件#import <pthread.h>
iOS中的读写安全方案需要注意一下场景

  • 同一时间,只能有1个线程进行写的操作
  • 同一时间,允许有多个线程进行读的操作
  • 同一时间,不允许既有写的操作,又有读的操作
//初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&_lock, NULL);

//读加锁
pthread_rwlock_rdlock(&_lock);
//读尝试加锁
pthread_rwlock_trywrlock(&_lock)

//写加锁
pthread_rwlock_wrlock(&_lock);
//写尝试加锁
pthread_rwlock_trywrlock(&_lock)

//解锁
pthread_rwlock_unlock(&_lock);
//销毁
pthread_rwlock_destroy(&_lock);

用法:实现多读单写

#import <pthread.h>
@interface pthread_rwlockDemo ()
@property (assign, nonatomic) pthread_rwlock_t lock;
@end

@implementation pthread_rwlockDemo

- (instancetype)init
{
    self = [super init];
    if (self) {
        // 初始化锁
        pthread_rwlock_init(&_lock, NULL);
    }
    return self;
}

- (void)otherTest{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            [self read];
        });
        dispatch_async(queue, ^{
            [self write];
        });
    }
}
- (void)read {
    pthread_rwlock_rdlock(&_lock);
    sleep(1);
    NSLog(@"%s", __func__);
    pthread_rwlock_unlock(&_lock);
}
- (void)write
{
    pthread_rwlock_wrlock(&_lock);
    sleep(1);
    NSLog(@"%s", __func__);
    pthread_rwlock_unlock(&_lock);
}
- (void)dealloc
{
    pthread_rwlock_destroy(&_lock);
}
@end



递归锁:

递归锁有一个特点,就是同一个线程可以加锁N次而不会引发死锁。

1. pthread_mutex(recursive):

pthread_mutex_t锁是默认是非递归的。可以通过设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t锁设置为递归锁。

pthread_mutex_t lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
2. NSRecursiveLock:

NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致

#import "RecursiveLockDemo.h"
@interface RecursiveLockDemo()
@property (nonatomic,strong) NSRecursiveLock *ticketLock;
@end
@implementation RecursiveLockDemo
//卖票
- (void)sellingTickets{
[self.ticketLock lock];
[super sellingTickets];
[self.ticketLock unlock];
}
@end

条件锁:

1. NSCondition:

定义:

@interface NSCondition : NSObject  {
@private
    void *_priv;
}
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

遵循NSLocking协议,使用的时候同样是lock,unlock加解锁,wait是傻等,waitUntilDate:方法是等一会,都会阻塞掉线程,signal是唤起一个在等待的线程,broadcast是广播全部唤起。

NSCondition *lock = [[NSCondition alloc] init];
//Son 线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [lock lock];
    while (No Money) {
        [lock wait];
    }
    NSLog(@"The money has been used up.");
    [lock unlock];
});

 //Father线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [lock lock];
    NSLog(@"Work hard to make money.");
    [lock signal];
    [lock unlock];
 });
2.NSConditionLock:

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
定义:

@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;

用法 :

@interface NSConditionLockDemo()
@property (strong, nonatomic) NSConditionLock *conditionLock;
@end
@implementation NSConditionLockDemo
- (instancetype)init
{
if (self = [super init]) {
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
}
return self;
}

- (void)otherTest
{
[[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
}

- (void)__one
{
[self.conditionLock lock];
NSLog(@"__one");
sleep(1);
[self.conditionLock unlockWithCondition:2];
}

- (void)__two
{
[self.conditionLock lockWhenCondition:2];
NSLog(@"__two");
[self.conditionLock unlockWithCondition:3];
}
@end

里面有三个常用的方法

* 1、- (instancetype)initWithCondition:(NSInteger)condition;  //初始化Condition,并且设置状态值
* 2、- (void)lockWhenCondition:(NSInteger)condition;   //当状态值为condition的时候加锁
* 3、- (void)unlockWithCondition:(NSInteger)condition;   //当状态值为condition的时候解锁

信号量 dispatch_semaphore:

在加锁的过程中,如过线程 1 已经获取了锁,并在执行任务过程中,那么其他线程会被阻塞,直到线程 1 任务结束后完成释放锁。

实现原理 :
信号量的 wait 最终调用到这里

int sem_wait (sem_t *sem) {  
  int *futex = (int *) sem;
  if (atomic_decrement_if_positive (futex) > 0)
    return 0;
  int err = lll_futex_wait (futex, 0);
    return -1;
)

首先把信号值减一,并判断是否大于 0,如过大于 0 说明不用等待,立即返回。
否则线程进入睡眠主动让出时间片,让出时间片会导致操作系统切换到另一个线程,这种上下文的切换也耗时,大概 10 微妙,而且还要切回来,如过等待时间很短,那么等待耗时还没有切换耗时长,很不划算。

自旋锁和信号量的实现简单,所以加锁和解锁的效率高


总结

其实本文写的都是一些再基础不过的内容,在平时阅读一些开源项目的时候经常会遇到一些保持线程同步的方式,因为场景不同可能选型不同,这篇就做一下简单的记录吧~我相信读完这篇你应该能根据不同场景选择合适的锁了吧、能够道出自旋锁和互斥锁的区别了吧。

性能排序:
1、os_unfair_lock
2、OSSpinLock
3、dispatch_semaphore
4、pthread_mutex
5、dispatch_queue(DISPATCH_QUEUE_SERIAL)
6、NSLock
7、NSCondition
8、pthread_mutex(recursive)
9、NSRecursiveLock
10、NSConditionLock
11、@synchronized

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