47

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

 5 years ago
source link: http://www.cocoachina.com/ios/20190409/26753.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

多线程的安全隐患

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

下面以取钱存钱为例:

7zaAZrn.png!web

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

mqiia2E.png!web

起始票数是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 线程
<nsthread: nbsp="" 0x600000461740="">
 {number = 5, name = (null)}2018-09-26 15:12:52.746209+0800 TEST[10226:312193] 最后还剩的票数14 线程
 <nsthread: nbsp="" 0x600000460780="">
  {number = 4, name = (null)}2018-09-26 15:12:52.746245+0800 TEST[10226:312195] 最后还剩的票数14 线程
  <nsthread: nbsp="" 0x600000460500="">
   {number = 3, name = (null)}2018-09-26 15:12:52.746414+0800 TEST[10226:312194] 最后还剩的票数12 线程
   <nsthread: nbsp="" 0x600000461740="">
    {number = 5, name = (null)}2018-09-26 15:12:52.746552+0800 TEST[10226:312193] 最后还剩的票数11 线程
    <nsthread: nbsp="" 0x600000460780="">
     {number = 4, name = (null)}2018-09-26 15:12:52.746650+0800 TEST[10226:312195] 最后还剩的票数10 线程
     <nsthread: nbsp="" 0x600000460500="">
      {number = 3, name = (null)}2018-09-26 15:12:52.746707+0800 TEST[10226:312194] 最后还剩的票数9 线程
      <nsthread: nbsp="" 0x600000461740="">
       {number = 5, name = (null)}2018-09-26 15:12:52.746730+0800 TEST[10226:312193] 最后还剩的票数8 线程
       <nsthread: nbsp="" 0x600000460780="">
        {number = 4, name = (null)}2018-09-26 15:12:52.746913+0800 TEST[10226:312195] 最后还剩的票数7 线程
        <nsthread: nbsp="" 0x600000460500="">
         {number = 3, name = (null)}2018-09-26 15:12:52.747049+0800 TEST[10226:312194] 最后还剩的票数6 线程
         <nsthread: nbsp="" 0x600000461740="">
          {number = 5, name = (null)}2018-09-26 15:12:52.747301+0800 TEST[10226:312193] 最后还剩的票数5 线程
          <nsthread: nbsp="" 0x600000460780="">
           {number = 4, name = (null)}2018-09-26 15:12:52.747861+0800 TEST[10226:312194] 最后还剩的票数4 线程
           <nsthread: nbsp="" 0x600000461740="">
            {number = 5, name = (null)}2018-09-26 15:12:52.747861+0800 TEST[10226:312195] 最后还剩的票数4 线程
            <nsthread: nbsp="" 0x600000460500="">
             {number = 3, name = (null)}2018-09-26 15:12:52.748157+0800 TEST[10226:312193] 最后还剩的票数3 线程
             <nsthread: nbsp="" 0x600000460780="">
              {number = 4, name = (null)}2018-09-26 15:12:52.749157+0800 TEST[10226:312195] 最后还剩的票数2 线程
              <nsthread: nbsp="" 0x600000460500="">
               {number = 3, name = (null)}
              </nsthread:>
             </nsthread:>
            </nsthread:>
           </nsthread:>
          </nsthread:>
         </nsthread:>
        </nsthread:>
       </nsthread:>
      </nsthread:>
     </nsthread:>
    </nsthread:>
   </nsthread:>
  </nsthread:>
 </nsthread:>
</nsthread:>

可以看到产生了混乱,最后剩余的票数并不为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元 - 
<nsthread: nbsp="" 0x604000269c80="">
 {number = 4, name = (null)}2018-09-26 15:27:13.265459+0800 TEST[10568:324337] 存50 还剩150元 - 
 <nsthread: nbsp="" 0x60400026b180="">
  {number = 3, name = (null)}2018-09-26 15:27:13.265587+0800 TEST[10568:324337] 存50 还剩180元 - 
  <nsthread: nbsp="" 0x60400026b180="">
   {number = 3, name = (null)}2018-09-26 15:27:13.265589+0800 TEST[10568:324343] 取20 还剩130元 - 
   <nsthread: nbsp="" 0x604000269c80="">
    {number = 4, name = (null)}2018-09-26 15:27:13.265685+0800 TEST[10568:324337] 存50 还剩230元 - 
    <nsthread: nbsp="" 0x60400026b180="">
     {number = 3, name = (null)}2018-09-26 15:27:13.265693+0800 TEST[10568:324343] 取20 还剩210元 - 
     <nsthread: nbsp="" 0x604000269c80="">
      {number = 4, name = (null)}2018-09-26 15:27:13.265771+0800 TEST[10568:324337] 存50 还剩260元 - 
      <nsthread: nbsp="" 0x60400026b180="">
       {number = 3, name = (null)}2018-09-26 15:27:13.265853+0800 TEST[10568:324343] 取20 还剩240元 - 
       <nsthread: nbsp="" 0x604000269c80="">
        {number = 4, name = (null)}2018-09-26 15:27:13.266059+0800 TEST[10568:324337] 存50 还剩290元 - 
        <nsthread: nbsp="" 0x60400026b180="">
         {number = 3, name = (null)}2018-09-26 15:27:13.266210+0800 TEST[10568:324343] 取20 还剩270元 - 
         <nsthread: nbsp="" 0x604000269c80="">
          {number = 4, name = (null)}2018-09-26 15:27:13.266343+0800 TEST[10568:324337] 存50 还剩320元 - 
          <nsthread: nbsp="" 0x60400026b180="">
           {number = 3, name = (null)}2018-09-26 15:27:13.266485+0800 TEST[10568:324343] 取20 还剩300元 - 
           <nsthread: nbsp="" 0x604000269c80="">
            {number = 4, name = (null)}2018-09-26 15:27:13.266667+0800 TEST[10568:324337] 存50 还剩350元 - 
            <nsthread: nbsp="" 0x60400026b180="">
             {number = 3, name = (null)}2018-09-26 15:27:13.266844+0800 TEST[10568:324343] 取20 还剩330元 - 
             <nsthread: nbsp="" 0x604000269c80="">
              {number = 4, name = (null)}2018-09-26 15:27:13.267284+0800 TEST[10568:324337] 存50 还剩380元 - 
              <nsthread: nbsp="" 0x60400026b180="">
               {number = 3, name = (null)}2018-09-26 15:27:13.267373+0800 TEST[10568:324343] 取20 还剩360元 - 
               <nsthread: nbsp="" 0x604000269c80="">
                {number = 4, name = (null)}2018-09-26 15:27:13.267496+0800 TEST[10568:324337] 存50 还剩410元 - 
                <nsthread: nbsp="" 0x60400026b180="">
                 {number = 3, name = (null)}2018-09-26 15:27:13.267866+0800 TEST[10568:324343] 取20 还剩390元 - 
                 <nsthread: nbsp="" 0x604000269c80="">
                  {number = 4, name = (null)}2018-09-26 15:27:13.268062+0800 TEST[10568:324337] 存50 还剩440元 - 
                  <nsthread: nbsp="" 0x60400026b180="">
                   {number = 3, name = (null)}2018-09-26 15:27:13.268578+0800 TEST[10568:324343] 取20 还剩420元 - 
                   <nsthread: nbsp="" 0x604000269c80="">
                    {number = 4, name = (null)}
                   </nsthread:>
                  </nsthread:>
                 </nsthread:>
                </nsthread:>
               </nsthread:>
              </nsthread:>
             </nsthread:>
            </nsthread:>
           </nsthread:>
          </nsthread:>
         </nsthread:>
        </nsthread:>
       </nsthread:>
      </nsthread:>
     </nsthread:>
    </nsthread:>
   </nsthread:>
  </nsthread:>
 </nsthread:>
</nsthread:>

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

那么多线程的安全隐患怎么解决呢? 解决方案就是使用线程同步技术,常见的线程同步技术是加锁。

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

可以看到现在的输出没有任何问题了。

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

下面再用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元 - 
<nsthread: nbsp="" 0x604000471e80="">
 {number = 3, name = (null)}2018-09-26 16:45:14.317953+0800 TEST[12223:379269] 存50 还剩200元 - 
 <nsthread: nbsp="" 0x604000471e80="">
  {number = 3, name = (null)}2018-09-26 16:45:14.318071+0800 TEST[12223:379269] 存50 还剩250元 - 
  <nsthread: nbsp="" 0x604000471e80="">
   {number = 3, name = (null)}2018-09-26 16:45:14.318182+0800 TEST[12223:379269] 存50 还剩300元 - 
   <nsthread: nbsp="" 0x604000471e80="">
    {number = 3, name = (null)}2018-09-26 16:45:14.318374+0800 TEST[12223:379269] 存50 还剩350元 - 
    <nsthread: nbsp="" 0x604000471e80="">
     {number = 3, name = (null)}2018-09-26 16:45:14.318500+0800 TEST[12223:379269] 存50 还剩400元 - 
     <nsthread: nbsp="" 0x604000471e80="">
      {number = 3, name = (null)}2018-09-26 16:45:14.318587+0800 TEST[12223:379269] 存50 还剩450元 - 
      <nsthread: nbsp="" 0x604000471e80="">
       {number = 3, name = (null)}2018-09-26 16:45:14.318689+0800 TEST[12223:379269] 存50 还剩500元 - 
       <nsthread: nbsp="" 0x604000471e80="">
        {number = 3, name = (null)}2018-09-26 16:45:14.318823+0800 TEST[12223:379269] 存50 还剩550元 - 
        <nsthread: nbsp="" 0x604000471e80="">
         {number = 3, name = (null)}2018-09-26 16:45:14.319047+0800 TEST[12223:379269] 存50 还剩600元 - 
         <nsthread: nbsp="" 0x604000471e80="">
          {number = 3, name = (null)}2018-09-26 16:45:14.320129+0800 TEST[12223:379270] 取20 还剩580元 - 
          <nsthread: nbsp="" 0x60000027d080="">
           {number = 4, name = (null)}2018-09-26 16:45:14.320242+0800 TEST[12223:379270] 取20 还剩560元 - 
           <nsthread: nbsp="" 0x60000027d080="">
            {number = 4, name = (null)}2018-09-26 16:45:14.320347+0800 TEST[12223:379270] 取20 还剩540元 - 
            <nsthread: nbsp="" 0x60000027d080="">
             {number = 4, name = (null)}2018-09-26 16:45:14.320459+0800 TEST[12223:379270] 取20 还剩520元 - 
             <nsthread: nbsp="" 0x60000027d080="">
              {number = 4, name = (null)}2018-09-26 16:45:14.320588+0800 TEST[12223:379270] 取20 还剩500元 - 
              <nsthread: nbsp="" 0x60000027d080="">
               {number = 4, name = (null)}2018-09-26 16:45:14.320693+0800 TEST[12223:379270] 取20 还剩480元 - 
               <nsthread: nbsp="" 0x60000027d080="">
                {number = 4, name = (null)}2018-09-26 16:45:14.320900+0800 TEST[12223:379270] 取20 还剩460元 - 
                <nsthread: nbsp="" 0x60000027d080="">
                 {number = 4, name = (null)}2018-09-26 16:45:14.321222+0800 TEST[12223:379270] 取20 还剩440元 - 
                 <nsthread: nbsp="" 0x60000027d080="">
                  {number = 4, name = (null)}2018-09-26 16:45:14.321331+0800 TEST[12223:379270] 取20 还剩420元 - 
                  <nsthread: nbsp="" 0x60000027d080="">
                   {number = 4, name = (null)}2018-09-26 16:45:14.321548+0800 TEST[12223:379270] 取20 还剩400元 - 
                   <nsthread: nbsp="" 0x60000027d080="">
                    {number = 4, name = (null)}
                   </nsthread:>
                  </nsthread:>
                 </nsthread:>
                </nsthread:>
               </nsthread:>
              </nsthread:>
             </nsthread:>
            </nsthread:>
           </nsthread:>
          </nsthread:>
         </nsthread:>
        </nsthread:>
       </nsthread:>
      </nsthread:>
     </nsthread:>
    </nsthread:>
   </nsthread:>
  </nsthread:>
 </nsthread:>
</nsthread:>

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

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

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的类文件中打下断点:

3EziyuZ.png!web

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

下面是第二条线程执行到断点处时进入汇编:

a2aAZbZ.png!web

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

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

EnqMfeZ.png!web

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

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

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

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

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

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

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

NSLock

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

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

其遵循的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 
<nslocking>
  {

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;@end
</nslocking>

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

NSCondition

NSCondition是对mutex和cond的封装

其主要API如下:

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

下面举一个例子说明其应用:

有两条线程,一条线程对数组元素进行删除操作,一条进行添加操作。这个时候在做删除操作的时候就要格外小心,因为如果数组为空,进行删除操作就可能引发崩溃,这个时候就可以在删除操作中做个判断,如果元素数为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)
        //如果信号量的值>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 
<nslocking>
  {

- (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
</nslocking>

比如我有三个任务,任务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对象,并用单例去创建它。

多线程同步方案性能对比

性能由高到低排序:

Z3I3Qz3.png!web
什么情况下使用自旋锁比较划算?
  • 预计线程等待锁的时间很短

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

  • 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文件,先看取值方法:

URBZjuQ.png!web再看一下设值的方法:

73Yf2iU.png!web

使用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。

作者:雪山飞狐_91ae

链接:https://www.jianshu.com/p/eff51e665ee5


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK