iOS 多线程之锁 Lock-线程安全

操作系统在管理普通线程时,通常采用时间片轮转算法(Round Robin,简称 RR)。每个线程会被分配一段时间片,通常在 10-100 毫秒左右。当线程用完属于自己的时间片以后,就会被操作系统挂起,放入等待队列中,直到下一次被分配时间片。

前面介绍的多线程方式GCDNSOperationQueue基本都要配合锁一起使用,比如多线程对属性的访问或者同一时间调用一段代码等都会需要锁来保证安全,下面 iOS 八种锁的性能对比图,图片来源 ibireme 的不再安全的 OSSpinLock

常用锁性能对比

下面介绍这几种锁的用法,OSSpinLock自旋锁因为不再安全,而且 APP 代码基本不会用到,所以不介绍。

dispatch_semaphore

GCD中的信号量,常用方法如下:

dispatch_semaphore_t dispatch_semaphore_create(long value);
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

create方法创建一个新的信号量,其中参数value为信号量的起始值, 不要传递小于零的值,否则返回 NULL,设为 1 就可以当作锁来用。当不再需要该信号量时,调用dispatch_release释放信号量
signal:信号量计数加1.
wait:信号量计数减1,如果信号量的值大于0,该方法所处线程就继续执行后面的代码,并且将信号量的值减1;否则,阻塞当前线程并等待timeout;如果在timeout 之前信号量的值被dispatch_semaphore_signal方法加1,那么就继续执行后面的代码并将信号量的值减1。如果等到timeout,其所处线程自动执行其后代码。
使用代码:

    _number = 1;
    _signal = dispatch_semaphore_create(1);
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(globalQueue, ^{
        [self funOne];
    });
    dispatch_async(globalQueue, ^{
        [self funOne];
    });

- (void)funOne {
    dispatch_semaphore_wait(_signal, DISPATCH_TIME_FOREVER);
    NSLog(@"funOne before run %zi",_number);
    [NSThread sleepForTimeInterval:0.5];
    NSLog(@"funOne end run %zi",_number);
    _number++;
    dispatch_semaphore_signal(_signal);
}

执行结果:

2018-11-16 14:42:40.723877+0800 YMultiThreadDemo[1938:261566] funOne before run 1
2018-11-16 14:42:41.224161+0800 YMultiThreadDemo[1938:261566] funOne end run 1
2018-11-16 14:42:41.224385+0800 YMultiThreadDemo[1938:261565] funOne before run 2
2018-11-16 14:42:41.729619+0800 YMultiThreadDemo[1938:261565] funOne end run 2

这样就保证funOne在多线程调用时,同一时间只在一个线程上执行,其他线程等待
signal。为什么要保证一段代码同一时间只在一个线程上被执行,下面实验买票过程,一共50张票,定时器模拟多人同时刷屏的情况,代码如下:

    _number = 0;
    _tickets = 50;
    dispatch_queue_t queue = dispatch_queue_create("com.yxw.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("com.yxw.queue2", DISPATCH_QUEUE_CONCURRENT);
    __weak typeof(self) wself = self;
    _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        dispatch_async(queue, ^{
            __strong typeof(wself) strongSelf = wself;
            [strongSelf buyTicket];
        });
    }];
    _timer2 = [NSTimer scheduledTimerWithTimeInterval:0.15 repeats:YES block:^(NSTimer * _Nonnull timer) {
        dispatch_async(queue2, ^{
            __strong typeof(wself) strongSelf = wself;
            [strongSelf buyTicket];
        });
    }];
    
- (void)buyTicket {
//    dispatch_semaphore_wait(_signal, DISPATCH_TIME_FOREVER);
    if (_tickets <= 0) {
        return;
    }
    NSLog(@"start buy ticket ");
    [NSThread sleepForTimeInterval:0.2];
    NSLog(@"buy ticket %zi",_tickets);
    _tickets --;
    if (_tickets <= 0) {
        NSLog(@"Tickets has been sold out");
        if ([_timer isValid]) {
            [_timer invalidate];
            [_timer2 invalidate];
        }
    }
    _number++;
    NSLog(@"end buy ticket run %zi",_number);
    
//    dispatch_semaphore_signal(_signal);
}

这里是没有加锁的代码,执行部分结果如下:

2018-11-16 16:02:59.620253+0800 YMultiThreadDemo[3207:346466] end buy ticket run 3
2018-11-16 16:02:59.669713+0800 YMultiThreadDemo[3207:346466] start buy ticket
2018-11-16 16:02:59.720036+0800 YMultiThreadDemo[3207:346464] start buy ticket
2018-11-16 16:02:59.720293+0800 YMultiThreadDemo[3207:346467] buy ticket 47
2018-11-16 16:02:59.720296+0800 YMultiThreadDemo[3207:346489] buy ticket 47
2018-11-16 16:02:59.720380+0800 YMultiThreadDemo[3207:346467] end buy ticket run 4
2018-11-16 16:02:59.720387+0800 YMultiThreadDemo[3207:346489] end buy ticket run 5
...
2018-11-16 16:03:02.619423+0800 YMultiThreadDemo[3207:346466] start buy ticket
2018-11-16 16:03:02.625722+0800 YMultiThreadDemo[3207:346464] buy ticket 1
2018-11-16 16:03:02.625892+0800 YMultiThreadDemo[3207:346464] Tickets has been sold out
2018-11-16 16:03:02.626038+0800 YMultiThreadDemo[3207:346464] end buy ticket run 51
2018-11-16 16:03:02.721501+0800 YMultiThreadDemo[3207:346489] buy ticket 0
2018-11-16 16:03:02.721501+0800 YMultiThreadDemo[3207:346467] buy ticket 0
2018-11-16 16:03:02.721702+0800 YMultiThreadDemo[3207:346489] Tickets has been sold out
2018-11-16 16:03:02.721702+0800 YMultiThreadDemo[3207:346467] Tickets has been sold out
2018-11-16 16:03:02.721787+0800 YMultiThreadDemo[3207:346489] end buy ticket run 52
2018-11-16 16:03:02.721822+0800 YMultiThreadDemo[3207:346467] end buy ticket run 53
2018-11-16 16:03:02.820595+0800 YMultiThreadDemo[3207:346466] buy ticket -2
2018-11-16 16:03:02.820792+0800 YMultiThreadDemo[3207:346466] Tickets has been sold out
2018-11-16 16:03:02.820892+0800 YMultiThreadDemo[3207:346466] end buy ticket run 54

会出现一张票卖出多次的情况,而且卖出的总票数多于50张,显然是不符合要求的,加上锁后(把信号量注释去掉)结果:

2018-11-16 16:09:35.791340+0800 YMultiThreadDemo[3275:352290] start buy ticket
2018-11-16 16:09:35.995282+0800 YMultiThreadDemo[3275:352290] buy ticket 50
2018-11-16 16:09:35.995452+0800 YMultiThreadDemo[3275:352290] end buy ticket run 1
2018-11-16 16:09:35.995600+0800 YMultiThreadDemo[3275:352636] start buy ticket
2018-11-16 16:09:36.198430+0800 YMultiThreadDemo[3275:352636] buy ticket 49
2018-11-16 16:09:36.198985+0800 YMultiThreadDemo[3275:352636] end buy ticket run 2
...
2018-11-16 16:09:45.761050+0800 YMultiThreadDemo[3275:352651] buy ticket 2
2018-11-16 16:09:45.761323+0800 YMultiThreadDemo[3275:352651] end buy ticket run 49
2018-11-16 16:09:45.761472+0800 YMultiThreadDemo[3275:352687] start buy ticket
2018-11-16 16:09:45.964220+0800 YMultiThreadDemo[3275:352687] buy ticket 1
2018-11-16 16:09:45.964416+0800 YMultiThreadDemo[3275:352687] Tickets has been sold out
2018-11-16 16:09:45.964561+0800 YMultiThreadDemo[3275:352687] end buy ticket run 50

这样就保证了不卖出相同的票以及多余的票

pthread_mutex

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

int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);

互斥锁的初始化方法,pthread_mutexattr_t参数用来设置属性,用法如下:

pthread_mutexattr_t attr;  
pthread_mutexattr_init(&attr);  
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);  // 定义锁的属性

其中settype有四个类型:

  • PTHREAD_MUTEX_NORMAL :默认值,普通锁,当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。如果线程在不解锁的情况下尝试重新锁定该互斥锁,则会产生死锁。尝试解除由其他线程锁定的互斥锁会产生不确定的行为。如果尝试解除锁定的互斥锁未锁定,则会产生不确定的行为。
  • PTHREAD_MUTEX_ERRORCHECK :检错锁,此类型的互斥锁可提供错误检查,上面NORMAL类型的三种情况都会返回错误,其他与普通锁类型动作相同。
  • PTHREAD_MUTEX_RECURSIVE: 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
  • PTHREAD_MUTEX_DEFAULT:iOS 下等同于PTHREAD_MUTEX_NORMAL

pthread_mutex_destroy ()用于注销一个互斥锁,要求锁当前处于解锁状态。
用下面代码测试:

static pthread_mutex_t mutex;//静态全局变量

    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);  // 定义锁的属性
    pthread_mutex_init(&mutex, &attr); // 创建锁
    [self funPerform:5];


- (void)funPerform:(NSInteger)num {
    NSInteger lockReturn = pthread_mutex_lock(&mutex);
    NSLog(@"lock return: %zi",lockReturn);
    if (num > 0) {
        NSLog(@"fun perform %zi",num);
        [self funPerform:num - 1];
    }
    pthread_mutex_unlock(&mutex);
}

上面代码执行正确,结果如下:

2018-11-19 16:09:15.806866+0800 YMultiThreadDemo[2562:316069] lock return: 0
2018-11-19 16:09:15.806946+0800 YMultiThreadDemo[2562:316069] fun perform 5
2018-11-19 16:09:15.814543+0800 YMultiThreadDemo[2562:316069] lock return: 0
2018-11-19 16:09:15.814908+0800 YMultiThreadDemo[2562:316069] fun perform 4
2018-11-19 16:09:15.815086+0800 YMultiThreadDemo[2562:316069] lock return: 0
2018-11-19 16:09:15.815173+0800 YMultiThreadDemo[2562:316069] fun perform 3
2018-11-19 16:09:15.815267+0800 YMultiThreadDemo[2562:316069] lock return: 0
2018-11-19 16:09:15.815350+0800 YMultiThreadDemo[2562:316069] fun perform 2
2018-11-19 16:09:15.815442+0800 YMultiThreadDemo[2562:316069] lock return: 0
2018-11-19 16:09:15.815528+0800 YMultiThreadDemo[2562:316069] fun perform 1
2018-11-19 16:09:15.815616+0800 YMultiThreadDemo[2562:316069] lock return: 0

如果锁类型改为PTHREAD_MUTEX_NORMAL,则死锁(如果加锁方法改为pthread_mutex_trylock(&mutex);,则返回16,表示EBUSY);如果改为PTHREAD_MUTEX_ERRORCHECK,能执行,但是pthread_mutex_lock返回11,表示EDEADLK,其定义为

#define EDEADLK     11      /* Resource deadlock avoided */
#define EBUSY       16      /* Device / Resource busy */

NSLock

NSLock 是以对象的形式暴露给开发者的一种锁,在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示。加解锁需要在同一线程, 从不同的线程加解锁可能导致未定义的行为。其属性和方法如下:

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name;

@end

lockunlocktryLock方法都与pthread_mutex中的对应方法类似。
lockBeforeDate:尝试在给定时间之前获取锁定并返回指示尝试是否成功的布尔值。

NSCondition

NSCondition 的底层是通过条件变量pthread_cond_t来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程继续执行。NSCondition属性和方法如下:

@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;

@property (nullable, copy) NSString *name;

@end

wait:阻塞当前线程,直到condition发出信号。必须在调用此方法之前加锁。
waitUntilDate:相对于wait,多加了个时间限制,如果到达时间但是condition还尚未发出信号,则唤醒线程,继续执行。
signal:发出状态信号,唤醒等待它的第一个线程。
broadcast:广播信号,唤醒等待它的所有线程。如果没有线程在等待该条件,则此方法不执行任何操作。
使用情况:比如一个任务,要等另一个线程保存文件完成后执行,模拟代码如下:

    __weak typeof(self) wself = self;
    dispatch_queue_t queue3 = dispatch_queue_create("com.yxw.queue3", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue4 = dispatch_queue_create("com.yxw.queue4", DISPATCH_QUEUE_CONCURRENT);
    NSCondition *condition = [[NSCondition alloc] init];
    dispatch_async(queue3, ^{
        [condition lock];
        if (!wself.writeCompleted) {
            [condition wait];
        }
        NSLog(@"write completed after");
        [condition unlock];
    });
    dispatch_async(queue4, ^{
        NSLog(@"write begin");
        [NSThread sleepForTimeInterval:2.];
        wself.writeCompleted = YES;
        NSLog(@"write completed");
        [condition signal];
    });

执行结果如下:

2018-11-20 11:34:06.575478+0800 YMultiThreadDemo[1556:174183] write begin
2018-11-20 11:34:08.581873+0800 YMultiThreadDemo[1556:174183] write completed
2018-11-20 11:34:08.582089+0800 YMultiThreadDemo[1556:174182] write completed after

其中,if (!wself.writeCompleted)语句可以改为while (!self.writeCompleted),因为[condition wait]会阻塞当前线程,所以while不是一直在运行,不会浪费 CPU 资源;与if不同的是,为 if时,其他线程只要收到signal就直接执行后面的代码,不会再判断self.writeCompleted的值,while则会继续判断,条件不满足就继续wait,看需求使用。

NSRecursiveLock

递归锁,也是通过 pthread_mutex_lock 函数来实现,内部封装的 pthread_mutex_t 对象的类型为PTHREAD_MUTEX_RECURSIVE。使用方法跟前面pthread_mutex_t的递归模式一样。

@interface NSRecursiveLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name;

@end

NSConditionLock

条件锁,可以与特定的用户定义条件相关联的锁,借助 NSCondition 来实现。使用NSConditionLock对象,可以确保线程只有在满足特定条件时才能获取锁。 一旦获得锁并执行代码的关键部分,线程就可以放弃锁并将相关条件设置为新的。 条件本身是任意的:可以根据应用需要定义它们。NSConditionLock属性方法如下:

@interface NSConditionLock : NSObject <NSLocking> {
@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;

@property (nullable, copy) NSString *name;

@end

lockWhenCondition:在锁定操作成功之前,接收器的条件必须等于传入的条件。 此方法阻止线程的执行,直到可以获取锁定。使用此方法加锁代码,由unlock方法解锁无效。
unlockWithCondition:解锁并把属性condition修改为传入的值。与lockWhenCondition配合使用。
使用情况,比如有三个任务的执行顺序需要三个条件来确定,模拟代码如下:

    _conditionNum = 1;
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    dispatch_async(queue3, ^{
        [NSThread sleepForTimeInterval:0.5];
        [conditionLock lockWhenCondition:1];
        NSLog(@"condition lock task: %zi",wself.conditionNum);
        [NSThread sleepForTimeInterval:1.];
        wself.conditionNum ++;
        [conditionLock unlockWithCondition:2];
    });
    dispatch_async(queue4, ^{
        [conditionLock lockWhenCondition:2];
        NSLog(@"condition lock task: %zi",wself.conditionNum);
        [NSThread sleepForTimeInterval:2.];
        wself.conditionNum ++;
        [conditionLock unlockWithCondition:3];
    });
    dispatch_async(globalQueue, ^{
        [conditionLock lockWhenCondition:3];
        NSLog(@"condition lock task: %zi",wself.conditionNum);
        [NSThread sleepForTimeInterval:2.];
        wself.conditionNum ++;
        [conditionLock unlockWithCondition:4];
    });

执行结果如下:

2018-11-20 15:29:37.196991+0800 YMultiThreadDemo[3719:336744] condition lock task: 1
2018-11-20 15:29:38.202500+0800 YMultiThreadDemo[3719:336745] condition lock task: 2
2018-11-20 15:29:40.203211+0800 YMultiThreadDemo[3719:336746] condition lock task: 3

@synchronized

使用最简单,通过牺牲性能换来语法上的简洁与可读。实现上是把这个对象当做锁来使用。这是通过一个哈希表来实现的,OC 在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值来得到对应的互斥锁。
使用代码如下:

   dispatch_async(globalQueue, ^{
        @synchronized (wself) {
            NSLog(@"synchronized run 1 begin");
            [NSThread sleepForTimeInterval:2.];
            NSLog(@"synchronized run 1 end");
        }
    });
    dispatch_async(globalQueue, ^{
        @synchronized (wself) {
            NSLog(@"synchronized run 2");
        }
    });

执行结果如下:

2018-11-20 15:51:14.517625+0800 YMultiThreadDemo[3952:363020] synchronized run 1 begin
2018-11-20 15:51:16.521971+0800 YMultiThreadDemo[3952:363020] synchronized run 1 end
2018-11-20 15:51:16.522215+0800 YMultiThreadDemo[3952:363021] synchronized run 2

参考文章:
不再安全的 OSSpinLock
深入理解 iOS 开发中的锁
关于 @synchronized,这儿比你想知道的还要多

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

推荐阅读更多精彩内容