OC-多線程

多線程

官方文檔:線程編程指南
GCD源碼:https://github.com/apple/swift-corelibs-libdispatch

iOS 中常見的多線程方案

iOS 中常將的多線程方案如下:


iOS多線程方案

GCD 多線程基本概念

  • 同步/異步: 是否有開啟新線程的能力
  • 串行隊列/并行隊列: 是否具有并行執(zhí)行任務(wù)的能力。主隊列是一種特殊的串行隊列
隊列概念

Note:
在主線程執(zhí)行同步串行任務(wù),會卡死主線程。
原理: 在串行隊列里面執(zhí)行同步任務(wù),就會產(chǎn)生死鎖。

多線程安全問題與解決

多線程安全問題在于:多個線程同時訪問并修改同一變量值,會造成最終值不正確。
例如:存取錢問題、售票問題

多線程同時修改資源導(dǎo)致異常

解決方案: 使用線程同步技術(shù)(就是協(xié)同步調(diào),按照預(yù)定的先后次序進行)。常見的線程同步技術(shù)為:加鎖

加鎖同步多線程資源競爭

原則: 對于修改同一個變量值,需要用同一個鎖。如果只是讀取,則無需加鎖

常用的鎖(效率從高到底):

  • os_unfair_lock
  • OSSpinLock
  • dispatch_semaphore
  • pthread_mutex
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSCondition
  • pthread_mutex(recursive)
  • NSRecursiveLock
  • NSConditionLock
  • @synchronized

OSSpinLock

OSSpinLock 自旋鎖, 等待鎖的線程會處于忙等狀態(tài)(busy-wait),一直占用著CPU資源

目前已經(jīng)不再安全,可能會出現(xiàn)線程優(yōu)先級翻轉(zhuǎn)問題。表現(xiàn)上也類似死鎖:如果等待鎖的線程優(yōu)先級較高,它就會一直占用CPU資源,優(yōu)先級低的線程就無法釋放鎖

已經(jīng)在iOS10開始被廢棄。需要引入頭文件#import <libkern/OSAtomic.h>,使用如下:

#import <libkern/OSAtomic.h>


// 初始化鎖
OSSpinLock lock = OS_SPINLOCK_INIT;

// 加鎖
OSSpinLockLock(&lock);

    // 中間需要做的操作...
    
// 解鎖
OSSpinLockUnlock(&lock); 

/////////////////////////////////////////////
iOS 10之后替代 os_unfair_lock 頭文件<os/lock.h>

os_unfair_lock

os_unfair_lock 作為 OSSpinLock 的替代品,解決了優(yōu)先級反轉(zhuǎn)問題,能做到讓等待的線程處于真正的休眠狀態(tài),其接口與OSSpinLock 相似。需導(dǎo)入頭文文件<os/lock.h>

#import <os/lock.h> 

// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;

// 加鎖/嘗試加鎖
void os_unfair_lock_lock(os_unfair_lock_t lock);
bool os_unfair_lock_trylock(os_unfair_lock_t lock);

// 解鎖
void os_unfair_lock_unlock(os_unfair_lock_t lock);

// 判斷是否鎖的擁有者是自己,
void os_unfair_lock_assert_owner(os_unfair_lock_t lock);
void os_unfair_lock_assert_not_owner(os_unfair_lock_t lock);

pthread_mutex

pthread_mutex 能做到讓等待的線程處于休眠狀態(tài)。需要引入頭文件 <pthread.h>

互斥鎖/遞歸鎖/條件鎖

// 普通互斥鎖,屬性傳NULL
pthread_mutex_init(&_mutex, NULL);
pthread_mutex_lock(&_mutex);
    // 中間需要的操作
pthread_mutex_unlock(&_mutex);

---遞歸鎖 -----------------------
// 遞歸鎖:允許同一個線程對一把鎖進行重復(fù)加鎖
// 初始化屬性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化鎖
    pthread_mutex_init(mutex, &attr);
    // 銷毀屬性
    pthread_mutexattr_destroy(&attr);

pthread_mutex_lock(&_mutex);
    // 中間需要的操作
pthread_mutex_unlock(&_mutex);

----條件鎖--------------------------
當多線程執(zhí)行任務(wù)有條件依賴的是可以用條件鎖。
- (void)__remove
{
    pthread_mutex_lock(&_mutex);
    
    if (self.data.count == 0) {
        // 等待
        pthread_cond_wait(&_cond, &_mutex);
    }
    
    [self.data removeLastObject];
    pthread_mutex_unlock(&_mutex);
}

// 線程2
// 往數(shù)組中添加元素
- (void)__add
{
    pthread_mutex_lock(&_mutex);
    sleep(1);
    
    [self.data addObject:@"Test"];
    
    // 信號 - 喚醒被該條件加的鎖
    pthread_cond_signal(&_cond);
    // 廣播
//    pthread_cond_broadcast(&_cond);
    pthread_mutex_unlock(&_mutex);
}

NSLock、NSRecursiveLock、NSCondition、NSConditionLock

這幾個鎖是基于 pthread_mutex 的 OC 封裝。其使用更加簡單、更加面向?qū)ο蟆?/p>

// NSLock - 封裝自 pthread_mutex_lock 默認鎖
self.lock = [[NSLock alloc] init]; 
[self.ticketLock lock];
    // 加鎖代碼
[self.ticketLock unlock];

// NSCondition -- 封裝自 pthread_mutex_lock 默認條件鎖
self.condition = [[NSCondition alloc] init];
[self.condition lock];
// 等待
[self.condition wait];
// 信號
[self.condition signal];    
// 廣播
[self.condition broadcast];
[self.condition unlock];

// NSConditionLock -- 封裝自 pthread_mutex_lock 條件鎖,可加自定義條件
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
// 以下三段代碼可以按順序執(zhí)行
[self.conditionLock lock];
NSLog(@"__one");
[self.conditionLock unlockWithCondition:2];

[self.conditionLock lockWhenCondition:2];
NSLog(@"__two");
[self.conditionLock unlockWithCondition:3];

[self.conditionLock lockWhenCondition:3];
NSLog(@"__three");
[self.conditionLock unlock];

// 

dispatch_queue(DISPATCH_QUEUE_SERIAL)

使用串行隊列也能解決多線程資源競爭問題,將線程加入到串行隊列按順序執(zhí)行。

self.serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);

dispatch_sync(self.serialQueue, ^{
        // 處理變量賦值等核心功能代碼
    });

dispatch_semaphore

semaphore 叫做“信號量”,用來控制線程的最大并發(fā)數(shù)量。

如果信號量的值 > 0,就讓信號量的值減1,然后繼續(xù)往下執(zhí)行代碼。
如果信號量的值 <= 0,就會休眠等待,直到信號量的值變成>0,就讓信號量的值減1,然后繼續(xù)往下執(zhí)行代碼。

dispatch_semaphore_signal(); 給對對應(yīng)的信號量 +1

semaphore 初始值為1時候,非常適合做線程同步

// 設(shè)置最大允許并發(fā)數(shù) 5
self.semaphore = dispatch_semaphore_create(5);

- (void)test
{
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    
    // 相關(guān)代碼
    
    // 讓信號量的值+1
    dispatch_semaphore_signal(self.semaphore);
}

@synchronized

@synchronized 是對 mutex 遞歸鎖的封裝。可以參考 runtime 源碼 objc_sync源碼。

// 參數(shù)即要設(shè)置為鎖的值,就是一個指針
@synchronized([self class]) {
        [super __drawMoney];
    }

鎖的使用小技巧: 宏

#define SemaphoreBegin \
static dispatch_semaphore_t semaphore; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
    semaphore = dispatch_semaphore_create(1); \
}); \
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);

--------------
SemaphoreBegin;
// .....    
SemaphoreEnd;

atomic 原子操作

寫屬性的時候常用 atomic、nonatomic

給屬性加上 atomic 修飾,可以保證屬性 setter 和 getter 方法都是原子性操作,也就是保證 setter 和 getter 內(nèi)部都是線程同步的。這里可以參考 runtime 源碼 objc-accessors。本質(zhì)上也是加鎖,源碼如下

// 獲取屬性對象
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) { // 對象本質(zhì)為結(jié)構(gòu)體,根據(jù)屬性的在結(jié)構(gòu)體內(nèi)的 offset 獲取。如果offset == 0 即獲取結(jié)構(gòu)體首地址,即 isa 地址
        return object_getClass(self);
    }

    // Retain release world
    // 根據(jù) offset 獲取結(jié)構(gòu)體內(nèi) 屬性指針
    id *slot = (id*) ((char*)self + offset);
    // 如果是非原子屬性,就直接返回屬性指針
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

atomic 給 setter/getter 內(nèi)部加鎖保證了屬性存取的安全,但是不能保證屬性取出之后的操作安全。

因為存取方法使用過于頻繁,所以 atomic 顯得過于消耗性能。

iOS 中的讀寫安全方案

IO 操作 -> 文件讀寫操作 -> 【多度,單寫】

實際操作條件:

  1. 同一時間,只能有一個線程進行寫操作
  2. 同一時間,允許有多個線程進行讀操作
  3. 同一時間,不允許既有寫操作,又有讀操作

方案如下:

  1. pthread_rwlock
  2. dispatch_barrier_sync

pthread_rwlock

pthread_rwlock 也是互斥鎖,等待鎖的進程會進入休眠。使用如下

// 創(chuàng)建讀寫鎖屬性
pthread_rwlockattr_t rwAttr;
pthread_rwlockattr_init(&rwAttr);
    
// 初始化鎖
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, &rwAttr); // pthread_rwlock_init(&lock, NULL);
// 讀-加鎖
pthread_rwlock_rdlock(&lock);
// 讀-嘗試加鎖
pthread_rwlock_tryrdlock(&lock);
// 寫-加鎖
pthread_rwlock_wrlock(&lock);
// 寫-嘗試加鎖
pthread_rwlock_trywrlock(&lock);
// 解鎖
pthread_rwlock_unlock(&lock);
// 銷毀
pthread_rwlock_destroy(&lock);

dispatch_barrier_sync

  • 這個函數(shù)闖入的并發(fā)隊列,必須是自己通過dispatch_queue_create創(chuàng)建的
  • 如果傳入的是一個串行或者全局并發(fā)隊列,那就相當于調(diào)用dispatch_async函數(shù)
// 創(chuàng)建隊列
dispatch_queue_t  _Nonnull queue = dispatch_queue_create("barrierQueue", DISPATCH_QUEUE_CONCURRENT);
    
// 讀 - 異步線程,可以多線程同時訪問
dispatch_barrier_async(queue, ^{
    
});
    
// 寫 - 同步任務(wù),只有一個線程可以寫
dispatch_barrier_sync(queue, ^{
    
});

面試題

  • 你理解的多線程?
線程是應(yīng)用程序內(nèi)部實現(xiàn)多個執(zhí)行路徑的相對輕量的方法。

系統(tǒng)->并行執(zhí)行進程->進程執(zhí)行一個或者多個線程。 
這些線程可以同時或者幾乎同時的方式執(zhí)行不同的任務(wù)。
系統(tǒng)本身實際上管理這些執(zhí)行的線程,安排它們在可用的內(nèi)核上運行,并根據(jù)需要中斷它們,將執(zhí)行時間分配給其他線程。

多線程有點:
1. 可以提高程序的感知響應(yīng)能力,
2. 可以提高應(yīng)用程序在多核系統(tǒng)上的實時性能

缺點:
1. 增加代碼復(fù)雜性,它們可以訪問同樣的資源,多個線程需協(xié)同合作,防止破壞程序的狀態(tài)信息
2. 線程間的資源競爭問題,需要線程同步的技術(shù)來額外處理
  • 以下代碼執(zhí)行情況如何?正確執(zhí)行/奔潰?why?
- (void)interview01
{
    // 會產(chǎn)生死鎖!卡死主線程
    NSLog(@"執(zhí)行任務(wù)1");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"執(zhí)行任務(wù)2");
    });
    
    NSLog(@"執(zhí)行任務(wù)3");
    
    // dispatch_sync立馬在當前線程同步執(zhí)行任務(wù)
}

- (void)interview02
{
    // 問題:以下代碼是在主線程執(zhí)行的,會不會產(chǎn)生死鎖?不會!
    NSLog(@"執(zhí)行任務(wù)1");
    
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"執(zhí)行任務(wù)2");
    });
    
    NSLog(@"執(zhí)行任務(wù)3");
    
    // dispatch_async不要求立馬在當前線程同步執(zhí)行任務(wù)
}

- (void)interview03
{
    // 問題:以下代碼是在主線程執(zhí)行的,會不會產(chǎn)生死鎖?會!
    NSLog(@"執(zhí)行任務(wù)1");
    
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{ // 0
        NSLog(@"執(zhí)行任務(wù)2");
        
        dispatch_sync(queue, ^{ // 1
            NSLog(@"執(zhí)行任務(wù)3");
        });
    
        NSLog(@"執(zhí)行任務(wù)4");
    });
    
    NSLog(@"執(zhí)行任務(wù)5");
}

- (void)interview04
{
    // 問題:以下代碼是在主線程執(zhí)行的,會不會產(chǎn)生死鎖?不會!
    NSLog(@"執(zhí)行任務(wù)1");
    
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
//    dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{ // 0
        NSLog(@"執(zhí)行任務(wù)2");
        
        dispatch_sync(queue2, ^{ // 1
            NSLog(@"執(zhí)行任務(wù)3");
        });
        
        NSLog(@"執(zhí)行任務(wù)4");
    });
    
    NSLog(@"執(zhí)行任務(wù)5");
}

- (void)interview05
{
    // 問題:以下代碼是在主線程執(zhí)行的,會不會產(chǎn)生死鎖?不會!
    NSLog(@"執(zhí)行任務(wù)1");
    
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{ // 0
        NSLog(@"執(zhí)行任務(wù)2");
        
        dispatch_sync(queue, ^{ // 1
            NSLog(@"執(zhí)行任務(wù)3");
        });
        
        NSLog(@"執(zhí)行任務(wù)4");
    });
    
    NSLog(@"執(zhí)行任務(wù)5");
}
  • 下面代碼打印什么?為什么?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self test2];
}

- (void)test
{
    NSLog(@"2");
}

- (void)test2
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");
    });
}

// 打印 1、3
// 原因: performSelector:withObject:afterDelay 這個方法本質(zhì)上是給Runloop 添加定時器。而子線程雖然已經(jīng)創(chuàng)建了 runloop 但是并沒有運行,所以不會打印,處理方式就是運行子線程的 runloop,讓子線程?;?
// 以下代碼同理
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");

        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];
    [thread start];

    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
  • 如何實現(xiàn)如首頁多個網(wǎng)絡(luò)請求,最后一個請求基于前面的網(wǎng)絡(luò)請求的情況
// 使用 dispatch_group 的方式。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 創(chuàng)建隊列組
    dispatch_group_t group = dispatch_group_create();
    // 創(chuàng)建并發(fā)隊列
    dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);
    
    // 添加異步任務(wù)
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)1-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)2-%@", [NSThread currentThread]);
        }
    });
    
    // 等前面的任務(wù)執(zhí)行完畢后,會自動執(zhí)行這個任務(wù)
//    dispatch_group_notify(group, queue, ^{
//        dispatch_async(dispatch_get_main_queue(), ^{
//            for (int i = 0; i < 5; i++) {
//                NSLog(@"任務(wù)3-%@", [NSThread currentThread]);
//            }
//        });
//    });
    
//    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//        for (int i = 0; i < 5; i++) {
//            NSLog(@"任務(wù)3-%@", [NSThread currentThread]);
//        }
//    });
    
    dispatch_group_notify(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)3-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_notify(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)4-%@", [NSThread currentThread]);
        }
    });  
}
  • iOS 的多線程有幾種方案,你更傾向于哪一種?
pthread
NSThread
GCD  ---> 更傾向
NSOperation
  • 你在項目中用過 GCD 嗎?
用過
如: 
dispatch_semaphore -> 信號量
dispatch_barrier
dispatch_queue
dispatch_group
dispatch_sync & dispatch_async
...
  • GCD 的隊列類型
串行隊列 & 并行隊列
  • 說一下 OperationQueue 和 GCD 的區(qū)別,以及各自優(yōu)勢?
GCD:
    基于C語言的API,旨在替代 NSThread 的線程技術(shù),可以高效利用設(shè)備多核。

OperationQueue:
    底層封裝自 GCD,增加了很多使用功能,更加面向?qū)ο蟆?
  • 線程安全處理的手段有哪些?
1. 加鎖
2. 使用 GCD 串行隊列
3. 使用 GCD 信號量
  • OC 你了解的鎖有哪些?在你的回答基礎(chǔ)上進行二次提問
    • 1.自旋鎖和互斥鎖的對比
    • 2.使用以上鎖需要注意哪些?
    • 3.使用 C/OC/C++,任選其一,實現(xiàn)自旋或互斥?口述即可
了解的鎖:
OSSpinLock、os_unfair_lock、pthread_mutex、NSLock、NSCondition、NSRecursiveLock、NSConditionLock、@synchronized

自旋鎖適合的場景
1. 預(yù)計線程等待鎖的時間很短
2. 加鎖的代碼(臨界區(qū))經(jīng)常被調(diào)用,但競爭情況不是很激烈
3. CPU 資源不是很緊張
4. 多核處理器

互斥鎖比較適合的場景
1. 預(yù)計線程等待的時間較長
2. 單核處理器(減少CPU占用)
3. 臨界區(qū)有 IO 操作(IO 操作本身占CPU)
4. 臨界區(qū)代碼復(fù)雜或者循環(huán)量很大
5. 臨界區(qū)競爭激烈
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容