深入浅出iOS多线程(五)——多线程锁


深入浅出iOS多线程(一)——线程的概念
深入浅出iOS多线程(二)——pthraed和NSThread的使用
深入浅出iOS多线程(三)——GCD多线程
深入浅出iOS多线程(四)——NSOperation多线程
深入浅出iOS多线程(五)——多线程锁

线程锁

使用多线程能提高程序的执行效率,但也同时也给程序带来一些线程安全上面的问题,比如是数据竞争关系(data race).

最常见的线程/进程同步的方法

  1. 临界资源
    • 各进程(线程)采取互斥的方式,实现共享的资源称作临界资源,也可以说是临界区所要访问的资源
  2. 临界区
    • 访问临界资源的一块代码
  3. 自旋锁
    • 用于多线程同步的一种锁,线程反复的去检测锁变量是否可用
    • 由于这一过程中一直执行,属于忙等待
    • 这种锁在阻塞时间短的场合下使用
  4. 互斥锁
    • 防止两条线程同时对同一资源进行读写的机制
    • 通常是将代码切成一个一个的临界区
    • 递归锁、非递归锁
  5. 读写锁
    • 读操作可以并发读取
    • 写操作是互斥的
    • 通常是使用互斥锁、条件变量、信号量实现
  6. 信号量锁
    • 更高级的一种锁,在semahpore去值为0/1的时候为互斥锁,
    • 通过取值的范围,来实现更加复杂的同步效果
  7. 条件锁:
    • 条件变量,当某些资源要求不满足的时候进入休眠,
    • 当资源要求满足的时候条件锁打开,

iOS中的线程锁

解释锁的时候,首先来实现一个简单数据竞态代码:

dispatch_queue_t q = dispatch_get_global_queue(0, 0);
__block int count = 0;
for (int i = 0; i<10000; i++) {
    dispatch_async(q, ^{
        count ++;
        NSLog(@"%d",count);
    });
}

打印结果:

 9995
 9996
 9997
 9998

最终结果应该是10000,最后的结果是9998,少+了2,说明线程之间有数据竞态的情况

一、互斥锁

1. NSLock<NSLocking>

NSLock是Foundation框架中一种锁,代码如下:

@protocol NSLocking

- (void)lock;//加锁
- (void)unlock;//解锁

@end

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

//尝试加锁,不会阻塞线程。YES则加锁成功,NO则失败,说明其他线程在加锁中这个方法无论如何都会立即返回。
- (BOOL)tryLock;

//尝试在指定NSDate之前加锁,会阻塞线程。YES则加锁成功,NO则失败,说明其他线程在加锁中这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
- (BOOL)lockBeforeDate:(NSDate *)limit;

//name 是用来标识用的
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

一个简单的加锁实例代码;

- (void)createLock{
    /*
     *  NSLock加锁互斥锁
     *
     **/
    __block int count = 0;
    //NSLock 初始化
    NSLock *lock = [[NSLock alloc]init];
    
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);
  
    for (int i = 0; i<10000; i++) {
        dispatch_async(q, ^{
            
            //
            //加锁
            [lock lock];
            count ++ ;
            NSLog(@"%d",count);
            //解锁
            [lock unlock];
        });
    }
    //尝试加锁,返回YES加锁成功,返回NO获取锁失败,加锁失败
//        [lock tryLock];
    //在nsdate之前加锁,加锁成功返回YES,加锁失败返回NO
//        [lock lockBeforeDate:[[NSDate new]dateByAddingTimeInterval:10]];
    
}

打印代码:

 9995
 9996
 9997
 9998
 9999
 10000

2. pthread_mutex

pthread_mutex是跨平台,iOS系统自带多线程技术pthread的线程锁,它的简单使用方法如下:

pthread_mutex_t pMutex;
pthread_mutex_init(&pMutex, NULL);  //初始化pthread_mutex_t
pthread_mutex_lock(&pMutex);        //加锁
pthread_mutex_unlock(&pMutex);      //解锁

代码例子:


    pthread_mutex_t pthreadmutex;
    
    /*
     #define PTHREAD_MUTEX_NORMAL           0     默认
     #define PTHREAD_MUTEX_ERRORCHECK       1     检错锁
     #define PTHREAD_MUTEX_RECURSIVE        2  递归锁
     #define PTHREAD_MUTEX_DEFAULT        PTHREAD_MUTEX_NORMAL  默认
     */

    pthread_mutex_init(&pthreadmutex, PTHREAD_MUTEX_NORMAL);
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);
    __block int count = 0;
    for (int i = 0; i<10000; i++) {
        dispatch_async(q, ^{
            pthread_mutex_lock(&pthreadmutex);
            count ++;
            NSLog(@"%d",count);
            pthread_mutex_unlock(&pthreadmutex);
        });
    }
    pthread_mutex_destroy(&pthreadmutex);

打印结果:

 9995
 9996
 9997
 9998
 9999
 10000

3. @synchronized

synchronized属于互斥锁中的另一个变种:递归锁(重入锁),上面有递归锁的概念介绍,synchronized在苹果已经开源了有关代码,可以这里查看synchronized还有

__block int count = 0;
dispatch_queue_t q = dispatch_get_global_queue(0, 0);

for (int i = 0; i<10000; i++) {
    dispatch_async(q, ^{
        
        //递归锁
        @synchronized (self) {
            count ++ ;
            NSLog(@"%d",count);
        }
    });
}

打印内容:

 9995
 9996
 9997
 9998
 9999
 10000

递归锁

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

首先来尝试一个重入死锁的代码:

    __block int count = 0;
    dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);

    NSLock *lock1 = [[NSLock alloc]init];
    dispatch_async(q, ^{
        for (int j = 0; j<5; j++) {
            [lock1 lock];
            int num = count;
            sleep(1);
            num ++;
            count = num;
            NSLog(@"%d",count);
        }
    });

上述内容就是造成死锁的条件

1. NSRecursiveLock<NSLocking>
    NSRecursiveLock *lock = [[NSRecursiveLock alloc]init];
    __block int count = 0;
    dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(q, ^{
        for (int j = 0; j<5; j++) {
            [lock lock];
            int num = count;
            sleep(1);
            num ++;
            count = num;
            NSLog(@"%d",count);
        }
    });
    
    //[lock unlock]; //解锁
    //尝试加锁,返回YES加锁成功,返回NO获取锁失败,加锁失败
    //        [lock tryLock];
    //在nsdate之前加锁,加锁成功返回YES,加锁失败返回NO
    //        [lock lockBeforeDate:[[NSDate new]dateByAddingTimeInterval:10]];
    

上述代码发现不会死锁。

2. pthread_mutex(recursive)
pthread_mutex锁也支持递归,只需要设置PTHREAD_MUTEX_RECURSIVE即可

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

pthread_mutex实现递归锁代码如下:


- (void)doing{
    
//    for (int j = 0; j<5; j++) {

        int num = _Mycount;
        sleep(1);
        num ++;
        _Mycount = num;
        NSLog(@"%d",_Mycount);

//    }
    
}

_Mycount = 0;
__block pthread_mutex_t lock;
    
//初始化锁属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
//设置该锁为递归锁
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    
//初始化锁
pthread_mutex_init(&lock, &attr);
    
    
  
dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(q, ^{
    
    for (int j = 0; j<5; j++) {
        pthread_mutex_lock(&lock);
        [self doing];
    }
    pthread_mutex_unlock(&lock);  //如果加了递归锁,不释放锁,同一个线程还是可以使用锁。

    NSLock *lock2 = [[NSLock alloc]init];
    dispatch_async(q, ^{
        for (int j = 0; j<5; j++) {
            [lock2 lock];
            [self doing];
            [lock2 unlock];
        }
    });
});
    
//销毁锁
pthread_mutexattr_destroy(&attr);

上述代码:我在一个循环之中一直加锁,但是并没有死锁。

二 自旋锁

何时使用自旋锁,何时使用互斥锁:

  • 当预计线程等待锁的时间很短,或者加锁的代码(临界区)经常被调用,但竞争情况很少发生,再或者CPU资源不紧张,拥有多核处理器的时候使用自旋锁比较合适。
  • 而当预计线程等待锁的时间较长,CPU是单核处理器,或者临界区有IO操作,或者临界区代码复杂或者循环量大,临界区竞争非常激烈的时候使用互斥锁比较合适

OSSpinLock自旋锁,因为不在安全,优先级反转。在iOS10以后被弃用,因为有可能会因为优先级反转导致线程不在安全。 苹果给出的替代方案是os\_unfair\_lock

os\_unfair\_lock猜测:

  • 在优先级反转的时候,应该是做了一些相应的处理,比如说直接释放低优先级的锁,在重新加入到线程当中。
  • 或者直接使用互斥锁代替,也可能是优化后的互斥锁

os_unfair_lock

__block os_unfair_lock _osunfairLock;
// 初始化os_unfair_lock
_osunfairLock = OS_UNFAIR_LOCK_INIT;
    
dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
__block int count = 0;
for (int i = 0; i<10000; i++) {
    dispatch_async(q, ^{
        // 加锁
        os_unfair_lock_lock(&(_osunfairLock));
        count ++;
        NSLog(@"%d",count);
        // 解锁
        os_unfair_lock_unlock(&(_osunfairLock));
    });
}
   
/*
 *如果锁当前由调用线程拥有,这个函数返回。
 *
 *如果锁被其他线程解锁或拥有,这个函数断言和终止进程。
 **/
//    void os_unfair_lock_assert_owner
/*
 *如果锁被其他线程解锁或拥有,这个函数
 *返回。
 *
 *如果锁当前由当前线程拥有,则此函数断言
 并终止进程。
 **/
//    void os_unfair_lock_assert_not_owner
    
/*
 尝试获取锁如果其他线程持有锁,返回NO,如果获取到锁,返回YES
 */
//    bool os_unfair_lock_trylock

三、读写锁

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

pthread的API:

pthread_rwlock
//加读锁
pthread_rwlock_rdlock(&rwlock);
//解锁
pthread_rwlock_unlock(&rwlock);
//加写锁
pthread_rwlock_wrlock(&rwlock);
//解锁
pthread_rwlock_unlock(&rwlock);

四、条件锁

1. NSCondition<NSLocking>

//条件锁NSCondition的API

wait  //等待
waitUntilDate:(NSDate *)limit //
signal
broadcast

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

dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
    
NSCondition *lock = [[NSCondition alloc] init];
//第一个线程
__block BOOL finished = NO;
dispatch_async(q, ^{
    [lock lock];
    while (!finished) {
        [lock wait];
        NSLog(@"第一个线程得到第二个线程的通知");
    }
    [lock unlock];
    NSLog(@"第一个线程使用完毕");
});
    
//第二个线程
dispatch_async(q, ^{
    [lock lock];
    sleep(2);
    finished = YES;
    NSLog(@"我做了一些事情,告诉第一个线程");
    [lock signal];
    [lock unlock];
    NSLog(@"第二个线程使用完毕");
});

2. NSConditionLock<NSLocking>

条件锁的API

@property (readonly) NSInteger condition;
//初始化时必须放入条件condition
- (instancetype)initWithCondition:(NSInteger)condition;
//加锁时添加条件
- (void)lockWhenCondition:(NSInteger)condition;
//尝试加锁
- (BOOL)tryLock;
//尝试加锁的条件
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
//解锁附加条件
- (void)unlockWithCondition:(NSInteger)condition;
//limit时间之前加锁
- (BOOL)lockBeforeDate:(NSDate *)limit;
//limit时间之前加锁附加条件
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

这个条件锁,跟NSLock使用类似,是不过API附加了condition

五、信号量

1. dispatch_semaphore_t

  • 创建信号量dispatch_semaphore_create(value),value代表信号量的值
  • dispatch_semaphore_wait(<dispatch_semaphore_t>,DISPATCH_TIME_FOREVER),
  • dispatch_semaphore_signal(<dispatch_semaphore_t>)
    1. 上面两个方法可以理解为一个完整的信号量,dispatch_semaphore_signal +1 信号量的值,而dispatch_semaphore_wait -1 信号量的值
    2. 信号量的值如果等于0dispatch_semaphore_wait就会阻塞该方法以下的内容,当调用dispatch_semaphore_signal,信号量的值 +1, dispatch_semaphore_wait就会收到信号,信号量的值大于0就继续向下执行,直到信号量的值为0位置。
    • 从上述的两个解释可以得出结论:
      • dispatch_semaphore_create的信号量的值必须大于等于0,信号量的值为0时,dispatch_semaphore_wait 阻塞,必须调用dispatch_semaphore_signal信号量值+1,不能再次调用dispatch_semaphore_wait让信号量小于0

所以通过信号量的值设置为0,在异步方法之后添加dispatch_semaphore_wait,信号量的值设置为0阻塞,这时异步方法执行完成执行dispatch_semaphore_signal,会执行dispatch_semaphore_wait信号量-1,达到同步线程的目的。

验证代码如下:

__block int count = 0;

dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
    
dispatch_async(q, ^{
    count ++;
    NSLog(@"%d",count);
    dispatch_semaphore_signal(sema);
});
NSLog(@"第一次wait");
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    
   
dispatch_async(q, ^{
    count ++;
    NSLog(@"%d",count);
    dispatch_semaphore_signal(sema);
});
NSLog(@"第二次wait");
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    
dispatch_async(q, ^{
    count ++;
    NSLog(@"%d",count);
    dispatch_semaphore_signal(sema);
});
NSLog(@"第三次wait");
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    
    
dispatch_async(q, ^{
    count ++;
    NSLog(@"%d",count);
    dispatch_semaphore_signal(sema);
});
NSLog(@"第四次wait");
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

打印内容:

 第一次wait
 1
 第二次wait
 2
 第三次wait
 3
 第四次wait
 4

线程锁遇到的问题

1. 死锁

所谓死锁,通常指有两个线程A和B都卡住了,并等待对方完成某些操作。A不能完成是因为它在等待B完成,B不能完成,因为它在等待A完成,于是就是死锁了。

2. 产生死锁的四个必要条件

四个必要条件

  • 互斥条件:一个锁每次只能被一个线程获取
  • 请求与保持条件:一个线程因请求获取锁而阻塞时,对已获得的锁保持
  • 不可剥夺条件:线程已获得的锁,在未使用完之前,不能强行释放
  • 环路等待条件:必然存在一个获取锁的环形链,即A等待获取b的锁,B等待获取C的锁,而C等待获取A的锁。这就形成了一个环

3. 死锁的方式

一般死锁

  • 如何造成这种死锁
    • 线程A1,A2都需要同时获取锁B1、B2锁才能正常地完成功能
    • 但是由于线程A1先持有了B1锁,而线程A2先获取了B2的锁
    • 线程A1等待A2释放B2的锁才能完成任务解锁,而线程A2等待A1释放A1的锁才能完成任务解锁
    • 这就造成了死锁
  • 解决方法:
      1. 等其中一条线程完全执行完之后再执行另外一条线程。
      1. 设置优先级,如果运行多条线程出现死锁,优先级低的回退,优先级高的先执行这样即可解决死锁问题。

递归死锁(重入死锁)

  • 如何造成这种死锁

    • 一个线程持有一个对象的锁,在没有释放这个锁之前又获取了一次锁,这也就造成了线程的死锁
  • 解决方法:

      1. 在第二次回去锁的时候先讲第一次获取的锁匙放
      1. 使用递归锁,进行加锁

总结

性能总结

参考 不在安全的OSSpinLock

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

推荐阅读更多精彩内容

  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,495评论 0 6
  • 前言 一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同...
    WQ_UESTC阅读 844评论 0 5
  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 636评论 0 0
  • 前言   在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题。解决资源争用,...
    小盟城主阅读 1,421评论 0 3
  • 一、前言 前段时间看了几个开源项目,发现他们保持线程同步的方式各不相同,有@synchronized、NSLock...
    稻春阅读 464评论 0 0