1. 程式人生 > >多執行緒六——加鎖方案一 : OSSpinLock

多執行緒六——加鎖方案一 : OSSpinLock

一、iOS 中的執行緒同步方案 -> 加鎖

  • OSSpinLock:自旋鎖
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • @synchronized

二、OSSpinLock

  1. 含義
  • OSSpinLock叫做”自旋鎖”,等待鎖的執行緒會處於忙等(busy-wait)狀態,一直佔用著CPU資源
  • 目前已經不再安全,可能會出現優先順序反轉問題
    • 如果等待鎖的執行緒優先順序較高,它會一直佔用著CPU資源,優先順序低的執行緒就無法釋放鎖
    • 需要匯入標頭檔案#import <libkern/OSAtomic.h>
  1. 主要程式碼 在這裡插入圖片描述

三、 示例程式: 賣票

  • 希望關鍵程式加鎖

沒加鎖程式碼:

/// 賣1張票
- (void)saleTicket {
    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 < 3; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0 ; i < 3; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0 ; i < 3; i++) {
            [self saleTicket];
        }
    });
}

加鎖程式碼: 加鎖: 別的執行緒無法再進行訪問 解鎖: 用完後,解除鎖定,讓其他執行緒可以訪問。 如果使用須匯入 #import <libkern/OSAtomic.h>

第一次加鎖:在 saleTicket 方法中 ,一開始就加鎖

/// 賣1張票
- (void)saleTicket {    
    // 加鎖 - 初始化 - iOS10 已經棄用
    OSSpinLock look = OS_SPINLOCK_INIT;
    // 加鎖, 因為要傳的是指標,所以傳入地址
    OSSpinLockLock(&look);
    
    // 原來程式碼
    int oldTicketsCount = self.ticketsCount;
    sleep(.2);
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;    
    NSLog(@"還剩 %d 張票 -%@", oldTicketsCount, [NSThread currentThread]);
    
    // 解鎖
    OSSpinLockUnlock(&look);
}

執行結果: 在這裡插入圖片描述

  • 可以看到,這樣的加鎖 ,跟沒有加一樣,票數還是錯誤的。
  • 這是因為 OSSpinLock look = OS_SPINLOCK_INIT; 是區域性變數,每次進入到 saleTicket這個方法的時候,都會重新建立。
  • 修改為:
@property(nonatomic,assign) OSSpinLock look;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 加鎖 - 初始化 - iOS10 已經棄用
    _look = OS_SPINLOCK_INIT;
    
    [self ticketTest];
}

/// 賣1張票
- (void)saleTicket {
    // 加鎖, 因為要傳的是指標,所以傳入地址
    OSSpinLockLock(&_look);
    
    // 原來程式碼
    int oldTicketsCount = self.ticketsCount;
    sleep(.2);
    oldTicketsCount--;
    self.ticketsCount = oldTicketsCount;
    
    NSLog(@"還剩 %d 張票 -%@", oldTicketsCount, [NSThread currentThread]);
    
    // 解鎖
    OSSpinLockUnlock(&_look);
}

列印結果: 在這裡插入圖片描述

  • 列印結果可以看到:剩餘票數正常

四、示例程式:存錢取錢 *沒有加鎖的 寫法

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 加鎖 - 初始化 - iOS10 已經棄用
    _look = OS_SPINLOCK_INIT;
    [self moneyTest];
}

/// 存錢,取錢演示
- (void)moneyTest {
    self.money = 50;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0 ; i < 5; i++) {
            [self saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0 ; i < 5; i++) {
            [self drawMoney];
        }
    });
}

/// 取錢
- (void)drawMoney {
    int oldMoney = self.money;
    sleep(.2);
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@"取 20 , 還剩 %d 元 - %@",oldMoney, [NSThread currentThread]);
}

/// 存錢
- (void)saveMoney {
    int oldMoney = self.money;
    sleep(.2);
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@"存 50 , 還剩 %d 元 - %@",oldMoney, [NSThread currentThread]);
}

執行結果:沒有對的 在這裡插入圖片描述

  • 應該如何加鎖?
    • 是 存錢一把鎖,取錢一把鎖
    • 還是 他們兩個共用一把鎖

思考: * 存錢取錢 是否能同時執行? * 不能 * 這兩個操作在同一時間段只能執行一個 。 * 所以可以使用一把鎖 * 如果 鎖不一樣,意味著 同一時間段可以兩個一起執行。 * 只有大家共用一把鎖,才能保證同一時間段只能執行一個操作。

加鎖之後的 程式碼

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 加鎖 - 初始化 - iOS10 已經棄用
    _look = OS_SPINLOCK_INIT;
    [self moneyTest];
}

/// 存錢,取錢演示
- (void)moneyTest {
    self.money = 50;
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (int i = 0 ; i < 5; i++) {
            [self saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0 ; i < 5; i++) {
            [self drawMoney];
        }
    });
}

/// 取錢
- (void)drawMoney {
    
    OSSpinLockLock(&_look);
    int oldMoney = self.money;
    sleep(.2);
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@"取 20 , 還剩 %d 元 - %@",oldMoney, [NSThread currentThread]);
    
    // 解鎖
    OSSpinLockUnlock(&_look);
}

/// 存錢
- (void)saveMoney {
    
    OSSpinLockLock(&_look);
    int oldMoney = self.money;
    sleep(.2);
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@"存 50 , 還剩 %d 元 - %@",oldMoney, [NSThread currentThread]);
    
    // 解鎖
    OSSpinLockUnlock(&_look);
}

五、存錢取錢 和 賣票

如果存錢取錢 和 賣票 使用同一把鎖,會出現什麼事情?

  • 意味著,同一時間段,只能執行一個操作,要不存錢,要不取錢,要不 賣票
  • 但沒有必要,效率會降低
  • 因為 存錢取錢 和 賣票 不是一個事情,訪問的變數不一樣。賣票訪問的是 ticketsCount 變數。 存錢取錢 訪問的是 money 變數。兩個互不相干。
  • 只有當多個執行緒訪問同一個變數(資源)時,才需要使用同一把鎖。