欢迎关注
最酷最in的云资讯

iOS源码解析:多线程线程同步


多线程的安全隐患

在使用多线程的过程中,一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,同一个变量,同一个对象,同一个文件。试想一下,三个线程同时向一个文件写东西,那势必会造成混乱。
下面以取钱存钱为例:

QQ截图20190409112440.png

在这个例子中,起初余额中有1000,存钱的线程首先读出余额1000,紧接着取钱的线程又取出余额1000,然后存钱的线程又存入了1000,所以把余额修改为了2000,之后,取钱的线程取出了500,由于之前读出的余额是500,所以将余额修改为1000-500=500,这样最终的余额就变成了500。按照正常的情况,余额应该是1500,这样就出现了混乱。

以车站卖票为例,车站中有多个窗口卖票,就相当于是多线程来处理

2.png

起始票数是1000,第一个卖票的站点先读取的票的余额,过了一会第二个卖票的站点也读取了票的余额,然后第一个站点卖出了一张票,因此把票数余额修改为了999,过了一会第二个站点也卖了一张票,把票数余额修改为了999,这样一来,票就永远卖不完了。
我们用代码实现一下卖票的过程:

@property (nonatomic, assign)int ticketsCount;- (void)saleTicket{    //这里使用oldTicketsCount主要是模拟整个读取票数然后卖票的过程,睡眠0.2使效果更明显    int oldTicketsCount = self.ticketsCount;    sleep(.2);    oldTicketsCount--;    self.ticketsCount = oldTicketsCount;        NSLog(@"最后还剩的票数%d 线程%@", oldTicketsCount, [NSThread currentThread]);}- (void)saleTickets{        self.ticketsCount = 15;        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);        dispatch_async(queue, ^{        for (int i = 0; i < 5; i++) {            [self saleTicket];        }    });        dispatch_async(queue, ^{        for (int i = 0; i < 5; i++) {            [self saleTicket];        }    });        dispatch_async(queue, ^{        for (int i = 0; i < 5; i++) {            [self saleTicket];        }    });    }

打印结果:

2018-09-26 15:12:52.746209+0800 TEST[10226:312194] 最后还剩的票数13 线程{number = 5, name = (null)}2018-09-26 15:12:52.746209+0800 TEST[10226:312193] 最后还剩的票数14 线程{number = 4, name = (null)}2018-09-26 15:12:52.746245+0800 TEST[10226:312195] 最后还剩的票数14 线程{number = 3, name = (null)}2018-09-26 15:12:52.746414+0800 TEST[10226:312194] 最后还剩的票数12 线程{number = 5, name = (null)}2018-09-26 15:12:52.746552+0800 TEST[10226:312193] 最后还剩的票数11 线程{number = 4, name = (null)}2018-09-26 15:12:52.746650+0800 TEST[10226:312195] 最后还剩的票数10 线程{number = 3, name = (null)}2018-09-26 15:12:52.746707+0800 TEST[10226:312194] 最后还剩的票数9 线程{number = 5, name = (null)}2018-09-26 15:12:52.746730+0800 TEST[10226:312193] 最后还剩的票数8 线程{number = 4, name = (null)}2018-09-26 15:12:52.746913+0800 TEST[10226:312195] 最后还剩的票数7 线程{number = 3, name = (null)}2018-09-26 15:12:52.747049+0800 TEST[10226:312194] 最后还剩的票数6 线程{number = 5, name = (null)}2018-09-26 15:12:52.747301+0800 TEST[10226:312193] 最后还剩的票数5 线程{number = 4, name = (null)}2018-09-26 15:12:52.747861+0800 TEST[10226:312194] 最后还剩的票数4 线程{number = 5, name = (null)}2018-09-26 15:12:52.747861+0800 TEST[10226:312195] 最后还剩的票数4 线程{number = 3, name = (null)}2018-09-26 15:12:52.748157+0800 TEST[10226:312193] 最后还剩的票数3 线程{number = 4, name = (null)}2018-09-26 15:12:52.749157+0800 TEST[10226:312195] 最后还剩的票数2 线程{number = 3, name = (null)}

可以看到产生了混乱,最后剩余的票数并不为0。

然后继续用代码实现取钱存钱的过程

@property (nonatomic, assign)int money;- (void)moneyTest{        self.money = 100;        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);    //存钱的线程    dispatch_async(queue, ^{        for (int i = 0; i < 10; i++) {            [self saveMoney];        }    });    //取钱的线程    dispatch_async(queue, ^{        for (int i = 0; i < 10; i++) {            [self drawmoney];        }    });}//存钱- (void)saveMoney{        int oldMoney = self.money;    sleep(.2);    oldMoney += 50;    self.money = oldMoney;        NSLog(@"存50 还剩%d元 - %@", oldMoney, [NSThread currentThread]);}//取钱- (void)drawmoney{        int oldMoney = self.money;    sleep(.2);    oldMoney -= 20;    self.money = oldMoney;        NSLog(@"取20 还剩%d元 - %@", oldMoney, [NSThread currentThread]);}

打印结果:

2018-09-26 15:27:13.265434+0800 TEST[10568:324343] 取20 还剩80元 - {number = 4, name = (null)}2018-09-26 15:27:13.265459+0800 TEST[10568:324337] 存50 还剩150元 - {number = 3, name = (null)}2018-09-26 15:27:13.265587+0800 TEST[10568:324337] 存50 还剩180元 - {number = 3, name = (null)}2018-09-26 15:27:13.265589+0800 TEST[10568:324343] 取20 还剩130元 - {number = 4, name = (null)}2018-09-26 15:27:13.265685+0800 TEST[10568:324337] 存50 还剩230元 - {number = 3, name = (null)}2018-09-26 15:27:13.265693+0800 TEST[10568:324343] 取20 还剩210元 - {number = 4, name = (null)}2018-09-26 15:27:13.265771+0800 TEST[10568:324337] 存50 还剩260元 - {number = 3, name = (null)}2018-09-26 15:27:13.265853+0800 TEST[10568:324343] 取20 还剩240元 - {number = 4, name = (null)}2018-09-26 15:27:13.266059+0800 TEST[10568:324337] 存50 还剩290元 - {number = 3, name = (null)}2018-09-26 15:27:13.266210+0800 TEST[10568:324343] 取20 还剩270元 - {number = 4, name = (null)}2018-09-26 15:27:13.266343+0800 TEST[10568:324337] 存50 还剩320元 - {number = 3, name = (null)}2018-09-26 15:27:13.266485+0800 TEST[10568:324343] 取20 还剩300元 - {number = 4, name = (null)}2018-09-26 15:27:13.266667+0800 TEST[10568:324337] 存50 还剩350元 - {number = 3, name = (null)}2018-09-26 15:27:13.266844+0800 TEST[10568:324343] 取20 还剩330元 - {number = 4, name = (null)}2018-09-26 15:27:13.267284+0800 TEST[10568:324337] 存50 还剩380元 - {number = 3, name = (null)}2018-09-26 15:27:13.267373+0800 TEST[10568:324343] 取20 还剩360元 - {number = 4, name = (null)}2018-09-26 15:27:13.267496+0800 TEST[10568:324337] 存50 还剩410元 - {number = 3, name = (null)}2018-09-26 15:27:13.267866+0800 TEST[10568:324343] 取20 还剩390元 - {number = 4, name = (null)}2018-09-26 15:27:13.268062+0800 TEST[10568:324337] 存50 还剩440元 - {number = 3, name = (null)}2018-09-26 15:27:13.268578+0800 TEST[10568:324343] 取20 还剩420元 - {number = 4, name = (null)}

从最后剩余的钱数来看就完全不对,数据发生了明显的混乱。

那么多线程的安全隐患怎么解决呢?解决方案就是使用线程同步技术,常见的线程同步技术是加锁。
iOS中的线程同步方案有下面这些:

OSSpinLock
  • OSSpinlock叫做”自旋锁”,等待锁的线程会处于忙等状态,一直占用CPU资源

  • 目前已经不再安全,可能会出现优先级反转的问题,即如果等待锁的线程优先级较高,它会一直占用着CPU的资源,优先级低的线程就无法释放锁。
    关于OSSpinLock的API:

    //初始化    OSSpinLock lock = OS_SPINLOCK_INIT;    //尝试加锁看,如果需要等待就不加锁,直接返回false,如果不需要等待就加锁,返回true。    bool result = OSSpinLockTry(&lock);    //加锁    OSSpinLockLock(&lock);    //解锁    OSSpinLockUnlock(&lock);

下面我们使用OSSpinLock来解决卖票的资源争夺的问题:

- (void)saleTicket{        //加锁    OSSpinLockLock(&_lock);    //这里使用oldTicketsCount主要是模拟整个读取票数然后卖票的过程,睡眠0.2使效果更明显    int oldTicketsCount = self.ticketsCount;    sleep(.2);    oldTicketsCount--;    self.ticketsCount = oldTicketsCount;        NSLog(@"最后还剩的票数%d 线程%@", oldTicketsCount, [NSThread currentThread]);        //解锁    OSSpinLockUnlock(&_lock);  }

我们看一下打印结果:

2018-09-26 15:59:05.225340+0800 TEST[11218:345833] 最后还剩的票数14 线程{number = 3, name = (null)}2018-09-26 15:59:05.225623+0800 TEST[11218:345833] 最后还剩的票数13 线程{number = 3, name = (null)}2018-09-26 15:59:05.225799+0800 TEST[11218:345833] 最后还剩的票数12 线程{number = 3, name = (null)}2018-09-26 15:59:05.225946+0800 TEST[11218:345833] 最后还剩的票数11 线程{number = 3, name = (null)}2018-09-26 15:59:05.226248+0800 TEST[11218:345833] 最后还剩的票数10 线程{number = 3, name = (null)}2018-09-26 15:59:05.227334+0800 TEST[11218:345826] 最后还剩的票数9 线程{number = 4, name = (null)}2018-09-26 15:59:05.227480+0800 TEST[11218:345826] 最后还剩的票数8 线程{number = 4, name = (null)}2018-09-26 15:59:05.227709+0800 TEST[11218:345826] 最后还剩的票数7 线程{number = 4, name = (null)}2018-09-26 15:59:05.228151+0800 TEST[11218:345826] 最后还剩的票数6 线程{number = 4, name = (null)}2018-09-26 15:59:05.233128+0800 TEST[11218:345826] 最后还剩的票数5 线程{number = 4, name = (null)}2018-09-26 15:59:05.237517+0800 TEST[11218:345827] 最后还剩的票数4 线程{number = 5, name = (null)}2018-09-26 15:59:05.238065+0800 TEST[11218:345827] 最后还剩的票数3 线程{number = 5, name = (null)}2018-09-26 15:59:05.238499+0800 TEST[11218:345827] 最后还剩的票数2 线程{number = 5, name = (null)}2018-09-26 15:59:05.239221+0800 TEST[11218:345827] 最后还剩的票数1 线程{number = 5, name = (null)}2018-09-26 15:59:05.239897+0800 TEST[11218:345827] 最后还剩的票数0 线程{number = 5, name = (null)}

可以看到现在的输出没有任何问题了。
线程加锁的原理就是,当某一个线程首次访问资源时,对该资源加锁,当另外一个线程要访问该资源时首先判断锁有没有加上,没有的话就加锁然后访问资源,如果锁已经加上了,那么就会等待,等待锁打开。

下面再用OSSpinLock来完成存钱取钱的加锁:

//存钱- (void)saveMoney{        OSSpinLockLock(&_lock);    int oldMoney = self.money;    sleep(.2);    oldMoney += 50;    self.money = oldMoney;        NSLog(@"存50 还剩%d元 - %@", oldMoney, [NSThread currentThread]);        OSSpinLockUnlock(&_lock);}//取钱- (void)drawmoney{        OSSpinLockLock(&_lock);    int oldMoney = self.money;    sleep(.2);    oldMoney -= 20;    self.money = oldMoney;        NSLog(@"取20 还剩%d元 - %@", oldMoney, [NSThread currentThread]);        OSSpinLockUnlock(&_lock);}

看一下打印结果:

2018-09-26 16:45:14.317794+0800 TEST[12223:379269] 存50 还剩150元 - {number = 3, name = (null)}2018-09-26 16:45:14.317953+0800 TEST[12223:379269] 存50 还剩200元 - {number = 3, name = (null)}2018-09-26 16:45:14.318071+0800 TEST[12223:379269] 存50 还剩250元 - {number = 3, name = (null)}2018-09-26 16:45:14.318182+0800 TEST[12223:379269] 存50 还剩300元 - {number = 3, name = (null)}2018-09-26 16:45:14.318374+0800 TEST[12223:379269] 存50 还剩350元 - {number = 3, name = (null)}2018-09-26 16:45:14.318500+0800 TEST[12223:379269] 存50 还剩400元 - {number = 3, name = (null)}2018-09-26 16:45:14.318587+0800 TEST[12223:379269] 存50 还剩450元 - {number = 3, name = (null)}2018-09-26 16:45:14.318689+0800 TEST[12223:379269] 存50 还剩500元 - {number = 3, name = (null)}2018-09-26 16:45:14.318823+0800 TEST[12223:379269] 存50 还剩550元 - {number = 3, name = (null)}2018-09-26 16:45:14.319047+0800 TEST[12223:379269] 存50 还剩600元 - {number = 3, name = (null)}2018-09-26 16:45:14.320129+0800 TEST[12223:379270] 取20 还剩580元 - {number = 4, name = (null)}2018-09-26 16:45:14.320242+0800 TEST[12223:379270] 取20 还剩560元 - {number = 4, name = (null)}2018-09-26 16:45:14.320347+0800 TEST[12223:379270] 取20 还剩540元 - {number = 4, name = (null)}2018-09-26 16:45:14.320459+0800 TEST[12223:379270] 取20 还剩520元 - {number = 4, name = (null)}2018-09-26 16:45:14.320588+0800 TEST[12223:379270] 取20 还剩500元 - {number = 4, name = (null)}2018-09-26 16:45:14.320693+0800 TEST[12223:379270] 取20 还剩480元 - {number = 4, name = (null)}2018-09-26 16:45:14.320900+0800 TEST[12223:379270] 取20 还剩460元 - {number = 4, name = (null)}2018-09-26 16:45:14.321222+0800 TEST[12223:379270] 取20 还剩440元 - {number = 4, name = (null)}2018-09-26 16:45:14.321331+0800 TEST[12223:379270] 取20 还剩420元 - {number = 4, name = (null)}2018-09-26 16:45:14.321548+0800 TEST[12223:379270] 取20 还剩400元 - {number = 4, name = (null)}
OSSpinLock目前已经不能使用的原因

OSSpinLock目前不建议使用的原因主要是会出现优先级反转。假设有3个线程线程1,线程2,线程3,那么如果这三个线程的优先级是一样的,那么CPU会平均的分配时间给这3个线程,比如首先给线程1 10ms去处理事件,然后给线程2 10ms去处理事件,再给线程3 10ms去处理事件,这样把时间切成碎片去处理,给人的感觉就像是三个线程一起在处理事件。但是当三个线程的优先级不一样的时候就会出现一些问题了,加入线程1的优先级较高,线程2的优先级较低,线程2首先访问资源,首先给资源加锁,这个时候线程1再去访问资源的时候,检查到锁已经加上了,所以就会在外面忙等,由于优先级很高,所以CPU分配给线程1的时间很多,分配给线程2的时间很少,这样会导致线程2没有时间来处理事件,锁很久不能打开,线程1长时间在外面等着,有点类似于死锁。

为了更加直管的观察各种锁,现在把存钱取钱卖票的业务逻辑抽到一个基类中,名为BaseDemo,主要代码如下:

@interface BaseDemo()    @property (nonatomic, assign)int money;@property (nonatomic, assign)int ticketsCount;@end@implementation BaseDemo- (void)moneyTest{        self.money = 100;        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);    //存钱的线程    dispatch_async(queue, ^{        for (int i = 0; i < 10; i++) {            [self saveMoney];        }    });    //取钱的线程    dispatch_async(queue, ^{        for (int i = 0; i < 10; i++) {            [self drawMoney];        }    });}//存钱- (void)saveMoney{        int oldMoney = self.money;    sleep(.2);    oldMoney += 50;    self.money = oldMoney;        NSLog(@"存50 还剩%d元 - %@", oldMoney, [NSThread currentThread]);    }//取钱- (void)drawMoney{        int oldMoney = self.money;    sleep(.2);    oldMoney -= 20;    self.money = oldMoney;        NSLog(@"取20 还剩%d元 - %@", oldMoney, [NSThread currentThread]);    }- (void)saleTicket{        //这里使用oldTicketsCount主要是模拟整个读取票数然后卖票的过程,睡眠0.2使效果更明显    int oldTicketsCount = self.ticketsCount;    sleep(.2);    oldTicketsCount--;    self.ticketsCount = oldTicketsCount;        NSLog(@"最后还剩的票数%d 线程%@", oldTicketsCount, [NSThread currentThread]);        }- (void)ticketTest{        self.ticketsCount = 15;        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);        dispatch_async(queue, ^{        for (int i = 0; i < 5; i++) {            [self saleTicket];        }    });        dispatch_async(queue, ^{        for (int i = 0; i < 5; i++) {            [self saleTicket];        }    });        dispatch_async(queue, ^{        for (int i = 0; i < 5; i++) {            [self saleTicket];        }    });    }@end

然后例如要演示OSSpinLock锁,我们可以创建一个类名为OSSPinLockDemo继承自BaseDemo,然后在其中实现存钱取钱卖票:

//OSSpinLockDemo.m- (instancetype)init{        if (self = [super init]) {        self.moneyLock = OS_SPINLOCK_INIT;        self.ticketlock = OS_SPINLOCK_INIT;    }        return self;}- (void)saveMoney{        OSSpinLockLock(&_moneyLock);        [super saveMoney];        OSSpinLockUnlock(&_moneyLock);    }- (void)drawMoney{        OSSpinLockLock(&_moneyLock);        [super drawMoney];        OSSpinLockUnlock(&_moneyLock);}- (void)saleTicket{        OSSpinLockLock(&_ticketlock);        [super saleTicket];        OSSpinLockUnlock(&_ticketlock);}

在主函数中这样调用:

    OSSpimLinkDemo *demo = [[OSSpimLinkDemo alloc] init];    [demo ticketTest];

这样做的好处是,我们可以更加专注于加锁的过程,而不用去管业务逻辑,每学习一个锁,就写一个子类。

os_unfair_lock

下面学习os_unfair_lock这种锁。

os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持。
从底层调用看,等待os_unfair_lock锁的线程处于休眠状态,并非忙等。
需要导入头文件

os_unfair_lock的基本API如下:

        //初始化        os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;        //尝试加锁        os_unfair_lock_trylock(&lock);        //加锁        os_unfair_lock_lock(&lock);        //解锁        os_unfair_lock_unlock(&lock);

接下来我们可以写一个子类OSUnFairLockDemo类,然后在这个类中重写卖票方法如下:

//OSUnFairLockDemo.m- (instancetype)init{        if (self = [super init]) {        self.ticketlock = OS_UNFAIR_LOCK_INIT;    }        return self;}- (void)saleTicket{        os_unfair_lock_lock(&_ticketlock);        [super saleTicket];        os_unfair_lock_unlock(&_ticketlock);}

然后看一下输出结果:

2018-09-27 16:06:24.453628+0800 TEST[26669:857080] 最后还剩的票数14 线程{number = 3, name = (null)}2018-09-27 16:06:24.453777+0800 TEST[26669:857080] 最后还剩的票数13 线程{number = 3, name = (null)}2018-09-27 16:06:24.453893+0800 TEST[26669:857080] 最后还剩的票数12 线程{number = 3, name = (null)}2018-09-27 16:06:24.453988+0800 TEST[26669:857080] 最后还剩的票数11 线程{number = 3, name = (null)}2018-09-27 16:06:24.454108+0800 TEST[26669:857080] 最后还剩的票数10 线程{number = 3, name = (null)}2018-09-27 16:06:24.454235+0800 TEST[26669:857082] 最后还剩的票数9 线程{number = 4, name = (null)}2018-09-27 16:06:24.454323+0800 TEST[26669:857082] 最后还剩的票数8 线程{number = 4, name = (null)}2018-09-27 16:06:24.454421+0800 TEST[26669:857082] 最后还剩的票数7 线程{number = 4, name = (null)}2018-09-27 16:06:24.454513+0800 TEST[26669:857082] 最后还剩的票数6 线程{number = 4, name = (null)}2018-09-27 16:06:24.454600+0800 TEST[26669:857082] 最后还剩的票数5 线程{number = 4, name = (null)}2018-09-27 16:06:24.454712+0800 TEST[26669:857083] 最后还剩的票数4 线程{number = 5, name = (null)}2018-09-27 16:06:24.454840+0800 TEST[26669:857083] 最后还剩的票数3 线程{number = 5, name = (null)}2018-09-27 16:06:24.458107+0800 TEST[26669:857083] 最后还剩的票数2 线程{number = 5, name = (null)}2018-09-27 16:06:24.458217+0800 TEST[26669:857083] 最后还剩的票数1 线程{number = 5, name = (null)}2018-09-27 16:06:24.458307+0800 TEST[26669:857083] 最后还剩的票数0 线程{number = 5, name = (null)}

可以看到,数据没有发生混乱。

pthread_mutex

mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。
需要导入头文件

与之相关的API有:

        //初始化锁的属性        pthread_mutexattr_t attr;        pthread_mutexattr_init(&attr);        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);                //初始化锁        pthread_mutex_t mutex;        pthread_mutex_init(&mutex, &attr);                //尝试加锁        pthread_mutex_trylock(&mutex);        //加锁        pthread_mutex_lock(&mutex);        //解锁        pthread_mutex_unlock(&mutex);        //销毁相关资源        pthread_mutexattr_destroy(&attr);        pthread_mutex_destroy(&mutex);                /*         *Mutex type attributes         */        #define PTHREAD_MUTEX_NORMAL       0        #define PTHREAD_MUTEX_ERRORCHECK   1        #define PTHREAD_MUTEX_RECURSIVE    2        #define PTHREAD_MUTEX_DEFAULT

我们可以创建一个子类MutexDemo,然后重写卖票方法:

//MutexDemo.m- (instancetype)init{        if (self = [super init]) {                //初始化锁的属性        pthread_mutexattr_t attr;        pthread_mutexattr_init(&attr);        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL );                //初始化锁        pthread_mutex_t mutex;        pthread_mutex_init(&_ticketLock, &attr);           pthread_mutexattr_destroy(&attr);    }        return self;}- (void)saleTicket{        pthread_mutex_lock(&_ticketLock);        [super saleTicket];        pthread_mutex_unlock(&_ticketLock);}

打印出来数据没有发生混乱。

由一个问题引出递归锁

创建一个子类MutexDemo2,在这个类中像MutexDemo一样,创建pthread_Mutex类型的互斥锁:

- (instancetype)init{        if (self = [super init]) {                //初始化锁的属性        pthread_mutexattr_t attr;        pthread_mutexattr_init(&attr);        //通过属性确定创建的是互斥锁        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);                //初始化锁        pthread_mutex_init(&_ticketLock, &attr);                pthread_mutexattr_destroy(&attr);           }        return self;}- (void)otherTest{        pthread_mutex_lock(&_ticketLock);        NSLog(@"%s", __func__);    [self otherTest2];        pthread_mutex_unlock(&_ticketLock);}- (void)otherTest2{        pthread_mutex_lock(&_ticketLock);        NSLog(@"%s", __func__);        pthread_mutex_unlock(&_ticketLock);}

然后创建实例对象去调用otherTest这个方法:

    MutexDemo2 *demo = [[MutexDemo2 alloc] init];    [demo otherTest];

我们看一下运行效果:

2018-09-27 18:44:56.627062+0800 TEST[30733:965088] -[MutexDemo2 otherTest]

只打印了otherTest方法中的输出,而没有打印otherTest2方法中的输出,这是什么原因呢?
原因在于,执行otherTest时,将ticketLock这个锁锁上了,锁上后去调用otherTest2方法,在otherTest2方法中,检查到锁锁上了,所以就会一直在碗面等,等这个锁打开,而锁打开又依赖于otherTest2方法执行完成,这样代码就没法执行下去了。
这个方法其实很好解决,由于是两个不同的方法,所以这两个方法使用不同的锁就行了,那么如果是递归呢?也就是otherTest里面调用otherTest呢?这样就不可能使用两把锁了,那这个问题又该怎么解决呢?
这个时候递归锁就派上用场了

递归锁:允许同一个线程对一把锁进行重复加锁

我们可以把pthread_Mutex锁的属性改为递归锁:

        //改变锁的属性为递归锁        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
- (void)otherTest{    //第二次调用到这个地方的时候,可以再给ticketLock这个锁加一次锁    pthread_mutex_lock(&_ticketLock);        NSLog(@"%s", __func__);    [self otherTest];    //在解锁的时候相对应也会解两次锁    pthread_mutex_unlock(&_ticketLock);}

这样就能解决这个递归死锁的问题。

从汇编实现来看自旋锁是忙等,互斥锁是休眠

我们在BaseDemo这个基类中修改ticketTest这个方法的实现,创建十条线程来调用saleTicket方法:

- (void)ticketTest{        self.ticketsCount = 15;        for (int i = 0; i < 15; i++) {        [[[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil] start];    }

然后在saleTicket这个方法里面设置睡眠时间为600s,这样一来,当第一条线程进入saleTicket方法后,由于休眠600s,所以锁在600s内会被锁着,当第二条线程调用saleTicket方法时,就会在外面等待:

- (void)saleTicket{        //睡眠600s是保证第二条线程进来时锁是被锁着,于是w要在外面等待    int oldTicketsCount = self.ticketsCount;    sleep(600);    oldTicketsCount--;    self.ticketsCount = oldTicketsCount;        NSLog(@"最后还剩的票数%d 线程%@", oldTicketsCount, [NSThread currentThread]);}
为了研究自旋锁,我们选择OSSpinLock这个锁,在OSSpinLock的类文件中打下断点:

QQ截图20190409112632.png

当第一条线程访问时,直接过掉断点,第二条线程执行到断点处时,进入汇编里面查看等待的过程。
下面是第二条线程执行到断点处时进入汇编:

QQ截图20190409112659.png

我们可以使用stepi指令或者si指令来一步一步执行汇编指令,这样单步执行遇到函数时会调进函数。

然后我们使用si指令来一步一步执行汇编指令,执行到ox105f329b1时跳进去了,通过si一步一步的执行,最终来到了下面的汇编:

QQ截图20190409112723.png

执行的时候发现,汇编指令在0x107ef3a32和0x107ef3a43之间循环执行,jne就是一个while循环,条件满足就继续执行框内的代码,等待条件不满足也就是锁已经打开就继续往下执行。这里也就证明了自旋锁使用的是忙等。

为了研究互斥锁,我们选择pthread_Mutex这个锁,单步执行很多次之后,跳到了下图:

采用研究OSSpinLock一样的方法,通过汇编指令来解读

QQ截图20190409112751.png

这个syscall是一个系统级的函数,单步执行到这一步的时候,下一步就是执行这个函数了,执行这一步之后,马上退出了汇编指令的界面,回到了模拟器的界面。这就说明线程产生了休眠,不干事了,所以会退出。这也就说明了互斥锁在等待的时候会线程休眠。

通过汇编指令判断os_unfair_lock是自旋锁还是互斥锁

还是通过和前面两个锁一样的方法来查看,单步执行汇编指令,执行到最后到了下面的指令:

汇编指令执行到最后还是执行到了syscall这一步,这就说明os_unfair_lock在等待时线程是休眠的,也就证明了其是互斥锁。

NSLock

NSLock是对mutex普通锁的封装,所以它是一种互斥锁。

@interface NSLock : NSObject  {- (BOOL)tryLock;//在这个时间之前如果能等到这把锁放开,那么就给这把锁加锁,加锁成功,返回YES,如果到了规定的时间这把锁还是没有放开,那就加锁失败,返回NO。- (BOOL)lockBeforeDate:(NSDate *)limit;@end

其遵循的NSLocking协议如下:

@protocol NSLocking- (void)lock;- (void)unlock;@end

因此NSLock使用起来也是非常简单,创建:

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

上锁:

[lock lock];

解锁:

[lock unlock];

NSRecursiveLock递归锁

这个锁是对mutex递归锁的封装,也就是mutex锁的属性为PTHREAD_MUTEX_RECURSIVE,这就是NSRecursiveLock锁了,这个锁的API和NSLock基本一致:

@interface NSRecursiveLock : NSObject  {- (BOOL)tryLock;- (BOOL)lockBeforeDate:(NSDate *)limit;@end

其同样遵守NSLocking协议。在使用上与NSLock也是基本一致。

NSCondition

NSCondition是对mutex和cond的封装
其主要API如下:

@interface NSCondition : NSObject  {- (void)wait;- (BOOL)waitUntilDate:(NSDate *)limit;- (void)signal;- (void)broadcast;@end

下面举一个例子说明其应用:
有两条线程,一条线程对数组元素进行删除操作,一条进行添加操作。这个时候在做删除操作的时候就要格外小心,因为如果数组为空,进行删除操作就可能引发崩溃,这个时候就可以在删除操作中做个判断,如果元素数为0,那么就等待,线程进入休眠状态。同时,在添加元素的操作中也要做处理,当添加完元素后要发出一个信号,这个信号告诉删除的那条线程可以醒来继续处理了。

@interface NSConditionDemo()@property (nonatomic, strong)NSMutableArray *data;@property (nonatomic, strong)NSCondition *condition;@end@implementation NSConditionDemo- (instancetype)init{        if (self = [super init]) {                self.data = [[NSMutableArray alloc] init];        self.condition = [[NSCondition alloc] init];    }        return self;}- (void)__remove{        [self.condition lock];    NSLog(@"__rermove - begin");        if (self.data.count == 0) {        [self.condition wait];    }        [self.data removeLastObject];    NSLog(@"删除了元素");    [self.condition unlock];}- (void)__add{        [self.condition lock];    sleep(1.0);        [self.data addObject:@"test"];    [self.condition signal];    NSLog(@"添加了元素");        [self.condition unlock];}- (void)otherTest{        [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];}@end

首先调用的是remove操作,进入remove后先加锁,然后判断元素个数是否为0,如果是0那就让线程进入休眠,同时放开锁。然后执行add操作,进入add操作后马上加锁,当添加元素完成后就发出信号,这时remove那条线程就会被唤醒,但是由于add操作时加的锁还没有放开,所以remove线程还要等待锁放开才能继续执行,当锁放开后就能执行删除元素的操作了,完成之后就把锁放开。

dispatch_semaphore

semaphore叫做”信号量”
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

相关API如下:

        //信号量的初始值        int value = 1;        //初始化信号量        dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);        //如果c信号量的值0)        //如果信号量的值>0,就减1,然后往下执行后面的代码        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);        //让信号量的值加1        dispatch_semaphore_signal(semaphore);

下面以一个实例来讲解信号量的用法:
要创建20条线程,每条线程执行同样的方法,这样20条线程会对同样的代码执行同样的方法,现在要限制同时执行该方法的线程数为5,那么 就可以使用信号量:

@interface SemaphoreDemo()@property (strong ,nonatomic)dispatch_semaphore_t sempahore;@end@implementation SemaphoreDemo- (instancetype)init{        if (self = [super init]) {                self.sempahore = dispatch_semaphore_create(5);    }        return self;}- (void)otherTest{        for (int i = 0; i < 20; i++) {        [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];    }}- (void)test{        dispatch_semaphore_wait(_sempahore, DISPATCH_TIME_FOREVER);    //这是为了使效果更明显    sleep(1);    NSLog(@"test - %@", [NSThread currentThread]);        dispatch_semaphore_signal(_sempahore);}@end

第一条线程执行test方法时信号量的值是5,在dispatch_semaphore_wait()这里,当信号量>0时会让线程进入,然后信号量减1,当信号量=0时就会让线程在外面等待,直到信号量>0才让线程进入。进入的线程在执行完以后会进入dispatch_semaphore_signal(),这个方法让信号量加1。

如果要用信号量保证线程同步,只需要使最大并发线程数为1。

NSConditionLock

NSConditionlock是对NSCondition的进一步封装,可以设置具体的条件值

具体的API如下:

@interface NSConditionLock : NSObject  {- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;@property (readonly) NSInteger condition;- (void)lockWhenCondition:(NSInteger)condition;- (BOOL)tryLock;- (BOOL)tryLockWhenCondition:(NSInteger)condition;- (void)unlockWithCondition:(NSInteger)condition;- (BOOL)lockBeforeDate:(NSDate *)limit;- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;@end

比如我有三个任务,任务1,任务2,任务3,我想要让任务1完成后再执行任务2,任务2执行完后再执行任务3,那么这时就可以使用条件锁:

@interface NSConditionLockDemo()@property (nonatomic, strong)NSConditionLock *conditionLock;@end@implementation NSConditionLockDemo- (instancetype)init{      if (self = [super init]) {                self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];    }     return self;}- (void)task1{    //当这把锁内部所存储的条件值为1的时候就会进行加锁,否则就会在这里等待    [self.conditionLock lockWhenCondition:1];    NSLog(@"任务一");       //设置这把锁内部的条件值为2,同时把锁放开    [self.conditionLock unlockWithCondition:2];    }- (void)task2{    //当条件值为2且锁放开时加锁    [self.conditionLock lockWhenCondition:2];       NSLog(@"任务二");        //设置这把锁内部的i条件值为3,同时把锁放开    [self.conditionLock unlockWithCondition:3];}- (void)task3{        //当条件值为3且锁放开时加锁    [self.conditionLock lockWhenCondition:3];        NSLog(@"任务三");        [self.conditionLock unlock];}- (void)otherTest{        [[[NSThread alloc] initWithTarget:self selector:@selector(task1) object:nil] start];    [[[NSThread alloc] initWithTarget:self selector:@selector(task2) object:nil] start];    [[[NSThread alloc] initWithTarget:self selector:@selector(task3) object:nil] start];}@end

SerialQueue

线程同步的本质是不能让多条线程占用同一份资源,直接使用GCD的串行队列,也可以实现线程同步
例如卖票的方法,要让票一张一张的卖,那也可以使用串行队列,把卖票的方法加入串行队列中,这样就能实现一张票卖完了之后才开始卖下一张票。

@synchronized

@synchronized是对mutex递归锁的封装
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁,加锁操作
从代码简洁度来看是最简单的方案

在买票的程序里我们可以这样用@synchronized:

- (void)saleTicket{        static NSObject *lock;    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        lock = [[NSObject alloc] init];    });        //保证每次传入的是同一个对象    @synchronized (lock) {        [super saleTicket];    }}

@synchronized的括号里相当于就是一把锁,这就相当于是给括号里的一把锁上锁,大括号里就是要执行的东西。任何对象都可以传入括号里面当锁,但是为了让大括号内的代码同一时刻只能被执行一次,这就要求每个线程进来时用的锁是一样的,所以这里声明了一个static类型的NSObject对象,并用单例去创建它。

多线程同步方案性能对比

性能由高到低排序:

QQ截图20190409112841.png

什么情况下使用自旋锁比较划算?

  • 预计线程等待锁的时间很短

  • 加锁的代码经常被调用,但竞争情况很少发生

  • CPU资源不紧张

  • 多核处理器
    什么情况使用互斥锁比较划算?

  • 预计线程等待时间较长

  • 单核处理器

  • 加锁的代码有IO操作(耗性能)

atomic

我们都知道,属性修饰符中有nonatomic和atomic,但是我们在申明属性的时候好像用的都是nonatomic而不是atomic,这是为什么呢?atomic又是什么意思呢?

atomic用于保证属性setter,getter的原子性操作,相当于在getter和setter内部加了线程同步的锁,会进行加锁和解锁。
可以参考runtime源码的objc-accessors.mm文件。
它并不能保证使用属性的过程是线程安全的。

当我们声明一个属性的时候,系统会自动帮我们实现set和get方法,比如我们声明一个NSString类型的name属性,并用nonatomic来修饰,那么其set和get方法的默认实现如下:

- (NSString *)name{        return _name;}- (void)setName:(NSString *)name{        _name = name;}

上面是用nonatomic方法修饰属性,如果是用atomic修饰属性,那么就会在访问属性和设置属性的时候给其加上锁:

//保证内部的线程同步- (NSString *)name{    //加锁    return _name;    //解锁}- (void)setName:(NSString *)name{    //加锁    _name = name;    //解锁}

下面我们通过源码来证实一下:
打开runtime源码的objc-accessors.mm文件,先看取值方法:

QQ截图20190409112905.png
再看一下设值的方法:

QQ截图20190409112932.png

使用atomic确实可以保证set方法和get方法内部是线程安全的,但是它并不能保证使用属性的过程是线程安全的,这句话是什么意思呢?
比如说有一个data属性:

@property (atomic, strong)NSMutableArray *data;

那么下列代码是不是线程安全的呢:

        self.data = [[NSMutableArray alloc] init];                [self.data addObject:@"1"];        [self.data addObject:@"2"];        [self.data addObject:@"3"];

有人可能会想,这不就是取值和设值的操作吗?就是调用了set和get方法呀,而atomic修饰的属性,其set和get方法是线程安全的呀。上述代码可以等价于下面的:

        [self setData:[[NSMutableArray alloc] init]];                [[self data] addObject:@"1"];        [[self data] addObject:@"2"];        [[self data] addObject:@"3"];

问题出就出在,并不是只用了set和get方法,还有addObject方法呀,这可不是线程安全的,加入有多条线程同时执行addObject方法,它就不是安全的了。

由于set方法和get方法使用的非常多,而如果是用atomic修饰的话,那么每使用一次set或者get方法都会进行加锁和解锁,这样频繁的加锁和解锁是非常耗性能的,并且也不能保证使用属性的过程是线程安全的,因此一般不用atomic,转而用nonatomic。

  • iOS源码解析:多线程

作者:雪山飞狐_91ae
链接:https://www.swifty.cc/p/eff51e665ee5

赞(0) 打赏
未经允许不得转载:云微资讯 » iOS源码解析:多线程线程同步
分享到: 更多 (0)

云微资讯 科技新媒体资讯平台

关于我们联系我们

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏