iOS-多線程

本文主要介紹了 iOS的多線程方案, 多線程安全方案, 多讀單寫方案.

篇幅稍長,還請耐心看完.

進程

理論上,每個iOS App都是一個進程, 有自己獨立的虛擬空間來存儲自己的運行數據.

線程

每個進程中有多個線程,這些線程共享進程的全局變量和堆數據. 多條線程可以在一個進程中并發執行.達到同一時間完成多個任務的效果. 其實在單處理器中,所謂的并發,是操作系統不斷的在線程間來回切換達到的一個偽并發效果.

多線程的作用

  • 避免線程堵塞,有些需要大量時間執行的任務,如果放在主線程同步執行,則會造成卡頓
  • 將復雜任務拆分, 如UITableViewCell加載圖片時,可以開辟新線程去處理圖片數據,最終再交由主線程實現
  • 多任務并行

iOS中的多線程方案

  • pthread: C語言,使用難度大.需要由使用者管理生命周期. 基本沒人用
  • NSThread: OC 面向對象,需要依賴Runloop保活,需要由使用者管理生命周期,使用較少
  • GCD: C語言. 由系統本身管理生命周期 是目前較主流的多線程方案
  • NSOperation: OC語言 基于GCD的封裝,更加面向對象. 由系統本身管理聲明周期. 也比較多人使用

隊列

隊列中裝載著多個線程,根據隊列的不同屬性安排線程的任務調度,隊列可分為串行隊列和并發隊列

  • 串行隊列:隊列中的任務一個一個連成串執行
  • 并發隊列:隊列可以同時執行任務

同步

所有任務在同一個線程一個接一個執行

B任務必須等待A任務執行完成才可以進行. 如果A任務耗時太長, 則B任務也會一直等到A任務進行完成.

iOS場景: 加載網絡數據時,使用加載指示器擋住當前view, 等到加載完成后指示器消失,頁面展示數據. 如果此時加載任務遲遲未完成.則界面一直卡在加載界面.

有可能會引起死鎖

異步

在新的線程中執行任務.

下面看看同步異步在不同線程的表現

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"main thread %@",[NSThread currentThread]);
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"main dispatch_async_thred:%@",[NSThread currentThread]);
    });
    
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"global dispatch_sync_thred:%@",[NSThread currentThread]);
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"global dispatch_async_thred:%@",[NSThread currentThread]);
    });
    
    dispatch_queue_t squeue = dispatch_queue_create("QUEUE1", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(squeue, ^{
        NSLog(@"squeue dispatch_sync_thred:%@",[NSThread currentThread]);
    });
    dispatch_async(squeue, ^{
        NSLog(@"squeue dispatch_async_thred:%@",[NSThread currentThread]);
    });
    
    dispatch_queue_t cqueue = dispatch_queue_create("QUEUE1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(cqueue, ^{
        NSLog(@"cqueue dispatch_sync_thred:%@",[NSThread currentThread]);
    });
    
    dispatch_async(squeue, ^{
        NSLog(@"cqueue dispatch_async_thred:%@",[NSThread currentThread]);
    });
}

main thread <NSThread: 0x600003a0c200>{number = 1, name = main}
global dispatch_sync_thred:<NSThread: 0x600003a0c200>{number = 1, name = main}
global dispatch_async_thred:<NSThread: 0x600003a4d980>{number = 2, name = (null)}
squeue dispatch_sync_thred:<NSThread: 0x600003a0c200>{number = 1, name = main}
cqueue dispatch_sync_thred:<NSThread: 0x600003a0c200>{number = 1, name = main}
squeue dispatch_async_thred:<NSThread: 0x600003a4d980>{number = 2, name = (null)}
cqueue dispatch_async_thred:<NSThread: 0x600003a4d980>{number = 2, name = (null)}
main dispatch_async_thred:<NSThread: 0x600003a0c200>{number = 1, name = main}

從結果可以看到.

  • 同步:都沒有開啟新的線程
  • 異步:全局隊列,串行隊列,并發隊列中開辟了新線程. 主隊列不開辟新線程

主隊列異步執行任務情況

NSLog(@"1%@",[NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"main async 2:%@",[NSThread currentThread]);
});
NSLog(@"3%@",[NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"main async 4:%@",[NSThread currentThread]);
});
NSLog(@"5%@",[NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"main async 6:%@",[NSThread currentThread]);
});
dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"main async 7:%@",[NSThread currentThread]);
});
dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"main async 8:%@",[NSThread currentThread]);
});
NSLog(@"9%@",[NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"main async 10:%@",[NSThread currentThread]);
});

1<NSThread: 0x600001564140>{number = 1, name = main}
3<NSThread: 0x600001564140>{number = 1, name = main}
5<NSThread: 0x600001564140>{number = 1, name = main}
9<NSThread: 0x600001564140>{number = 1, name = main}
main async 2:<NSThread: 0x600001564140>{number = 1, name = main}
main async 4:<NSThread: 0x600001564140>{number = 1, name = main}
main async 6:<NSThread: 0x600001564140>{number = 1, name = main}
main async 7:<NSThread: 0x600001564140>{number = 1, name = main}
main async 8:<NSThread: 0x600001564140>{number = 1, name = main}
main async 10:<NSThread: 0x600001564140>{number = 1, name = main}

從結果可以得出. 在主隊列中運行異步任務, 會等待viewdidload中的任務執行完成后,再串行的執行任務.那么根據上述的結果.拋出一個問題.下屬代碼,在主隊列執行同步任務.那么會有什么后果.

NSLog(@"1%@",[NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"main sync 2:%@",[NSThread currentThread]);
});
NSLog(@"3%@",[NSThread currentThread]);

答案就是:死鎖. 因為同步任務需要等待viewdidload執行完畢,而viewdidload又要等待同步任務出隊.大家一起等.大家都不出隊.那就大家抱著一起卡死唄.

全局隊列異步執行任務情況

image-20210603174841144.png
dispatch_queue_t gQueue = dispatch_get_global_queue(0, 0);
    NSLog(@"1%@",[NSThread currentThread]);
dispatch_async(gQueue, ^{
    NSLog(@"global async 2:%@",[NSThread currentThread]);
});
NSLog(@"3%@",[NSThread currentThread]);
dispatch_async(gQueue, ^{
    NSLog(@"global async 4:%@",[NSThread currentThread]);
});
NSLog(@"5%@",[NSThread currentThread]);
dispatch_async(gQueue, ^{
    NSLog(@"global async 6:%@",[NSThread currentThread]);
});
dispatch_async(gQueue, ^{
    NSLog(@"global async 7:%@",[NSThread currentThread]);
});
dispatch_async(gQueue, ^{
    NSLog(@"global async 8:%@",[NSThread currentThread]);
});
NSLog(@"9%@",[NSThread currentThread]);
dispatch_async(gQueue, ^{
    NSLog(@"global async 10:%@",[NSThread currentThread]);
});

1<NSThread: 0x6000003f0080>{number = 1, name = main}
3<NSThread: 0x6000003f0080>{number = 1, name = main}
global async 2:<NSThread: 0x6000003b1840>{number = 6, name = (null)}
5<NSThread: 0x6000003f0080>{number = 1, name = main}
9<NSThread: 0x6000003f0080>{number = 1, name = main}
global async 4:<NSThread: 0x6000003b2180>{number = 7, name = (null)}
global async 6:<NSThread: 0x6000003b1840>{number = 6, name = (null)}
global async 7:<NSThread: 0x6000003f3380>{number = 5, name = (null)}
global async 8:<NSThread: 0x6000003b2180>{number = 7, name = (null)}
global async 10:<NSThread: 0x6000003ec9c0>{number = 4, name = (null)}

我們上面已經得出結論.在全局隊列執行異步任務,是會開辟新的線程.所以很明顯.他并不需要等到viewdidload執行完畢就可以執行任務.

自建串行隊列異步執行任務情況

dispatch_queue_t squeue = dispatch_queue_create("QUEUE1", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1%@",[NSThread currentThread]);
dispatch_async(squeue, ^{
    NSLog(@"squeue async 2:%@",[NSThread currentThread]);
});
NSLog(@"3%@",[NSThread currentThread]);
dispatch_async(squeue, ^{
    NSLog(@"squeue async 4:%@",[NSThread currentThread]);
});
NSLog(@"5%@",[NSThread currentThread]);
dispatch_async(squeue, ^{
    NSLog(@"squeue async 6:%@",[NSThread currentThread]);
});
dispatch_async(squeue, ^{
    NSLog(@"squeue async 7:%@",[NSThread currentThread]);
});
dispatch_async(squeue, ^{
    NSLog(@"squeue async 8:%@",[NSThread currentThread]);
});
NSLog(@"9%@",[NSThread currentThread]);
dispatch_async(squeue, ^{
    NSLog(@"squeue async 10:%@",[NSThread currentThread]);
});

1<NSThread: 0x60000215c600>{number = 1, name = main}
3<NSThread: 0x60000215c600>{number = 1, name = main}
squeue async 2:<NSThread: 0x600002108140>{number = 6, name = (null)}
5<NSThread: 0x60000215c600>{number = 1, name = main}
9<NSThread: 0x60000215c600>{number = 1, name = main}
squeue async 4:<NSThread: 0x600002108140>{number = 6, name = (null)}
squeue async 6:<NSThread: 0x600002108140>{number = 6, name = (null)}
squeue async 7:<NSThread: 0x600002108140>{number = 6, name = (null)}
squeue async 8:<NSThread: 0x600002108140>{number = 6, name = (null)}
squeue async 10:<NSThread: 0x600002108140>{number = 6, name = (null)}

結果與全局隊列等同,但只創建了一條線程

自建并發隊列異步執行任務情況

dispatch_queue_t cqueue = dispatch_queue_create("QUEUE1", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1%@",[NSThread currentThread]);
dispatch_async(cqueue, ^{
    NSLog(@"cqueue async 2:%@",[NSThread currentThread]);
});
NSLog(@"3%@",[NSThread currentThread]);
dispatch_async(cqueue, ^{
    NSLog(@"cqueue async 4:%@",[NSThread currentThread]);
});
NSLog(@"5%@",[NSThread currentThread]);
dispatch_async(cqueue, ^{
    NSLog(@"cqueue async 6:%@",[NSThread currentThread]);
});
dispatch_async(cqueue, ^{
    NSLog(@"cqueue async 7:%@",[NSThread currentThread]);
});
dispatch_async(cqueue, ^{
    NSLog(@"cqueue async 8:%@",[NSThread currentThread]);
});
NSLog(@"9%@",[NSThread currentThread]);
dispatch_async(cqueue, ^{
    NSLog(@"cqueue async 10:%@",[NSThread currentThread]);
});

1<NSThread: 0x6000028a03c0>{number = 1, name = main}
3<NSThread: 0x6000028a03c0>{number = 1, name = main}
cqueue async 2:<NSThread: 0x60000289d1c0>{number = 3, name = (null)}
5<NSThread: 0x6000028a03c0>{number = 1, name = main}
cqueue async 4:<NSThread: 0x6000028f8800>{number = 7, name = (null)}
cqueue async 6:<NSThread: 0x60000289d1c0>{number = 3, name = (null)}
cqueue async 7:<NSThread: 0x6000028f8800>{number = 7, name = (null)}
cqueue async 8:<NSThread: 0x6000028f8800>{number = 7, name = (null)}
9<NSThread: 0x6000028a03c0>{number = 1, name = main}
cqueue async 10:<NSThread: 0x6000028f8800>{number = 7, name = (null)}

結果與全局隊列相同,也創建了多條線程

global_queue

從上述結果對比,global_queue是一條并發隊列

main_queue

main_queue是一條串行隊列

死鎖

在往主隊列添加同步任務時,會造成死鎖.其實不只是主隊列,在串行隊列同步任務中添加同步任務,也會引起死鎖.一下代碼同樣會引起奔潰.而在并發隊列中則不會產生這種情況.

dispatch_queue_t cqueue = dispatch_queue_create("QUEUE1", DISPATCH_QUEUE_SERIAL);
dispatch_sync(cqueue, ^{
    NSLog(@"cqueue async 2:%@",[NSThread currentThread]);
    dispatch_sync(cqueue, ^{
        NSLog(@"cqueue sync 3:%@",[NSThread currentThread]);
    });
});

線程安全

多線程在帶來便利的同時,也會帶來隱患.當多個線程對同一個變量進行寫操作時,可能會造成結果不同.賣票問題就是很典型的多線程導致的隱患.

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i < 30; i++) {
             [self saleTickets];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 30; i++) {
             [self saleTickets];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 40; i++) {
             [self saleTickets];
        }
    });
}

- (void)saleTickets {
    tickets = tickets - 1;
    NSLog(@"賣出1張票,還剩%d張票",tickets);
}

從最終結果可以看出.最終的余票并不為0.

說明有多條線程在同時操作一塊內存

例如:多個線程訪問tickets時值為98,進行減1. 所以造成了結果的不同.

解決方法1.加鎖

iOS中的鎖
  • OSSpinLock(自旋鎖)
  • os_unfairLock
  • pthread_mutex
  • dispatch_semaphor
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditonLock
  • @synchronized
自旋鎖

當線程檢測到自旋鎖上鎖時,會進行忙等,直到鎖被釋放,才會繼續執行任務.類似于while(lock){}

互斥鎖

當線程檢測到上鎖時,該線程會進行休眠,等到其他線程解鎖,才會喚起該線程.

OSSpinLock
#import <libkern/OSAtomic.h>
- (void)viewDidLoad {
    [super viewDidLoad];
    self.lock = OS_SPINLOCK_INIT;
    [self beginSaleTicket];
}

- (void)saleTickets {
    OSSpinLockLock(&_lock);
    tickets = tickets - 1;
    NSLog(@"賣出1張票,還剩%d張票",tickets);
    OSSpinLockUnlock(&_lock);
}

- (void)beginSaleTicket {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i < 30; i++) {
             [self saleTickets];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 30; i++) {
             [self saleTickets];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 40; i++) {
             [self saleTickets];
        }
    });
}

最終執行結果,發現最終余票為0. 也就是說,線程安全了

OSSpinLock為什么被棄用
原因1:死鎖
#import <os/lock.h>
- (void)viewDidLoad {
    [super viewDidLoad];
    self.lock = OS_SPINLOCK_INIT;
    [self deadLock];
    NSLog(@"test");
}

- (void)deadLock {
    OSSpinLockLock(&_lock);
    [self lockAgain];
    NSLog(@"等待lockAgain執行完成");
    OSSpinLockUnlock(&_lock);
}

- (void)lockAgain {
    OSSpinLockLock(&_lock);
    NSLog(@"加鎖");
    OSSpinLockUnlock(&_lock);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touch");
}

上述代碼的結果就是, 什么都不輸出, touchesBegan也無法響應.為什么呢?分析一下

自旋鎖中, deadLock方法拿到鎖,上鎖后調用lockAgain,此時檢測到_lock上鎖,lockAgain會進入忙等. deadLock被堵住,無法拿到鎖進行解鎖.所以線程卡死. 當然, 互斥鎖也會有這種問題發生.

原因2:優先級反轉

線程運行時搶占式的, 當高優先級的線程輪轉時,會搶占低優先級任務的CPU控制器 .

假設有 A,B兩條線程, 優先級 : A > B

B先于A持有鎖并上鎖, 此時A輪轉, 搶占B的CPU控制權, B未對鎖就行解鎖操作, A任務檢測到上了鎖, 忙等, 最終掛起線程, 繼續回到B操作. 只有兩條線程可能問題不大. 但是如果有多個線程不斷輪轉, 就會出現優先級最高的任務A遲遲無法完成, 最終影響了程序性能

os_unfair_lock

iOS10之后, OC推出的用來替代 OSSpinLock 的鎖,

- (void)viewDidLoad {
    [super viewDidLoad];
    self.lock = OS_UNFAIR_LOCK_INIT;
    [self beginSaleTicket];
}

- (void)saleTickets {
    os_unfair_lock_lock(&_lock);
    tickets = tickets - 1;
    NSLog(@"賣出1張票,還剩%d張票",tickets);
    os_unfair_lock_unlock(&_lock);
}

使用方法也很簡單, iOS內部沒有標明os_unfair_lock是自旋鎖還是互斥鎖, 但是通過從匯編看到的結果, 檢測到加鎖的線程走到某一步之后, 是直接休眠的. 根據定義看, os_unfair_lock是一個互斥鎖.

pthread_mutex_t

pthread_mutex_t是一個比較強大的鎖, 里面封裝了多個類型的鎖. 普通鎖, 遞歸鎖. 也可以為鎖添加條件

- (void)viewDidLoad {
    [super viewDidLoad];
    pthread_mutex_init(&(_lock), NULL);
    [self beginSaleTicket];
}

- (void)saleTickets {
    pthread_mutex_lock(&_lock);
    tickets = tickets - 1;
    NSLog(@"賣出1張票,還剩%d張票",tickets);
    pthread_mutex_unlock(&_lock);
}

眼尖的朋友應該看出來了.pthread_mutex_init可以傳入兩個參數, 第二個參數指定了鎖的類型. 還記得上面提到的同一線程中不同任務多次加鎖導致的死鎖問題嗎? pthread_mutex_init提供了解決方案. 那就是 遞歸鎖

遞歸鎖
- (void)viewDidLoad {
    [super viewDidLoad];
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&(_lock), &attr);
    pthread_mutexattr_destroy(&attr);
    [self deadLock];
    NSLog(@"test");
}

- (void)deadLock {
    pthread_mutex_lock(&_lock);
    [self lockAgain];
    NSLog(@"等待lockAgain執行完成");
    pthread_mutex_unlock(&_lock);
}

- (void)lockAgain {
    pthread_mutex_lock(&_lock);
    NSLog(@"加鎖");
    pthread_mutex_unlock(&_lock);
}

可以看到, 當設置pthread_mutex_lock為遞歸鎖時, 任務可以順利執行. 遞歸鎖允許同一線程中的不同任務對同一把鎖多次上鎖.

條件

給鎖添加條件時, 線程對當前持有的鎖進行釋放, 然后進行休眠, 等待其他線程釋放條件信號,就會喚醒該線程, 重新加鎖并繼續執行.

假設我們有一個場景: 搬磚糊墻. 當有磚的時候, 才可以開始糊墻.假設糊墻跟搬磚的是兩個人, 當糊墻的把磚用完的時候, 就需要等搬磚的人把磚搬過來才可以繼續進行.

- (void)viewDidLoad {
    [super viewDidLoad];
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&_lock, &attr);
    pthread_mutexattr_destroy(&attr);
    pthread_cond_init(&_cond, NULL);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self build];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self getBrick];
    });
}

- (void)build {
    pthread_mutex_lock(&_lock);
    NSLog(@"開始糊墻");
    if (brick == 0) {
        // 等待
        NSLog(@"沒磚了,等磚來");
        pthread_cond_wait(&_cond, &_lock);
    }
    NSLog(@"糊墻");
    pthread_mutex_unlock(&_lock);
}

- (void)getBrick {
    pthread_mutex_lock(&_lock);
    sleep(1);
    brick += 1;
    NSLog(@"磚來了");
    pthread_cond_signal(&_cond);
    pthread_mutex_unlock(&_lock);
}

值得注意的一點是. 條件信號釋放后, 添加條件的一方并不會馬上喚醒并加鎖執行后續任務. 而是會等待信號發出方解開當前鎖的時候才會喚醒. 所以如果有特別耗時的任務. 可以放在unlock 后面做.

NSLock
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.lock = [NSLock new];
    [self beginSaleTicket];
}

- (void)saleTickets {
    [self.lock lock];
    tickets = tickets - 1;
    NSLog(@"賣出1張票,還剩%d張票",tickets);
    [self.lock unlock];
}

NSLock是對mutex 普通鎖的封裝, 原理相同.此處就不贅述了

NSRecursiveLock

對mutex遞歸鎖的封裝,API也與NSLock基本一致.此處就不貼代碼占篇幅了

NSCondition

對mutex和cond的封裝

- (void)viewDidLoad {
    [super viewDidLoad];
    self.condition = [NSCondition new];
    [self buildWall];
}

- (void)buildWall {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self build];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self getBrick];
    });
}

- (void)build {
    [self.condition lock];
    NSLog(@"開始糊墻");
    if (brick == 0) {
        // 等待
        NSLog(@"沒磚了,搬磚去");
        [self.condition wait];
    }
    NSLog(@"糊墻");
    [self.condition unlock];
}

- (void)getBrick {
    [self.condition lock];
    sleep(1);
    brick += 1;
    NSLog(@"開始搬磚");
    [self.condition signal];
    [self.condition unlock];
}
NSConditionLock

對NSCondition進行封裝,可以在同一把鎖上添加不同的條件

- (void)viewDidLoad {
    [super viewDidLoad];
    self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self stepThree];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self stepOne];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self stepTwo];
    });
}

- (void)stepOne
{
    [self.conditionLock lock];
    NSLog(@"%s",__func__);
    sleep(1);
    [self.conditionLock unlockWithCondition:2];
}

- (void)stepTwo
{
    [self.conditionLock lockWhenCondition:2];
    NSLog(@"%s",__func__);
    sleep(1);
    [self.conditionLock unlockWithCondition:3];
}

- (void)stepThree
{
    [self.conditionLock lockWhenCondition:3];
    NSLog(@"%s",__func__);
    [self.conditionLock unlock];
}
@synchronized

是OC的語法糖,本質上也是對mutex的封裝

- (void)viewDidLoad {
    [super viewDidLoad];
    [self beginSaleTicket];
}
- (void)saleTickets {
    @synchronized (self) {
        tickets = tickets - 1;
        NSLog(@"賣出1張票,還剩%d張票",tickets);
    }
}

使用@synchronized代碼相當簡潔. 通過測試, @synchronized內部實現的是遞歸鎖.

說完了iOS中提供的鎖,再來說說他們的性能

普通鎖 > 條件鎖 > 遞歸鎖

os_unfair_lock > OSSPinLock > pthread_mutext_t > NSLock > NSCondition > pthread_mutex(recursive) > NSRecursiveLock > NSConditionLock > @synchronized

解決方法2:GCD串行隊列

上面我們提到,在串行隊列中,異步任務是串行執行的.所以我們可以新建一個GCD的串行隊列來進行賣票操作.

- (void)viewDidLoad {
    [super viewDidLoad];
    self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
    [self beginSaleTicket];
}


- (void)saleTickets {
    tickets = tickets - 1;
    NSLog(@"賣出1張票,還剩%d張票",tickets);
}

- (void)beginSaleTicket {
    dispatch_async(self.ticketQueue, ^{
        for (int i = 0; i < 30; i++) {
             [self saleTickets];
        }
    });
    dispatch_async(self.ticketQueue, ^{
        for (int i = 0; i < 30; i++) {
             [self saleTickets];
        }
    });
    dispatch_async(self.ticketQueue, ^{
        for (int i = 0; i < 40; i++) {
             [self saleTickets];
        }
    });
}

解決方法3:信號量dispatch_semaphore_t

此處涉及PV操作的概念.

執行P操作時, 若 P > 0, 信號量-1, 執行任務, 若 P <= 0, 休眠等待

執行V操作, 信號量+1

我們可以通過設置信號量為1, 來設置并發線程的最大并發量為1

- (void)viewDidLoad {
    [super viewDidLoad];
    self.ticketQueue = dispatch_get_global_queue(0, 0);
    self.semaphore = dispatch_semaphore_create(1);
    [self beginSaleTicket];
}


- (void)saleTickets {
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    tickets = tickets - 1;
    NSLog(@"賣出1張票,還剩%d張票",tickets);
    dispatch_semaphore_signal(self.semaphore);
}

讀寫安全(多讀單寫)

當一個文件, 我們可以對它進行讀寫的時候, 就會出現不安全的狀況, 多條線程同時寫, 或者一邊讀一邊寫都是安全隱患.

所以我們希望對文件操作是可以做到

  • 可以多條線程同時讀取文件
  • 進行寫操作時,不可以讀取文件, 也不允許多條線程進行寫入操作

解決方案:dispatch_barrier_async

- (void)viewDidLoad {
    [super viewDidLoad];
    self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_async(self.queue, ^{
            [self read];
        });
        dispatch_barrier_async(self.queue, ^{
            [self write];
        });
    }
}

- (void)read {
    sleep(1);
    NSLog(@"read");
}

- (void)write
{
    sleep(1);
    NSLog(@"write");
}

從輸出結果看到, 會同時打印多個read,但一次只會打印出一個write,而且在讀操作時,也不會進行其他操作.

dispatch_barrier_async 通過為并發隊列設置柵欄的方式, 當柵欄建立時,其他異步任務都不允許執行.

注意

思考一下,把上面的并發隊列換成全局隊列,會怎么樣?

self.queue = dispatch_get_global_queue(0, 0);

2021-06-04 11:53:23.544718+0800 MultiThread[98352:14881477] read
2021-06-04 11:53:23.544718+0800 MultiThread[98352:14881479] write
2021-06-04 11:53:23.544733+0800 MultiThread[98352:14881480] read
2021-06-04 11:53:23.544743+0800 MultiThread[98352:14881485] read
2021-06-04 11:53:23.544718+0800 MultiThread[98352:14881476] read
2021-06-04 11:53:23.544718+0800 MultiThread[98352:14881482] read
2021-06-04 11:53:23.544766+0800 MultiThread[98352:14881483] read
2021-06-04 11:53:23.544780+0800 MultiThread[98352:14881490] read
2021-06-04 11:53:23.544780+0800 MultiThread[98352:14881489] write
2021-06-04 11:53:23.544830+0800 MultiThread[98352:14881491] read
2021-06-04 11:53:23.544841+0800 MultiThread[98352:14881492] read
2021-06-04 11:53:23.544882+0800 MultiThread[98352:14881493] write
2021-06-04 11:53:23.544905+0800 MultiThread[98352:14881494] read
2021-06-04 11:53:23.545049+0800 MultiThread[98352:14881496] read
2021-06-04 11:53:23.545061+0800 MultiThread[98352:14881495] read
2021-06-04 11:53:23.545064+0800 MultiThread[98352:14881498] read
2021-06-04 11:53:23.545077+0800 MultiThread[98352:14881499] read
2021-06-04 11:53:23.545146+0800 MultiThread[98352:14881501] write
2021-06-04 11:53:23.545083+0800 MultiThread[98352:14881497] write

以上是部分的結果.可以看出,就算加了GCD柵欄,還是等于異步并發.

值得注意的是, GCD的柵欄只能擋住我們自己創建的并發隊列.

并不能擋住全局隊列. 這是為了安全起見. 如果全局隊列被一個及其耗時的操作block住, 那會引發很多其他的問題.

文章很長,感謝觀看. 如果文章有問題,還請不吝賜教.如果對你有一些幫助, 麻煩點個贊

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容