主頁 > 移動端開發 > iOS底層原理(七)多執行緒(中)

iOS底層原理(七)多執行緒(中)

2021-04-09 07:07:42 移動端開發

多執行緒的安全隱患

一塊資源可能會被多個執行緒共享,也就是多個執行緒可能會訪問同一塊資源;當多個執行緒訪問同一塊資源時,很容易引發資料錯亂和資料安全問題### 問題案例
賣票和存錢取錢的兩個案例,具體見下面代碼

@interface BaseDemo: NSObject

- (void)moneyTest;
- (void)ticketTest;

#pragma mark - 暴露給子類去使用
- (void)__saveMoney;
- (void)__drawMoney;
- (void)__saleTicket;
@end

@interface BaseDemo()
@property (assign, nonatomic) int money;
@property (assign, nonatomic) 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]);
}

/**
 賣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 < 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


@interface ViewController ()
@property (strong, nonatomic) BaseDemo *demo;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    BaseDemo *demo = [[BaseDemo alloc] init];
    [demo ticketTest];
    [demo moneyTest];
}
@end

解決方案是使用執行緒同步技術(同步,就是協同步調,按預定的先后次序進行,常見的方案就是加鎖

執行緒同步方案

iOS中的執行緒同步方案有以下這些

  • OSSpinLock- os_unfair_lock- pthread_mutex- dispatch_semaphore- dispatch_queue(DISPATCH_QUEUE_SERIAL)- NSLock- NSRecursiveLock- NSCondition- NSConditionLock- @synchronized#### OSSpinLock
    OSSpinLock叫做”自旋鎖”,等待鎖的執行緒會處于忙等(busy-wait)狀態,一直占用著CPU資源OSSpinLock來解決上述示例問題
@interface OSSpinLockDemo: BaseDemo

@end

#import "OSSpinLockDemo.h"
#import <libkern/OSAtomic.h>

@interface OSSpinLockDemo()
@property (assign, nonatomic) OSSpinLock moneyLock;
// @property (assign, nonatomic) OSSpinLock ticketLock;
@end

@implementation OSSpinLockDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.moneyLock = OS_SPINLOCK_INIT;
        // self.ticketLock = OS_SPINLOCK_INIT;
    }
    return self;
}

- (void)__drawMoney
{
    OSSpinLockLock(&_moneyLock);
    
    [super __drawMoney];
    
    OSSpinLockUnlock(&_moneyLock);
}

- (void)__saveMoney
{
    OSSpinLockLock(&_moneyLock);
    
    [super __saveMoney];
    
    OSSpinLockUnlock(&_moneyLock);
}

- (void)__saleTicket
{
	// 不用屬性,用一個靜態變數也可以
	static OSSpinLock ticketLock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&ticketLock);
    
    [super __saleTicket];
    
    OSSpinLockUnlock(&ticketLock);
}

@end
static的問題

上面的ticketLock也可以用static來修飾作為內部靜態變數來使用

#define	OS_SPINLOCK_INIT    0

由于OS_SPINLOCK_INIT就是0,所以才可以用static來修飾;static只能在編譯時賦值一個確定值,不能動態賦予一個函式值

// 這樣賦值一個函式回傳值是會報錯的
static OSSpinLock ticketLock = [NSString stringWithFormat:@"haha"];
OSSpinLock的問題

OSSpinLock現在已經不再安全,可能會出現優先級反轉問題

由于多執行緒的本質是在不同執行緒之間進行來回的調度,每個執行緒可能對應分配的資源優先級不同;如果優先級低的執行緒先進行了加鎖并準備執行代碼,這時優先級高的執行緒就會在外面回圈等待加鎖;但因為其優先級高,所以CPU可能會大量的給其分配任務,那么就沒辦法處理優先級低的執行緒;優先級低的執行緒就無法繼續往下執行代碼,那么也就沒辦法解鎖,所以又會變成了互相等待的局面,造成死鎖,這也是蘋果現在廢棄了OSSpinLock的原因

解決辦法

用嘗試加鎖OSSpinLockTry來替換OSSpinLockLock,如果沒有加鎖才會進到判斷里執行代碼并加鎖,避免了因上鎖了一直在回圈等待的問題

// 用賣票的函式來舉例,其他幾個加鎖的方法也是同樣
- (void)__saleTicket
{
    if (OSSpinLockTry(&_ticketLock)) {
        [super __saleTicket];
        
        OSSpinLockUnlock(&_ticketLock);
    }
}
通過匯編來分析

我們通過斷點來分析加鎖之后做了什么

我們在賣票的加鎖代碼處打上斷點,并通過轉匯編的方式一步步呼叫分析

轉成匯編后呼叫OSSpinLockLock

內部會呼叫_OSSpinLockLockSlow

核心部分,在_OSSpinLockLockSlow會進行比較,然后執行到斷點處又會再次跳回0x7fff5e73326f再次執行代碼


所以通過匯編底層執行邏輯,我們能看出OSSpinLock是會不斷回圈去呼叫判斷的,只有解鎖之后才會往下執行代碼

鎖的等級

OSSpinLock自旋鎖是高等級的鎖(High-level lock),因為會一直回圈呼叫

os_unfair_lock

蘋果現在用os_unfair_lock用于取代不安全的OSSpinLock ,從iOS10開始才支持

從底層呼叫看,等待os_unfair_lock鎖的執行緒會處于休眠狀態,并非忙等

修改示例代碼如下

#import "BaseDemo.h"

@interface OSUnfairLockDemo: BaseDemo

@end

#import "OSUnfairLockDemo.h"
#import <os/lock.h>

@interface OSUnfairLockDemo()

@property (assign, nonatomic) os_unfair_lock moneyLock;
@property (assign, nonatomic) os_unfair_lock ticketLock;
@end

@implementation OSUnfairLockDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.moneyLock = OS_UNFAIR_LOCK_INIT;
        self.ticketLock = OS_UNFAIR_LOCK_INIT;
    }
    return self;
}

- (void)__saleTicket
{
    os_unfair_lock_lock(&_ticketLock);
    
    [super __saleTicket];
    
    os_unfair_lock_unlock(&_ticketLock);
}

- (void)__saveMoney
{
    os_unfair_lock_lock(&_moneyLock);
    
    [super __saveMoney];
    
    os_unfair_lock_unlock(&_moneyLock);
}

- (void)__drawMoney
{
    os_unfair_lock_lock(&_moneyLock);
    
    [super __drawMoney];
    
    os_unfair_lock_unlock(&_moneyLock);
}

@end

如果不寫os_unfair_lock_unlock,那么所有的執行緒都會卡在os_unfair_lock_lock進入睡眠,不會再繼續執行代碼,這種情況叫做死鎖

通過匯編來分析

我們也通過斷點來分析加鎖之后做了什么

首先會呼叫os_unfair_lock_lock

然后會呼叫os_unfair_lock_lock_slow

然后在os_unfair_lock_lock_slow中會執行__ulock_wait


核心部分,代碼執行到syscall會直接跳出斷點,不再執行代碼,也就是進入了睡眠


所以通過匯編底層執行邏輯,我們能看出os_unfair_lock一旦進行了加鎖,就會直接進入休眠,等待解鎖后喚醒再繼續執行代碼,也由此可以認為os_unfair_lock是互斥鎖

syscall的呼叫可以理解為系統級別的呼叫進入睡眠,會直接卡住執行緒,不再執行代碼

鎖的等級

我們進到os_unfair_lock的頭檔案lock.h,可以看到注釋說明os_unfair_lock是一個低等級的鎖(Low-level lock),因為一旦發現加鎖后就會自動進入睡眠

pthread_mutex

互斥鎖

mutex叫做”互斥鎖”,等待鎖的執行緒會處于休眠狀態

使用代碼如下

@interface MutexDemo: BaseDemo

@end

#import "MutexDemo.h"
#import <pthread.h>

@interface MutexDemo()
@property (assign, nonatomic) pthread_mutex_t ticketMutex;
@property (assign, nonatomic) pthread_mutex_t moneyMutex;
@end

@implementation MutexDemo

- (void)__initMutex:(pthread_mutex_t *)mutex
{
    // 靜態初始化
    // 需要在定義這個變數時給予值才可以這么寫
    //        pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
//    // 初始化屬性
//    pthread_mutexattr_t attr;
//    pthread_mutexattr_init(&attr);
//    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
//    // 初始化鎖
//    pthread_mutex_init(mutex, &attr);
    
    // &attr傳NULL默認就是PTHREAD_MUTEX_DEFAULT
    pthread_mutex_init(mutex, NULL);
}

- (instancetype)init
{
    if (self = [super init]) {
        [self __initMutex:&_ticketMutex];
        [self __initMutex:&_moneyMutex];
    }
    return self;
}

- (void)__saleTicket
{
    pthread_mutex_lock(&_ticketMutex);
    
    [super __saleTicket];
    
    pthread_mutex_unlock(&_ticketMutex);
}

- (void)__saveMoney
{
    pthread_mutex_lock(&_moneyMutex);
    
    [super __saveMoney];
    
    pthread_mutex_unlock(&_moneyMutex);
}

- (void)__drawMoney
{
    pthread_mutex_lock(&_moneyMutex);
    
    [super __drawMoney];
    
    pthread_mutex_unlock(&_moneyMutex);
}

- (void)dealloc
{
  
  // 物件銷毀時呼叫
  	pthread_mutex_destroy(&_moneyMutex);
    pthread_mutex_destroy(&_ticketMutex);
}

pthread_mutex_t實際就是pthread_mutex *型別

遞回鎖

當屬性設定為PTHREAD_MUTEX_RECURSIVE時,就可以作為遞回鎖來使用

遞回鎖允許同一個執行緒對一把鎖進行重復加鎖;多個執行緒不可以用遞回鎖

- (void)__initMutex:(pthread_mutex_t *)mutex
{
    // 初始化屬性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化鎖
    pthread_mutex_init(mutex, &attr);
}

- (void)otherTest
{
    pthread_mutex_lock(&_mutex);
    
    NSLog(@"%s", __func__);
    
    static int count = 0;
    if (count < 10) {
        count++;
        [self otherTest];
    }
    
    pthread_mutex_unlock(&_mutex);
}

根據條件來進行加鎖

我們可以設定一定的條件來選擇執行緒之間的呼叫進行加鎖解鎖,示例如下

@interface MutexDemo()
@property (assign, nonatomic) pthread_mutex_t mutex;
@property (assign, nonatomic) pthread_cond_t cond;
@property (strong, nonatomic) NSMutableArray *data;
@end

@implementation MutexDemo

- (instancetype)init
{
    if (self = [super init]) {
        // 初始化鎖
        pthread_mutex_init(&_mutex, NULL);        
        // 初始化條件
        pthread_cond_init(&_cond, NULL);
        
        self.data = https://www.cnblogs.com/funkyRay/archive/2021/04/08/[NSMutableArray array];
    }
    return self;
}

- (void)otherTest
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 執行緒1
// 洗掉陣列中的元素
- (void)__remove
{
    pthread_mutex_lock(&_mutex);
    NSLog(@"__remove - begin");
    
    // 如果資料為空,那么設定條件等待喚醒
    // 等待期間會先解鎖,讓其他執行緒執行代碼
    if (self.data.count == 0) {
        pthread_cond_wait(&_cond, &_mutex);
    }
    
    [self.data removeLastObject];
    NSLog(@"洗掉了元素");
    
    pthread_mutex_unlock(&_mutex);
}

// 執行緒2
// 往陣列中添加元素
- (void)__add
{
    pthread_mutex_lock(&_mutex);
    
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    
    // 一旦添加了元素,變發送條件信號,讓等待洗掉的條件繼續執行代碼,并再次加鎖
    // 信號(通知一個條件)
    pthread_cond_signal(&_cond);
    // 廣播(通知所有條件)
//    pthread_cond_broadcast(&_cond);
    
    pthread_mutex_unlock(&_mutex);
}

- (void)dealloc
{
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
}

@end
通過匯編來分析

我們通過斷點來分析加鎖之后做了什么

首先會執行pthread_mutex_lock

然后會執行pthread_mutex_firstfit_lock_slow


然后會執行pthread_mutex_firstfit_lock_wait


然后會執行__psynch_mutexwait

核心部分,在__psynch_mutexwait里,代碼執行到syscall會直接跳出斷點,不再執行代碼,也就是進入了睡眠


所以pthread_mutexos_unfair_lock一樣,都是在加鎖之后會進入到睡眠

鎖的等級

pthread_mutexos_unfair_lock一樣,都是低等級的鎖(Low-level lock)

NSLock

NSLock是對mutex普通鎖的封裝

NSLock遵守了<NSLocking>協議,支持以下兩個方法

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

其他常用方法

// 嘗試解鎖
- (BOOL)tryLock;

// 設定一個時間等待加鎖,時間到了如果還不能成功加鎖就回傳NO
- (BOOL)lockBeforeDate:(NSDate *)limit;

具體使用看下面代碼

@interface NSLockDemo: BaseDemo

@end

@interface NSLockDemo()
@property (strong, nonatomic) NSLock *ticketLock;
@property (strong, nonatomic) NSLock *moneyLock;
@end

@implementation NSLockDemo


- (instancetype)init
{
    if (self = [super init]) {
        self.ticketLock = [[NSLock alloc] init];
        self.moneyLock = [[NSLock alloc] init];
    }
    return self;
}

- (void)__saleTicket
{
    [self.ticketLock lock];
    
    [super __saleTicket];
    
    [self.ticketLock unlock];
}

- (void)__saveMoney
{
    [self.moneyLock lock];
    
    [super __saveMoney];
    
    [self.moneyLock unlock];
}

- (void)__drawMoney
{
    [self.moneyLock lock];
    
    [super __drawMoney];
    
    [self.moneyLock unlock];
}

@end
分析底層實作

由于NSLock是不開源的,我們可以通過GNUstep Base來分析具體實作

找到NSLock.m可以看到initialize初始化方法里是創建的pthread_mutex_t物件,所以可以確定NSLock是對pthread_mutex的面向物件的封裝

@implementation NSLock

+ (id) allocWithZone: (NSZone*)z
{
  if (self == baseLockClass && YES == traceLocks)
    {
      return class_createInstance(tracedLockClass, 0);
    }
  return class_createInstance(self, 0);
}

+ (void) initialize
{
  static BOOL	beenHere = NO;

  if (beenHere == NO)
    {
      beenHere = YES;

      /* Initialise attributes for the different types of mutex.
       * We do it once, since attributes can be shared between multiple
       * mutexes.
       * If we had a pthread_mutexattr_t instance for each mutex, we would
       * either have to store it as an ivar of our NSLock (or similar), or
       * we would potentially leak instances as we couldn't destroy them
       * when destroying the NSLock.  I don't know if any implementation
       * of pthreads actually allocates memory when you call the
       * pthread_mutexattr_init function, but they are allowed to do so
       * (and deallocate the memory in pthread_mutexattr_destroy).
       */
      pthread_mutexattr_init(&attr_normal);
      pthread_mutexattr_settype(&attr_normal, PTHREAD_MUTEX_NORMAL);
      pthread_mutexattr_init(&attr_reporting);
      pthread_mutexattr_settype(&attr_reporting, PTHREAD_MUTEX_ERRORCHECK);
      pthread_mutexattr_init(&attr_recursive);
      pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);

      /* To emulate OSX behavior, we need to be able both to detect deadlocks
       * (so we can log them), and also hang the thread when one occurs.
       * the simple way to do that is to set up a locked mutex we can
       * force a deadlock on.
       */
      pthread_mutex_init(&deadlock, &attr_normal);
      pthread_mutex_lock(&deadlock);

      baseConditionClass = [NSCondition class];
      baseConditionLockClass = [NSConditionLock class];
      baseLockClass = [NSLock class];
      baseRecursiveLockClass = [NSRecursiveLock class];

      tracedConditionClass = [GSTracedCondition class];
      tracedConditionLockClass = [GSTracedConditionLock class];
      tracedLockClass = [GSTracedLock class];
      tracedRecursiveLockClass = [GSTracedRecursiveLock class];

      untracedConditionClass = [GSUntracedCondition class];
      untracedConditionLockClass = [GSUntracedConditionLock class];
      untracedLockClass = [GSUntracedLock class];
      untracedRecursiveLockClass = [GSUntracedRecursiveLock class];
    }
}

NSRecursiveLock

NSRecursiveLock也是對mutex遞回鎖的封裝,APINSLock基本一致

NSCondition

NSCondition是對mutexcond的封裝

具體使用代碼如下

@interface NSConditionDemo()
@property (strong, nonatomic) NSCondition *condition;
@property (strong, nonatomic) NSMutableArray *data;
@end

@implementation NSConditionDemo

- (instancetype)init
{
    if (self = [super init]) {
        self.condition = [[NSCondition alloc] init];
        self.data = https://www.cnblogs.com/funkyRay/archive/2021/04/08/[NSMutableArray array];
    }
    return self;
}

- (void)otherTest
{
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 執行緒1
// 洗掉陣列中的元素
- (void)__remove
{
    [self.condition lock];
    NSLog(@"__remove - begin");
    
    if (self.data.count == 0) {
        // 等待
        [self.condition wait];
    }
    
    [self.data removeLastObject];
    NSLog(@"洗掉了元素");
    
    [self.condition unlock];
}

// 執行緒2
// 往陣列中添加元素
- (void)__add
{
    [self.condition lock];
    
    sleep(1);
    
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    // 信號
    [self.condition signal];
    
    // 廣播
//    [self.condition broadcast];
    [self.condition unlock];
    
}
@end
```##### 分析底層實作`NSCondition`也遵守了`NSLocking`協議,說明其內部已經封裝了鎖的相關代碼

@interface NSCondition : NSObject {
@private
void *_priv;
}

  • (void)wait;
  • (BOOL)waitUntilDate:(NSDate *)limit;
  • (void)signal;
  • (void)broadcast;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

我們通過`GNUstep Base`也可以看到其初始化方法里對`pthread_mutex_t`進行了封裝

@implementation NSCondition

  • (id) allocWithZone: (NSZone*)z
    {
    if (self == baseConditionClass && YES == traceLocks)
    {
    return class_createInstance(tracedConditionClass, 0);
    }
    return class_createInstance(self, 0);
    }

  • (void) initialize
    {
    [NSLock class]; // Ensure mutex attributes are set up.
    }

  • (id) init
    {
    if (nil != (self = [super init]))
    {
    if (0 != pthread_cond_init(&_condition, NULL))
    {
    DESTROY(self);
    }
    else if (0 != pthread_mutex_init(&_mutex, &attr_reporting))
    {
    pthread_cond_destroy(&_condition);
    DESTROY(self);
    }
    }
    return self;
    }

#### NSConditionLock
`NSConditionLock`是對`NSCondition`的進一步封裝,可以設定具體的條件值

通過設定條件值可以對執行緒做依賴控制執行順序,具體使用見示例代碼

@interface NSConditionLockDemo()
@property (strong, nonatomic) NSConditionLock *conditionLock;
@end

@implementation NSConditionLockDemo

  • (instancetype)init
    {
    // 創建的時候可以設定一個條件
    // 如果不設定,默認就是0
    if (self = [super init]) {
    self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    }
    return self;
    }

  • (void)otherTest
    {
    [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];

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

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

  • (void)__one
    {
    // 不需要任何條件,只有沒有鎖就加鎖
    [self.conditionLock lock];

    NSLog(@"__one");
    sleep(1);

    [self.conditionLock unlockWithCondition:2];
    }

  • (void)__two
    {
    // 根據對應條件來加鎖
    [self.conditionLock lockWhenCondition:2];

    NSLog(@"__two");
    sleep(1);

    [self.conditionLock unlockWithCondition:3];
    }

  • (void)__three
    {
    [self.conditionLock lockWhenCondition:3];

    NSLog(@"__three");

    [self.conditionLock unlock];
    }

@end

// 列印的先后順序為:1、2、3


#### dispatch\_queue\_t 
我們可以直接使用`GCD`的串行佇列,也是可以實作執行緒同步的,具體代碼可以參考`GCD`部分的示例代碼

#### dispatch_semaphore
`semaphore`叫做”信號量”

信號量的初始值,可以用來控制執行緒并發訪問的最大數量

示例代碼如下

@interface SemaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore;
@property (strong, nonatomic) dispatch_semaphore_t moneySemaphore;
@end

@implementation SemaphoreDemo

  • (instancetype)init
    {
    if (self = [super init]) {
    // 初始化信號量
    // 最多只開5條執行緒,也就是可以5條執行緒同時訪問同一塊空間,然后加鎖,其他執行緒再進來就只能等待了
    self.semaphore = dispatch_semaphore_create(5);
    // 最多只開1條執行緒
    self.ticketSemaphore = dispatch_semaphore_create(1);
    self.moneySemaphore = dispatch_semaphore_create(1);
    }
    return self;
    }

  • (void)__drawMoney
    {
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);

    [super __drawMoney];

    dispatch_semaphore_signal(self.moneySemaphore);
    }

  • (void)__saveMoney
    {
    dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);

    [super __saveMoney];

    dispatch_semaphore_signal(self.moneySemaphore);
    }

  • (void)__saleTicket
    {
    // 如果信號量的值>0就減1,然后往下執行代碼
    // 當信號量的值<=0時,當前執行緒就會進入休眠等待(直到信號量的值>0)
    dispatch_semaphore_wait(self.ticketSemaphore, DISPATCH_TIME_FOREVER);

    [super __saleTicket];

    // 讓信號量的值加1
    dispatch_semaphore_signal(self.ticketSemaphore);
    }

@end


#### @synchronized

`@synchronized`是對`mutex`遞回鎖的封裝

示例代碼如下

@interface SynchronizedDemo: BaseDemo

@end

@implementation SynchronizedDemo

  • (void)__drawMoney
    {
    // @synchronized需要加鎖的是同一個物件才行
    @synchronized([self class]) {
    [super __drawMoney];
    }
    }

  • (void)__saveMoney
    {
    @synchronized([self class]) { // objc_sync_enter
    [super __saveMoney];
    } // objc_sync_exit
    }

  • (void)__saleTicket
    {
    static NSObject *lock;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    lock = [[NSObject alloc] init];
    });

    @synchronized(lock) {
    [super __saleTicket];
    }
    }
    @end


##### 原始碼分析
我們可以通程序式運行中轉匯編看到,最終都會呼叫`objc_sync_enter`

![](https://img2020.cnblogs.com/blog/2320802/202104/2320802-20210408144446882-975064667.jpg)

我們可以通過`objc4`中`objc-sync.mm`來分析對應的原始碼實作

int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;

if (obj) {
    SyncData* data = https://www.cnblogs.com/funkyRay/archive/2021/04/08/id2data(obj, ACQUIRE);
    ASSERT(data);
    data->mutex.lock();
} else {
    // @synchronized(nil) does nothing
    if (DebugNilSync) {
        _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
    }
    objc_sync_nil();
}

return result;

}


可以看到會根據傳進來的`obj`找到對應的`SyncData`

typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;


在找到`SyncData`里面的成員變數`recursive_mutex_t`的真實型別,里面有一個遞回鎖

using recursive_mutex_t = recursive_mutex_tt;

class recursive_mutex_tt : nocopy_t {
os_unfair_recursive_lock mLock; // 遞回鎖

public:
constexpr recursive_mutex_tt() : mLock(OS_UNFAIR_RECURSIVE_LOCK_INIT) {
lockdebug_remember_recursive_mutex(this);
}

constexpr recursive_mutex_tt(__unused const fork_unsafe_lock_t unsafe)
    : mLock(OS_UNFAIR_RECURSIVE_LOCK_INIT)
{ }

void lock()
{
    lockdebug_recursive_mutex_lock(this);
    os_unfair_recursive_lock_lock(&mLock);
}

void unlock()
{
    lockdebug_recursive_mutex_unlock(this);

    os_unfair_recursive_lock_unlock(&mLock);
}

void forceReset()
{
    lockdebug_recursive_mutex_unlock(this);

    bzero(&mLock, sizeof(mLock));
    mLock = os_unfair_recursive_lock OS_UNFAIR_RECURSIVE_LOCK_INIT;
}

bool tryLock()
{
    if (os_unfair_recursive_lock_trylock(&mLock)) {
        lockdebug_recursive_mutex_lock(this);
        return true;
    }
    return false;
}

bool tryUnlock()
{
    if (os_unfair_recursive_lock_tryunlock4objc(&mLock)) {
        lockdebug_recursive_mutex_unlock(this);
        return true;
    }
    return false;
}

void assertLocked() {
    lockdebug_recursive_mutex_assert_locked(this);
}

void assertUnlocked() {
    lockdebug_recursive_mutex_assert_unlocked(this);
}

};


然后我們分析獲取`SyncData`的實作方法`id2data`,通過`obj`從`LIST_FOR_OBJ`真正取出資料

static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData *listp = &LIST_FOR_OBJ(object);
SyncData
result = NULL;

if SUPPORT_DIRECT_THREAD_KEYS

// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
SyncData *data = https://www.cnblogs.com/funkyRay/archive/2021/04/08/(SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
    fastCacheOccupied = YES;

    if (data->object == object) {
        // Found a match in fast cache.
        uintptr_t lockCount;

        result = data;
        lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
        if (result->threadCount <= 0  ||  lockCount <= 0) {
            _objc_fatal("id2data fastcache is buggy");
        }

        switch(why) {
        case ACQUIRE: {
            lockCount++;
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
            break;
        }
        case RELEASE:
            lockCount--;
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
            if (lockCount == 0) {
                // remove from fast cache
                tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                // atomic because may collide with concurrent ACQUIRE
                OSAtomicDecrement32Barrier(&result->threadCount);
            }
            break;
        case CHECK:
            // do nothing
            break;
        }

        return result;
    }
}

endif

// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);
if (cache) {
    unsigned int i;
    for (i = 0; i < cache->used; i++) {
        SyncCacheItem *item = &cache->list[i];
        if (item->data->object != object) continue;

        // Found a match.
        result = item->data;
        if (result->threadCount <= 0  ||  item->lockCount <= 0) {
            _objc_fatal("id2data cache is buggy");
        }
            
        switch(why) {
        case ACQUIRE:
            item->lockCount++;
            break;
        case RELEASE:
            item->lockCount--;
            if (item->lockCount == 0) {
                // remove from per-thread cache
                cache->list[i] = cache->list[--cache->used];
                // atomic because may collide with concurrent ACQUIRE
                OSAtomicDecrement32Barrier(&result->threadCount);
            }
            break;
        case CHECK:
            // do nothing
            break;
        }

        return result;
    }
}

// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple 
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.

lockp->lock();

{
    SyncData* p;
    SyncData* firstUnused = NULL;
    for (p = *listp; p != NULL; p = p->nextData) {
        if ( p->object == object ) {
            result = p;
            // atomic because may collide with concurrent RELEASE
            OSAtomicIncrement32Barrier(&result->threadCount);
            goto done;
        }
        if ( (firstUnused == NULL) && (p->threadCount == 0) )
            firstUnused = p;
    }

    // no SyncData currently associated with object
    if ( (why == RELEASE) || (why == CHECK) )
        goto done;

    // an unused one was found, use it
    if ( firstUnused != NULL ) {
        result = firstUnused;
        result->object = (objc_object *)object;
        result->threadCount = 1;
        goto done;
    }
}

// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = https://www.cnblogs.com/funkyRay/archive/2021/04/08/*listp;
*listp = result;

done:
lockp->unlock();
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread caches above.
if (why == RELEASE) {
// Probably some thread is incorrectly exiting
// while the object is held by another thread.
return nil;
}
if (why != ACQUIRE) _objc_fatal("id2data is buggy");
if (result->object != object) _objc_fatal("id2data is buggy");

if SUPPORT_DIRECT_THREAD_KEYS

    if (!fastCacheOccupied) {
        // Save in fast thread cache
        tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
        tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
    } else 

endif

    {
        // Save in thread cache
        if (!cache) cache = fetch_cache(YES);
        cache->list[cache->used].data = https://www.cnblogs.com/funkyRay/archive/2021/04/08/result;
        cache->list[cache->used].lockCount = 1;
        cache->used++;
    }
}

return result;

}


`LIST_FOR_OBJ`是一個哈希表,哈希表的實作就是將傳進來的`obj`作為`key`,然后對應的鎖為`value`

define LIST_FOR_OBJ(obj) sDataLists[obj].data // 哈希表

static StripedMap sDataLists;
// 哈希表的實作就是將傳進來的物件作為key,然后對應的鎖為value


通過原始碼分析我們也可以看出,`@synchronized`內部的鎖是遞回鎖

### 鎖的比較
#### 性能比較排序
下面是每個鎖按性能從高到低排序

- os\_unfair\_lock- OSSpinLock- dispatch\_semaphore- pthread\_mutex- dispatch\_queue(DISPATCH\_QUEUE\_SERIAL)- NSLock- NSCondition- pthread_mutex(recursive)- NSRecursiveLock- NSConditionLock- @synchronized

選擇性最高的鎖

- dispatch\_semaphore- pthread\_mutex

#### 互斥鎖、自旋鎖的比較
##### 什么情況使用自旋鎖
- 預計執行緒等待鎖的時間很短- 加鎖的代碼(臨界區)經常被呼叫,但競爭情況很少發生- CPU資源不緊張- 多核處理器

##### 什么情況使用互斥鎖
- 預計執行緒等待鎖的時間較長- 單核處理器(盡量減少CPU的消耗)- 臨界區有IO操作(IO操作比較占用CPU資源)- 臨界區代碼復雜或者回圈量大- 臨界區競爭非常激烈

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/274028.html

標籤:其他

上一篇:iOS底層原理(七)多執行緒(上)

下一篇:iOS底層原理(七)多執行緒(下)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more