iOS-多线程-锁

多线程需要一种互斥的机制来访问共享资源。

一、 互斥锁

互斥锁的意思是某一时刻只允许一个线程访问某一资源。为了保证这一点,每个想要访问共享资源的线程,需要首先获得一个共享资源的互斥锁,一旦某个线程对共享资源完成了访问,就释放掉这个互斥锁,这样别的线程就有机会获取互斥锁,然后访问该共享资源了。

一般情况下,一个线程只能申请一次锁,也只能在获得锁的情况下才能释放锁,多次申请锁释放未获得的锁都会导致崩溃。假设在已经获得锁的情况下再次申请锁,线程会因为等待锁的释放而进入睡眠状态,因此就不可能再释放锁,从而导致死锁

互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。

1. pthread_mutex

由于 pthread_mutex 有多种类型,可以支持递归锁等,因此在申请加锁时,需要对锁的类型加以判断,这也就是为什么它和信号量的实现类似,但效率略低的原因。

如果已经得到锁,再次申请锁,会导致死锁。然而这种情况经常会发生,比如某个函数申请了锁,在临界区内又递归调用了自己。辛运的是 pthread_mutex 支持递归锁,也就是允许一个线程递归的申请锁,只要把 attr 的类型改成 PTHREAD_MUTEX_RECURSIVE 即可。


712028-c2d5d99ae4fb9cfc.png

2. NSLock

NSLock只是在内部封装了一个pthread_mutex,属性为PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。这里使用宏定义的原因是,OC 内部还有其他几种锁,他们的 lock 方法都是一模一样,仅仅是内部pthread_mutex
互斥锁的类型不同。通过宏定义,可以简化方法的定义。
NSLock比pthread_mutex略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响。

//设置票的数量为5
    _tickets = 5;

    //创建锁
    _mutexLock = [[NSLock alloc] init];

    //线程1
    dispatch_async(self.concurrentQueue, ^{
        [self saleTickets];
    });

    //线程2
    dispatch_async(self.concurrentQueue, ^{
        [self saleTickets];
    });

- (void)saleTickets
{

    while (1) {
        [NSThread sleepForTimeInterval:1];
        //加锁
        [_mutexLock lock];
        if (_tickets > 0) {
            _tickets--;
            NSLog(@"剩余票数= %ld, Thread:%@",_tickets,[NSThread currentThread]);        
        } else {
            NSLog(@"票卖完了  Thread:%@",[NSThread currentThread]);
            break;
        }
        //解锁
        [_mutexLock unlock];
    }
}

性能与使用场景
pthread_mutex是pthread经典的基于互斥量机制的同步锁,特性、性能以及稳定各方面都已被大量项目所验证,也是比较推荐作为常规同步锁首选

二、自旋锁OSSpinLock

上述文章中已经介绍了 OSSpinLock 不再安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,一直循环,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。

原理,通过一个全局变量和申请锁的原子操作。

然而在多处理器的情况下,能够被多个处理器同时执行的操作任然算不上原子操作。因此,真正的原子操作必须由硬件提供支持,比如 x86 平台上如果在指令前面加上 “LOCK” 前缀,对应的机器码在执行时会把总线锁住,使得其他 CPU不能再执行相同操作,从而从硬件层面确保了操作的原子性。
这些非常底层的概念无需完全掌握,我们只要知道上述申请锁的过程,可以用一个原子性操作 test_and_set 来完成。

实际使用:

#import <libkern/OSAtomic.h>
@interface ViewController ()
{
    OSSpinLock spinlock;
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.number = 10;
    spinlock = OS_SPINLOCK_INIT;
}
- (IBAction)test:(id)sender {
    for (int i = 0; i<10; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
             [self sellTicket];
        });
    }
}
- (void)sellTicket {
    OSSpinLockLock(&spinlock);
    
    if (self.number > 0) {
        self.number--;
        NSLog(@"%@还剩%ld张票",[NSThread currentThread],self.number);
    }
    
    OSSpinLockUnlock(&spinlock);
}
@end

使用场景
如果临界区的执行时间过长,使用自旋锁不是个好主意,自旋锁适合短时间的操作,加锁性能最快,但不能使用不同优先级。

三、信号量

不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。

缺点
在时间较短的操作,没有自旋锁高效,会有上下文切换的成本。
优点
效率高。

四、条件锁

1. NSCondition

NSCondition 其实是封装了一个互斥锁条件变量。NSCondition 的底层是通过条件变量(condition variable) pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程。它仅仅是控制了线程的执行顺序。

互斥锁提供线程安全,条件变量提供线程阻塞与信号机制。

它的基本用法和NSLock一样,这里说一下NSCondition的特殊用法。
NSCondition提供更高级的用法,方法如下:

- (void)wait; //阻塞当前线程 直到等待唤醒 
- (BOOL)waitUntilDate:(NSDate *)limit;  //阻塞当前线程到一定时间 之后自动唤醒
- (void)signal; //唤醒一条阻塞线程
- (void)broadcast; //唤醒所有阻塞线程

2. NSConditionLock

借助 NSCondition 来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容。NSConditionLock 的内部持有一个 NSCondition 对象,以及 _condition_value 属性,在初始化时就会对这个属性进行赋值:

// 简化版代码
- (id) initWithCondition: (NSInteger)value {
    if (nil != (self = [super init])) {
        _condition = [NSCondition new]
        _condition_value = value;
    }
    return self;
}

它的 lockWhenCondition 方法其实就是消费者方法:

- (void) lockWhenCondition: (NSInteger)value {
    [_condition lock];
    while (value != _condition_value) {
        [_condition wait];
    }
}

对应的 unlockWhenCondition 方法则是生产者,使用了 broadcast 方法通知了所有的消费者:

- (void) unlockWithCondition: (NSInteger)value {
    _condition_value = value;
    [_condition broadcast];
    [_condition unlock];
}

具体使用:

//主线程中
    NSConditionLock *theLock = [[NSConditionLock alloc] init];

    //线程1
    dispatch_async(self.concurrentQueue, ^{
        for (int i=0;i<=3;i++)
        {
            [theLock lock];
            NSLog(@"thread1:%d",i);
            sleep(1);
            [theLock unlockWithCondition:i];
        }
    });

    //线程2
    dispatch_async(self.concurrentQueue, ^{
        [theLock lockWhenCondition:2];
        NSLog(@"thread2");
        [theLock unlock];
    });

五、递归锁NSRecursiveLock

上文已经说过,递归锁也是通过 pthread_mutex_lock
函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。
NSRecursiveLock
与 NSLock
的区别在于内部封装的 pthread_mutex_t
对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE

多次调用不会阻塞已获取该锁的线程,不会死锁。
实际使用:

// 实例类person
Person *person = [[Person alloc] init];
// 创建锁对象
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];

// 创建递归方法
static void (^testCode)(int);
testCode = ^(int value) {
    [theLock tryLock];
    if (value > 0)
    {
        [person personA];
        [NSThread sleepForTimeInterval:1];
        testCode(value - 1);
    }
    [theLock unlock];
};

//线程A
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    testCode(5);
});

//线程B
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [theLock lock];
    [person personB];
    [theLock unlock];
});

六、@synchronized

这其实是一个 OC 层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读。
我们知道 @synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做锁来使用。你调用 sychronized 的每个对象,Objective-C runtime 都会为其分配一个递归锁并存储在哈希表中。OC 在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象地址哈希值来得到对应的互斥锁。

若是在self对象上频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码,这样做其实并没有必要。
使用场景:创建单例时使用。

综合上述分析与讨论,总结有以下几点原则:

1、总的来看,推荐pthread_mutex作为实际项目的首选方案;
2、对于耗时较大又易冲突的读操作,可以使用读写锁代替pthread_mutex;
3、如果确认仅有set/get的访问操作,可以选用原子操作属性;
4、对于性能要求苛刻,可以考虑使用OSSpinLock,需要确保加锁片段的耗时足够小;
5、条件锁基本上使用面向对象的NSCondition和NSConditionLock即可;
6、@synchronized则适用于低频场景如初始化或者紧急修复使用;

1.自旋锁:OSSpinLock 在ios中已经不是线程安全的了,如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。(效率最高,如果一直等不到锁会较占用cpu资源)

2.信号量:dispatch_semaphore是gcd中通过信号量来实现共享数据的数据安全。(效率第二)

3.互斥锁:pthread_mutex ,nslock ,synchronized都是互斥锁。如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。(synchronized效率最低)

4.递归锁:pthread_mutex(recursive)与NSRecursiveLock , 多次调用不会阻塞已获取该锁的线程。

5.条件锁:nsconditionlock 满足一定的条件的加锁和解锁,可以实现依赖关系。nscondition条件锁,也是通过信号来解锁,主要用来实现生产者消费者模式。

七、我们平时使用的:

1. @synchronized,一般用在创建单例的时候。
2. atomic修饰属性的关键字,它不是绝对安全的。
3. 一般使用NSLock即可,但是如果方法会有递归调用则会死锁
,这时我们使用递归锁:
4. OSSpinLock自旋锁,轮询的方式,用于轻量级的数据操作+1/-1。
5. 信号量:dispatch_semaphore是gcd中通过信号量来实现共享数据的数据安全。(效率第二)

资料:
[iOS]深入理解并发--锁
深入理解 iOS 开发中的锁
iOS开发-多线程开发之线程安全篇
iOS 中几种常用的锁总结
iOS中的5种锁

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

推荐阅读更多精彩内容

  • iOS线程安全的锁与性能对比 一、锁的基本使用方法 1.1、@synchronized 这是我们最熟悉的枷锁方式,...
    Jacky_Yang阅读 2,198评论 0 17
  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 641评论 0 0
  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,503评论 0 6
  • demo下载 建议一边看文章,一边看代码。 声明:关于性能的分析是基于我的测试代码来的,我也看到和网上很多测试结果...
    炸街程序猿阅读 768评论 0 2
  • 前言 iOS开发中由于各种第三方库的高度封装,对锁的使用很少,刚好之前面试中被问到的关于并发编程锁的问题,都是一知...
    喵渣渣阅读 3,669评论 0 33