iOS的线程安全与锁

一、什么是线程安全?

WIKI: Thread-safe code only manipulates shared data structures in a manner that ensures that all threads behave properly and fulfil their design specifications without unintended interaction.

用人话来说:多线程操作共享数据不会出现想不到的结果就是线程安全的,否则,是线程不安全的。

举个例子:

NSInteger total = 0;

- (void)threadNotSafe {

    for (NSInteger index = 0; index < 3; index++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            total += 1;

            NSLog(@"total: %ld", total);

            total -= 1;

            NSLog(@"total: %ld", total);

        });

    }

}

//第一次输出:

2017-11-28 23:34:11.551570+0800 BasicDemo[75679:5312246] total: 1

2017-11-28 23:34:11.551619+0800 BasicDemo[75679:5312248] total: 3

2017-11-28 23:34:11.551618+0800 BasicDemo[75679:5312249] total: 2

2017-11-28 23:34:11.552120+0800 BasicDemo[75679:5312246] total: 2

2017-11-28 23:34:11.552143+0800 BasicDemo[75679:5312248] total: 1

2017-11-28 23:34:11.552171+0800 BasicDemo[75679:5312249] total: 0

//第二次输出

2017-11-28 23:34:55.738947+0800 BasicDemo[75683:5313401] total: 1

2017-11-28 23:34:55.738979+0800 BasicDemo[75683:5313403] total: 2

2017-11-28 23:34:55.738985+0800 BasicDemo[75683:5313402] total: 3

2017-11-28 23:34:55.739565+0800 BasicDemo[75683:5313401] total: 2

2017-11-28 23:34:55.739570+0800 BasicDemo[75683:5313402] total: 1

2017-11-28 23:34:55.739577+0800 BasicDemo[75683:5313403] total: 0

NSInteger total = 0;

NSLock *lock = [NSLock new];

- (void)threadSafe {

    for (NSInteger index = 0; index < 3; index++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            [lock lock];

            total += 1;

            NSLog(@"total: %ld", total);

            total -= 1;

            NSLog(@"total: %ld", total);

            [lock unlock];

        });

    }

}

//第一次输出

2017-11-28 23:35:37.696614+0800 BasicDemo[75696:5314483] total: 1

2017-11-28 23:35:37.696928+0800 BasicDemo[75696:5314483] total: 0

2017-11-28 23:35:37.696971+0800 BasicDemo[75696:5314481] total: 1

2017-11-28 23:35:37.696995+0800 BasicDemo[75696:5314481] total: 0

2017-11-28 23:35:37.697026+0800 BasicDemo[75696:5314482] total: 1

2017-11-28 23:35:37.697050+0800 BasicDemo[75696:5314482] total: 0

//第二次输出

2017-11-28 23:36:01.790264+0800 BasicDemo[75700:5315159] total: 1

2017-11-28 23:36:01.790617+0800 BasicDemo[75700:5315159] total: 0

2017-11-28 23:36:01.790668+0800 BasicDemo[75700:5315161] total: 1

2017-11-28 23:36:01.790687+0800 BasicDemo[75700:5315161] total: 0

2017-11-28 23:36:01.790711+0800 BasicDemo[75700:5315160] total: 1

2017-11-28 23:36:01.790735+0800 BasicDemo[75700:5315160] total: 0

第一个函数第一次和第二次调用的结果不一样,换句话说,不能确定代码的运行顺序和结果,是线程不安全的;第二个函数第一次和第二次输出结果一样,可以确定函数的执行结果,是线程安全的。

居于线程安全的含义,知道线程安全是相对于多线程而言的,单线程不会存在线程安全问题。因为,单线程代码的执行顺序是确定的,可以知道代码的执行结果。


二、锁锁锁

线程不安全是由于多线程访问造成的,那么如何解决?

1.既然线程安全问题是由多线程引起的,那么,最极端的可以使用单线程保证线程安全。

2.线程安全是由于多线程访问和修改共享资源而引起不可预测的结果,因此,如果都是访问共享资源而不去修改共享资源也可以保证线程安全,比如:设置只读属性的全局变量。

3.使用锁。

引用 ibireme 在《不再安全的 OSSpinLock:https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/》中的一张图片说明加解锁的效率:


我也下载了 ibireme 在 GitHub 上面的Demo来跑过(环境 iPhone6 iOS11.1)。发现,不同的循环次数,结果都不一样,并没有得到和 ibireme 一样的结果。所以,上面的柱状图也只做一个定向分析,并不是很准确的结果。

OSSpinLock:

自旋锁的实现原理比较简单,就是死循环。当a线程获得锁以后,b线程想要获取锁就需要等待a线程释放锁。在没有获得锁的期间,b线程会一直处于忙等的状态。如果a线程在临界区的执行时间过长,则b线程会消耗大量的cpu时间,不太划算。所以,自旋锁用在临界区执行时间比较短的环境性能会很高。

自旋锁的代码实现:

#import OSSpinLock lock = OS_SPINLOCK_INIT;

OSSpinLockLock(&lock);

//需要执行的代码

OSSpinLockUnlock(&lock);

//OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock)

//苹果在OSSpinLock注释表示被废弃,改用不安全的锁替代

dispatch_semaphore:

dispatch_semaphore实现的原理和自旋锁有点不一样。首先会先将信号量减一,并判断是否大于等于0,如果是,则返回0,并继续执行后续代码,否则,使线程进入睡眠状态,让出cpu时间。直到信号量大于0或者超时,则线程会被重新唤醒执行后续操作。

dispatch_semaphore_t lock = dispatch_semaphore_create(1);    //传入的参数必须大于或者等于0,否则会返回Null

long wait = dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);    //wait = 0,则表示不需要等待,直接执行后续代码;wait != 0,则表示需要等待信号或者超时,才能继续执行后续代码。lock信号量减一,判断是否大于0,如果大于0则继续执行后续代码;lock信号量减一少于或者等于0,则等待信号量或者超时。

//需要执行的代码

long signal = dispatch_semaphore_signal(lock);    //signal = 0,则表示没有线程需要其处理的信号量,换句话说,没有需要唤醒的线程;signal != 0,则表示有一个或者多个线程需要唤醒,则唤醒一个线程。(如果线程有优先级,则唤醒优先级最高的线程,否则,随机唤醒一个线程。)

pthread_mutex:

pthread_mutex表示互斥锁,和信号量的实现原理类似,也是阻塞线程并进入睡眠,需要进行上下文切换。

pthread_mutexattr_t attr;

pthread_mutexattr_init(&attr);

pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);


pthread_mutex_t lock;

pthread_mutex_init(&lock, &attr);    //设置属性


pthread_mutex_lock(&lock);    //上锁

//需要执行的代码

pthread_mutex_unlock(&lock);    //解锁

NSLock:

NSLock在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK。

NSLock *lock = [NSLock new];

[lock lock];

//需要执行的代码

[lock unlock];

NSCondition:

NSCondition封装了一个互斥锁和条件变量。互斥锁保证线程安全,条件变量保证执行顺序。

NSCondition *lock = [NSCondition new];

[lock lock];

//需要执行的代码

[lock unlock];

pthread_mutex(recursive):

pthread_mutex锁的一种,属于递归锁。一般一个线程只能申请一把锁,但是,如果是递归锁,则可以申请很多把锁,只要上锁和解锁的操作数量就不会报错。

pthread_mutexattr_t attr;

pthread_mutexattr_init(&attr);

pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);


pthread_mutex_t lock;

pthread_mutex_init(&lock, &attr);    //设置属性


pthread_mutex_lock(&lock);    //上锁

//需要执行的代码

pthread_mutex_unlock(&lock);    //解锁

NSRecursiveLock:

递归锁,pthread_mutex(recursive)的封装。

NSRecursiveLock *lock = [NSRecursiveLock new];

[lock lock];

//需要执行的代码

[lock unlock];

NSConditionLock:

NSConditionLock借助 NSCondition 来实现,本质是生产者-消费者模型。

NSConditionLock *lock = [NSConditionLock new];

[lock lock];

//需要执行的代码

[lock unlock];

@synchronized:

一个对象层面的锁,锁住了整个对象,底层使用了互斥递归锁来实现。

NSObject *object = [NSObject new];

@synchronized(object) {

  //需要执行的代码

}

三、总结

这里只是一些简单的总结,更多深入的研究请自行 Google。

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

推荐阅读更多精彩内容

  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,458评论 0 6
  • 前言 一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同...
    WQ_UESTC阅读 830评论 0 5
  • iOS线程安全的锁与性能对比 一、锁的基本使用方法 1.1、@synchronized 这是我们最熟悉的枷锁方式,...
    Jacky_Yang阅读 2,133评论 0 17
  • 在iOS编码中,锁的出现其实是因为多线程会出现线程安全的问题。那么,问题来了,什么是线程安全?为什么锁可以解决线程...
    一剑孤城阅读 4,132评论 0 7
  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 615评论 0 0