iOS 多线程之线程安全

线程安全问题

在单线程的情形下,任务依次串行执行是不存在线程安全问题的。在单线程的情形下,如果多线程都是访问共享资源而不去修改共享资源也可以保证线程安全,比如:设置只读属性的全局变量。线程不安全是由于多线程访问造成的,是由于多线程访问和修改共享资源而引起不可预测的结果。而线程锁可以有效的解决线程安全问题,大致过程如下图:

无线程锁
加线程锁

iOS 多线程开发中为保证线程安全而常用的几种锁:NSLock、dispatch_semaphore、NSCondition、NSRecursiveLock、NSConditionLock、@synchronized,这几种锁各有优点,适用于不同的场景,下面我们就来依次介绍一下。

1. NSLock

NSLock 是OC层封装底层线程操作来实现的一种锁,继承NSLocking协议,在此我们不讨论各种锁的实现细节,因为基本用不到。NSLock使用非常简单:

NSLock *lock = [NSLock alloc] init];

// 加锁
[lock lock];

/*
* 被加锁的代码区间
*/

// 解锁
[lock Unlock];

我们以车站购票为例子,多个窗口同时售票,每个窗口有人循环购票:

// 定义NSLock变量
@property (nonatomic, strong) NSLock *lock;
// 实例化
_lock = [[NSLock alloc] init];

/*******************************************************************************/

// 调用测试方法
dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSLock];
        });
    }

/*******************************************************************************/

// 测试方法
- (void)testNSLock {
    
    while (1) {
        [_lock lock];
        if (_ticketCount > 0) {
            _ticketCount --;
            NSLog(@"--->> %@已购票1张,剩余%ld张", [NSThread currentThread], (long)_ticketCount);
        }
        else {
            [_lock unlock];
            return;
        }
        [_lock unlock];
        sleep(0.2);
    }
}
2. dispatch_semaphore

dispatch_semaphore 是 GCD 提供的,使用信号量来控制并发线程的数量(可同时进入并执行加锁代码块的线程的数量),相关的三个函数:

// 创建信号量
dispatch_semaphore_create(long value); 

//等待信号
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

发送信号
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
//! 定义信号量semaphore
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
//! 实例化
_semaphore = dispatch_semaphore_create(1);

/*******************************************************************************/

// 调用测试方法
- (void)multiThread {
    
    dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    for (NSInteger i=0; i<2; i++) {
        dispatch_async(queue, ^{
            [self testDispatchSemaphore:i];
        });
    }
}

/*******************************************************************************/

// 测试方法
- (void)testDispatchSemaphore:(NSInteger)num {
    
    while (1) {
        // 参数1为信号量;参数2为超时时间;ret为返回值
        //dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
        long ret = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.21*NSEC_PER_SEC)));
        if (ret == 0) {
            if (_ticketCount > 0) {
                NSLog(@"%d 窗口 卖了第%d张票", (int)num, (int)_ticketCount);
                _ticketCount --;
            }
            else {
                dispatch_semaphore_signal(_semaphore);
                NSLog(@"%d 卖光了", (int)num);
                break;
            }
            [NSThread sleepForTimeInterval:0.2];
            dispatch_semaphore_signal(_semaphore);
        }
        else {
            NSLog(@"%d %@", (int)num, @"超时了");
        }
        
        [NSThread sleepForTimeInterval:0.2];
    }
}

当第一各参数semaphore取值为1时,dispatch_semaphore_wait(semaphore, timeout)与dispatch_semaphore_signal(signal)成对出现,所达到的效果就跟NSLock中的lock和unlock是一样的。区别在于当semaphore取值为n时,则可以有n个线程同时访问被保护的临界区,即可以控制多个线程并发。第二个参数为dispatch_time_t类型,如果直接输入一个非dispatch_time_t的值会导致dispatch_semaphore_wait方法偶尔返回非0值。

3.NSCondition

NSCondition 常用于生产者-消费者模式,它继承于NSLocking协议,同样有lock和unlock方法。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待数据就绪,再唤醒线程。

NSCondition *lock = [[NSCondition alloc] init];

//线程A
[lock lock];

[lock wait]; // 线程被挂起

[lock unlock];

//线程2
sleep(1);//以保证让线程2的代码后执行

[lock lock];

[lock signal]; // 唤醒线程1

[lock unlock];

我们执行了两次for循环,起了两批新线程,一批来add数据,另一批来remove数据。其中add数据方法加锁,remove数据方法也加了锁:

// 定义变量
@property (nonatomic, strong) NSCondition *condition;
// 实例化
_condition = [[NSCondition alloc] init];

/*******************************************************************************/

// 调用测试方法
- (void)multiThread {
    
    dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionAdd];
        });
    }

    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionRemove];
        });
    }
}

/*******************************************************************************/

// 测试方法
- (void)testNSConditionAdd {
    
    [_condition lock];
    
    // 生产数据
    NSObject *object = [NSObject new];
    [_ticketsArr addObject:object];
    NSLog(@"--->>%@ add", [NSThread currentThread]);
    [_condition signal];
    
    [_condition unlock];
}

- (void)testNSConditionRemove {
    
    [_condition lock];
    
    // 消费数据
    if (!_ticketsArr.count) {
        NSLog(@"--->> wait");
        [_condition wait];
    }
    [_ticketsArr removeObjectAtIndex:0];
    NSLog(@"--->>%@ remove", [NSThread currentThread]);
    
    [_condition unlock];
}
4.NSConditionLock

NSConditionLock 为条件锁,lockWhenCondition:方法是当condition参数与初始化时候的 condition 相等时才可加锁。而unlockWithCondition:方法并不是当 Condition 符合条件时才解锁,而是解锁之后,修改 Condition 的值。NSConditionLock 借助 NSCondition 来实现,它的本质就是一个生产者-消费者模型。“条件被满足”可以理解为生产者提供了新的内容NSConditionLock 的内部持有一个 NSCondition 对象,以及 _condition_value 属性,在初始化时就会对这个属性进行赋值:

// 设置条件
#define CONDITION_NO_DATA   100
#define CONDITION_HAS_DATA  101

/*******************************************************************************/

// 初始化条件锁对象
@property (nonatomic, strong) NSConditionLock *conditionLock;
// 实例化
_conditionLock = [[NSConditionLock alloc] initWithCondition:CONDITION_NO_DATA];

/*******************************************************************************/

// 调用测试方法
- (void)multiThread {
    
    dispatch_queue_t queue = dispatch_queue_create("QiMultiThreadSafeQueue", DISPATCH_QUEUE_CONCURRENT);
    
    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionLockAdd];
        });
    }

    for (NSInteger i=0; i<10; i++) {
        dispatch_async(queue, ^{
            [self testNSConditionLockRemove];
        });
    }
}

/*******************************************************************************/

// 测试方法
- (void)testNSConditionLockAdd {
    
    // 满足CONDITION_NO_DATA时,加锁
    [_conditionLock lockWhenCondition:CONDITION_NO_DATA];
    
    // 生产数据
    NSObject *object = [NSObject new];
    [_ticketsArr addObject:object];
    NSLog(@"---->>%@ add", [NSThread currentThread]);
    [_condition signal];
    
    // 有数据,解锁并设置条件
    [_conditionLock unlockWithCondition:CONDITION_HAS_DATA];
}

- (void)testNSConditionLockRemove {
    
    // 有数据时,加锁
    [_conditionLock lockWhenCondition:CONDITION_HAS_DATA];
    
    // 消费数据
    if (!_ticketsArr.count) {
        NSLog(@"---->> wait");
        [_condition wait];
    }
    [_ticketsArr removeObjectAtIndex:0];
    NSLog(@"---->>%@ remove", [NSThread currentThread]);
    
    //3. 没有数据,解锁并设置条件
    [_conditionLock unlockWithCondition:CONDITION_NO_DATA];
}
5.NSRecursiveLock

顾名思义,NSRecursiveLock定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。NSRecursiveLock在识别到递归时,只加1次锁,在递归返回时也只解锁1次。

// 初始化锁对象
@property (nonatomic, strong) NSRecursiveLock *recursiveLock;
_recursiveLock = [[NSRecursiveLock alloc] init];

/*******************************************************************************/

// 加锁的递归方法
- (void)testNSRecursiveLock:(NSInteger)tag {
    
    [_recursiveLock lock];
    
    if (tag > 0) {
        
        [self testNSRecursiveLock:tag - 1];
        NSLog(@"--->> %ld", (long)tag);
    }
    
    [_recursiveLock unlock];
}
6.@synchronized

@synchronized是一个 OC 层面的锁,非常简单易用。参数需要传一个 OC 对象,它实际上是把这个对象当做锁的唯一标识。使用时直接将加锁的代码区间放入花括号中即可,但是它的缺点也显而易见,虽然易用,但是没有之上介绍几个锁的复杂功能。

- (void)testSynchronized {
    
    @synchronized (self) {
        
        if (_ticketCount > 0) {
            
            _ticketCount --;
            NSLog(@"--->> %@已购票1张,剩余%ld张", [NSThread currentThread], (long)_ticketCount);
        }
    }
}

原子操作
原子操作是指不可打断的操作,也就是说线程在执行操作过程中,不会被操作系统挂起,而是一定会执行完。如文章开头出图中17+1 = 18这个动作,在整个运算过程中,就属于一个原子操作。

变量属性Property中的原子定义
一般我们定义一个变量 @property (nonatomic, strong) NSMutableArray *ticketsArr;
nonatomic:非原子属性,不会为setter方法加锁,适合内存小的移动设备;
atomic:原子属性,默认为setter方法加锁(默认就是atomic),线程安全。

PS: 在iOS开发过程中,一般都将属性声明为nonatomic,尽量避免多线程抢夺同一资源,尽量将加锁等资源抢夺业务交给服务器。

本文参考了以下文章:
https://www.cnblogs.com/crash-wu/p/4806499.html
https://blog.csdn.net/abc649395594/article/details/52747864
非常感谢!

工程源码GitHub地址

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