美文网首页
iOS GCD线程同步问题

iOS GCD线程同步问题

作者: Good_Citizen | 来源:发表于2019-11-28 16:27 被阅读0次

    我们平时在开发中比较常用的多线程主要包括三个: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----

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

    相关文章

      网友评论

          本文标题:iOS GCD线程同步问题

          本文链接:https://www.haomeiwen.com/subject/myiuwctx.html