iOS GCD线程同步问题

我们平时在开发中比较常用的多线程主要包括三个:NSThread、NSOperation和GCD,当然还有一个较底层的pthread,这三种底层实现都是基于pthread,本文着重讲述GCD的使用以及线程同步问题

先看下两个概念,同步异步任务,串行并行队列

同步异步决定着是否有开启子线程的能力,串行并行决定着任务执行的先后顺序

再看下GCD的简单实用

如果有三个线程A、B、C(C是主线程),需求是先让A、B并行执行完后再执行C!这个时候就可以使用GCD里面的线程组了

-(void)groupTest{

    dispatch_group_t group = dispatch_group_create();

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

//    子线程任务A

    dispatch_group_async(group, queue, ^{

        sleep(0.1);

        for(inti =0; i <5; i ++) {

            NSLog(@"--1--%@--",[NSThreadcurrentThread]);

        }

    });


    //    子线程任务B

    dispatch_group_async(group, queue, ^{

        sleep(0.1);

        for(inti =0; i <5; i ++) {

            NSLog(@"--2--%@--",[NSThreadcurrentThread]);

        }

    });


    dispatch_group_notify(group, queue, ^{

//        主线程任务C

        dispatch_async(dispatch_get_main_queue(), ^{

            for(inti =0; i <5; i ++) {

                NSLog(@"--3--dispatch_group_notify -- %@--",[NSThread currentThread]);

            }

        });

    });

}

再看下打印结果:

2019-12-02 15:28:42.106699+0800 GCD[57729:16321233] --1--<NSThread: 0x280d77b40>{number = 3, name = (null)}--

2019-12-02 15:28:42.106869+0800 GCD[57729:16321235] --2--<NSThread: 0x280d36840>{number = 4, name = (null)}--

2019-12-02 15:28:42.106985+0800 GCD[57729:16321233] --1--<NSThread: 0x280d77b40>{number = 3, name = (null)}--

2019-12-02 15:28:42.107052+0800 GCD[57729:16321233] --1--<NSThread: 0x280d77b40>{number = 3, name = (null)}--

2019-12-02 15:28:42.107114+0800 GCD[57729:16321233] --1--<NSThread: 0x280d77b40>{number = 3, name = (null)}--

2019-12-02 15:28:42.107177+0800 GCD[57729:16321233] --1--<NSThread: 0x280d77b40>{number = 3, name = (null)}--

2019-12-02 15:28:42.107257+0800 GCD[57729:16321235] --2--<NSThread: 0x280d36840>{number = 4, name = (null)}--

2019-12-02 15:28:42.107319+0800 GCD[57729:16321235] --2--<NSThread: 0x280d36840>{number = 4, name = (null)}--

2019-12-02 15:28:42.107377+0800 GCD[57729:16321235] --2--<NSThread: 0x280d36840>{number = 4, name = (null)}--

2019-12-02 15:28:42.107435+0800 GCD[57729:16321235] --2--<NSThread: 0x280d36840>{number = 4, name = (null)}--

2019-12-02 15:28:42.119624+0800 GCD[57729:16321196] --3--dispatch_group_notify -- <NSThread: 0x280d6af80>{number = 1, name = main}--

2019-12-02 15:28:42.119739+0800 GCD[57729:16321196] --3--dispatch_group_notify -- <NSThread: 0x280d6af80>{number = 1, name = main}--

2019-12-02 15:28:42.119803+0800 GCD[57729:16321196] --3--dispatch_group_notify -- <NSThread: 0x280d6af80>{number = 1, name = main}--

2019-12-02 15:28:42.119862+0800 GCD[57729:16321196] --3--dispatch_group_notify -- <NSThread: 0x280d6af80>{number = 1, name = main}--

2019-12-02 15:28:42.119920+0800 GCD[57729:16321196] --3--dispatch_group_notify -- <NSThread: 0x280d6af80>{number = 1, name = main}--

可以看出线程A和B并行执行完后才执行C,需要注意的是group和queue必须是同一个,而且queue必须是并发队列,如果是串行队列,线程A和B就是按顺序执行了!

上面是GCD和队列的一个简单应用,下面看下一个多线程的一个经典应用:卖票!

#import "ViewController.h"

@interface ViewController ()

@property(nonatomic,assign) int ticketCount;

@end

@implementationViewController

- (void)viewDidLoad {

    [super viewDidLoad];

    self.ticketCount = 30;

    [self sellingTickets];

}

//卖一张票

-(void)sellingTicket{

    intoldTicketCount =self.ticketCount;

//    休眠更能体现线程安全问题

    sleep(0.3);

    oldTicketCount --;

    self.ticketCount= oldTicketCount;

    NSLog(@"还剩下%d张票,---%@",self.ticketCount,[NSThreadcurrentThread]);

}

//三个窗口同时卖票,每个窗口都卖掉10张票

-(void)sellingTickets{

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        for(inti =0; i <10; i ++) {

            [selfsellingTicket];

        }

    });


    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        for(inti =0; i <10; i ++) {

            [selfsellingTicket];

        }

    });


    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        for(inti =0; i <10; i ++) {

            [selfsellingTicket];

        }

    });

}

再看下代码执行的打印结果

可以看出数据是错乱的,而且每次运行结果都会有点不一样,这个时候就要进行线程同步了,我们在开发过程中最常用的就是加锁,iOS加锁的方式有很多,找了一个网上的性能对比图

各种锁的性能对比

1、OSSpinLock(自旋锁),顾名思义,就是自己在那里旋转,只要发现这个锁还没有被解开,一直占用CPU资源,处于忙等状态,直到锁被解开,下面会证明这个问题

先看下如何使用,需要导入头文件#import <libkern/OSAtomic.h>

//卖一张票

-(void)sellingTicket{

    static OSSpinLock lock = OS_SPINLOCK_INIT;

    OSSpinLockLock(&lock);

    intoldTicketCount =self.ticketCount;

//    休眠更能体现线程安全问题

    sleep(0.3);

    oldTicketCount --;

    self.ticketCount= oldTicketCount;

    NSLog(@"还剩下%d张票,---%@",self.ticketCount,[NSThreadcurrentThread]);

    OSSpinLockUnlock(&lock);

}

使用起来非常简单,OS_SPINLOCK_INIT就是0,所以这里可以直接使用static变量,如果不是一个具体的值就不能这样初始化了,可以写成属性,效果一样,不过OSSpinLock这个锁在iOS10之后就被弃用了,有可能会出现优先级反转问题,这里简单说下出现优先级反转问题的原因:

由于OSSpinLock这个自旋锁有个优先级的概念,就是优先级比较高的线程会被多分配一些CPU资源,如果有两个线程A、B,A的优先级较低,B的优先级较高,如果较低的线程A先进入方法进行加锁,当线程B进来时会等待线程A进行解锁才能继续执行,处于忙等状态,由于线程B是优先级较高的线程,系统会多分配一些资源给线程B进行自旋的操作,有可能导致线程B占用的资源过多,导致线程A分配的资源不足无法继续执行下面的代码,这样就造成了线程A的锁永远都无法被解锁!

下面通过汇编(这里是真机模式,ARM64架构)验证OSSpinLock自旋锁在等待锁被放开期间是一直处于循环忙等状态,先说下思路:在加锁的地方打上断点,过掉第一条线程,查看第二条线程在等待过程中的汇编干了些啥

第一条线程进来
第二条线程进来

第二条线程进入后直接进入汇编模式,Debug-》Debug Workflow-》勾上Always show Disassembly,然后一路敲si,直到出现下面的汇编为止

你会方向进入这个汇编指令后一直敲si,会在图片中阴影部位一直循环跳转,也就是进入了一个while循环,这也就证明了自旋锁是一直处于循环忙等状态

2、os_unfair_lock(自旋锁),OSSpinLock锁被废弃后,苹果推荐使用os_unfair_lock这个锁,不过有的文章说这是互斥锁,但本人自己测试过,看到的情景就是符合自旋锁,如有不正确,还请指出,万分感谢!

先看下如何使用,需要导入头文件#import <os/lock.h>

用法也很简单,需要注意的是,加锁解锁需要传入一个指针变量os_unfair_lock_t,可以看下这个定义

所以将os_unfair_lock这个结构体的地址传进去就可以了,按照同样的方法,进入第二条线程的汇编

可以看出一直在图片标明的部位循环执行,这也证明了os_unfair_lock是自旋锁

3、pthread_mutex_t(互斥锁),互斥锁意思就是在等待锁被放开期间该线程处于休眠状态,汇编证明可以自己尝试下,这里就不再累赘

先看下简单使用

pthread_mutex_t的用法有很多,这只是普通用法,假如有这样的一个场景,当票卖光了,就不能继续卖了,而是等待有人退票,退票成功后再回到卖票线程中继续卖票,类似生产者消费者模式,说白了就是消费者想买东西,必须依靠生产者生产出东西才能买到东西,直接看代码

#import "ViewController.h"

#import

@interface ViewController ()

@property(nonatomic,assign) int ticketCount;

@property(nonatomic,assign)pthread_mutex_t lock;

@property(nonatomic,assign)pthread_cond_t cond;

@end

@implementationViewController

- (void)viewDidLoad {

    [super viewDidLoad];

    self.ticketCount = 0;

    pthread_mutex_t lockTmp = PTHREAD_MUTEX_INITIALIZER;

    self.lock= lockTmp;


    pthread_cond_t condTmp = PTHREAD_COND_INITIALIZER;

    self.cond= condTmp;


    [self sellingTickets];

}

-(void)sellingTickets{

    [[[NSThread alloc] initWithTarget:self selector:@selector(sellingTicket) object:nil] start];

    [[[NSThread alloc] initWithTarget:self selector:@selector(refundTicket) object:nil] start];

}

//退一张票

-(void)refundTicket{

    NSLog(@"进入退票--refundTicket");

    pthread_mutex_lock(&_lock);

    sleep(2);

    intoldTicketCount =self.ticketCount;

    //    休眠更能体现线程安全问题

    oldTicketCount ++;

    self.ticketCount= oldTicketCount;

    NSLog(@"退票,还剩下%d张票,---%@",self.ticketCount,[NSThreadcurrentThread]);


    pthread_cond_signal(&_cond);

    pthread_mutex_unlock(&_lock);

}

//卖一张票

-(void)sellingTicket{

    NSLog(@"进入卖票--sellingTicket");

    pthread_mutex_lock(&_lock);


    if(self.ticketCount==0) {

        pthread_cond_wait(&_cond, &_lock);

    }

    intoldTicketCount =self.ticketCount;

    oldTicketCount --;

    self.ticketCount= oldTicketCount;

    NSLog(@"卖票,还剩下%d张票,---%@",self.ticketCount,[NSThreadcurrentThread]);


    pthread_mutex_unlock(&_lock);

}

上面的代码中创建了两个线程,一个执行卖票一个执行退票,而且卖票先执行,并且初始化的票数为0,再说下这句代码的作用

if(self.ticketCount==0) {

        pthread_cond_wait(&_cond, &_lock);

    }

如果票数为0,则解开当前的锁,并一直处于等待状态,这时退票任务就可以进行加锁,执行退票任务了,如果被再次唤醒而且锁已经被解开,则会再次加锁继续执行下面的任务。

pthread_cond_signal(&_cond); 这句代码意思是唤醒之前等待的地方,但只是被唤醒,退票的锁还没有解开前,卖票任务并不会执行,当退票任务中的锁被解开时,卖票任务才会继续执行

需要注意的是pthread_mutex_t创建的锁和一些条件参数在不使用的时候需要释放掉

-(void)dealloc{

    pthread_mutex_destroy(&_lock);

    pthread_cond_destroy(&_cond);

}

后面陆续补充pthread_mutex_t中递归锁以及其他类型的锁的用法!

最后再看一下IO操作相关的读写锁:pthread_rwlock_t

先看下普通锁的一个情况

self.lock1= [[NSLockalloc]init];

    for(inti =0; i <5; i ++) {

        [[[NSThread alloc] initWithTarget:self selector:@selector(read1) object:nil] start];

        [[[NSThread alloc] initWithTarget:self selector:@selector(write1) object:nil] start];

    }

-(void)read1{

    [self.lock1lock];

    NSLog(@"----read----");

    sleep(1);

    [self.lock1unlock];

}

-(void)write1{

    [self.lock1lock];

    NSLog(@"----write----");

    sleep(2);

    [self.lock1unlock];

}

2019-12-02 17:11:27.015666+0800 GCD[58077:16338699] ----read----

2019-12-02 17:11:28.021159+0800 GCD[58077:16338700] ----write----

2019-12-02 17:11:30.023676+0800 GCD[58077:16338701] ----read----

2019-12-02 17:11:31.028279+0800 GCD[58077:16338702] ----write----

2019-12-02 17:11:33.031558+0800 GCD[58077:16338703] ----read----

2019-12-02 17:11:34.032123+0800 GCD[58077:16338704] ----write----

2019-12-02 17:11:36.035910+0800 GCD[58077:16338705] ----read----

2019-12-02 17:11:37.040473+0800 GCD[58077:16338707] ----read----

2019-12-02 17:11:38.045163+0800 GCD[58077:16338708] ----write----

2019-12-02 17:11:40.049023+0800 GCD[58077:16338706] ----write----

可以看出普通锁对读和写进行加锁后,读和写同一时间只能有一条线程进行操作,但对于文件IO操作,我们需要做到的是:

1、同一时间只能有一条写的线程对文件进行操作

2、同一时间可以有多条线程进行读的操作,但没有写的操作

我们再看下读写锁pthread_rwlock_t

- (void)viewDidLoad {

    [super viewDidLoad];

    pthread_rwlock_init(&_rwlock, NULL);

    for(inti =0; i <5; i ++) {

        [[[NSThread alloc] initWithTarget:self selector:@selector(read) object:nil] start];

        [[[NSThread alloc] initWithTarget:self selector:@selector(write) object:nil] start];

    }

}

-(void)read{

    pthread_rwlock_rdlock(&_rwlock);

    NSLog(@"----read----");

    sleep(1);

    pthread_rwlock_unlock(&_rwlock);

}

-(void)write{

    pthread_rwlock_wrlock(&_rwlock);

    NSLog(@"----write----");

    sleep(2);

    pthread_rwlock_unlock(&_rwlock);

}

2019-12-02 17:19:24.244420+0800 GCD[58106:16339881] ----write----

2019-12-02 17:19:26.250256+0800 GCD[58106:16339880] ----read----

2019-12-02 17:19:26.250586+0800 GCD[58106:16339882] ----read----

2019-12-02 17:19:27.256933+0800 GCD[58106:16339883] ----write----

2019-12-02 17:19:29.258925+0800 GCD[58106:16339884] ----read----

2019-12-02 17:19:30.264998+0800 GCD[58106:16339885] ----write----

2019-12-02 17:19:32.270677+0800 GCD[58106:16339886] ----read----

2019-12-02 17:19:33.275315+0800 GCD[58106:16339887] ----write----

2019-12-02 17:19:35.279796+0800 GCD[58106:16339888] ----read----

2019-12-02 17:19:36.283005+0800 GCD[58106:16339889] ----write----

可以看出同一时间可以有多条读的线程进行操作,但同一时间只能有一条写的线程进行操作,需要注意的是如果将读写锁代码加入到队列中,则必须加入到自己创建的并发队列中,如果放到全局队列是达不到多读单写的需求!

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