2019 iOS面試題大全---全方面剖析面試
-
GCD---同步/異步 ,串行/并發
-
死鎖
-
GCD任務執行順序
-
dispatch_barrier_async
-
dispatch_group_async
-
Dispatch Semaphore
-
延時函數(dispatch_after)
-
使用dispatch_once實現單例
一、GCD---隊列
iOS中,有GCD、NSOperation、NSThread等幾種多線程技術方案。
而GCD共有三種隊列類型:
main queue:通過dispatch_get_main_queue()獲得,這是一個與主線程相關的串行隊列。
global queue:全局隊列是并發隊列,由整個進程共享。存在著高、中、低三種優先級的全局隊列。調用dispath_get_global_queue并傳入優先級來訪問隊列。
自定義隊列:通過函數dispatch_queue_create創建的隊列。
二、 死鎖
死鎖就是隊列引起的循環等待
1、一個比較常見的死鎖例子:主隊列同步
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"deallock");
});
// Do any additional setup after loading the view, typically from a nib.
}
在主線程中運用主隊列同步,也就是把任務放到了主線程的隊列中。
同步對于任務是立刻執行的,那么當把任務放進主隊列時,它就會立馬執行,只有執行完這個任務,viewDidLoad才會繼續向下執行。
而viewDidLoad和任務都是在主隊列上的,由于隊列的先進先出原則,任務又需等待viewDidLoad執行完畢后才能繼續執行,viewDidLoad和這個任務就形成了相互循環等待,就造成了死鎖。
想避免這種死鎖,可以將同步改成異步dispatch_async,或者將dispatch_get_main_queue換成其他串行或并行隊列,都可以解決。
2、同樣,下邊的代碼也會造成死鎖:
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{
NSLog(@"deadlock");
});
});
外面的函數無論是同步還是異步都會造成死鎖。
這是因為里面的任務和外面的任務都在同一個serialQueue隊列內,又是同步,這就和上邊主隊列同步的例子一樣造成了死鎖
解決方法也和上邊一樣,將里面的同步改成異步dispatch_async,或者將serialQueue換成其他串行或并行隊列,都可以解決
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue2, ^{
NSLog(@"deadlock");
});
});
這樣是不會死鎖的,并且serialQueue和serialQueue2是在同一個線程中的。
三、GCD任務執行順序
1、串行隊列先異步后同步
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(serialQueue, ^{
NSLog(@"2");
});
NSLog(@"3");
dispatch_sync(serialQueue, ^{
NSLog(@"4");
});
NSLog(@"5");
打印順序是13245
原因是:
首先先打印1
接下來將任務2其添加至串行隊列上,由于任務2是異步,不會阻塞線程,繼續向下執行,打印3
然后是任務4,將任務4添加至串行隊列上,因為任務4和任務2在同一串行隊列,根據隊列先進先出原則,任務4必須等任務2執行后才能執行,又因為任務4是同步任務,會阻塞線程,只有執行完任務4才能繼續向下執行打印5
所以最終順序就是13245。
這里的任務4在主線程中執行,而任務2在子線程中執行。
如果任務4是添加到另一個串行隊列或者并行隊列,則任務2和任務4無序執行(可以添加多個任務看效果)
2、performSelector
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(test:) withObject:nil afterDelay:0];
});
這里的test方法是不會去執行的,原因在于
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
這個方法要創建提交任務到runloop上的,而gcd底層創建的線程是默認沒有開啟對應runloop的,所有這個方法就會失效。
而如果將dispatch_get_global_queue改成主隊列,由于主隊列所在的主線程是默認開啟了runloop的,就會去執行(將dispatch_async改成同步,因為同步是在當前線程執行,那么如果當前線程是主線程,test方法也是會去執行的)。
四、dispatch_barrier_async
1、問:怎么用GCD實現多讀單寫?
多讀單寫的意思就是:可以多個讀者同時讀取數據,而在讀的時候,不能去寫入數據。并且,在寫的過程中,不能有其他寫者去寫。即讀者之間是并發的,寫者與讀者或其他寫者是互斥的。
這里的寫處理就是通過柵欄的形式去寫。
就可以用dispatch_barrier_sync(柵欄函數)去實現
2、dispatch_barrier_sync的用法:
dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i < 10; i++) {
dispatch_sync(concurrentQueue, ^{
NSLog(@"%zd",i);
});
}
dispatch_barrier_sync(concurrentQueue, ^{
NSLog(@"barrier");
});
for (NSInteger i = 10; i < 20; i++) {
dispatch_sync(concurrentQueue, ^{
NSLog(@"%zd",i);
});
}
這里的dispatch_barrier_sync上的隊列要和需要阻塞的任務在同一隊列上,否則是無效的。
從打印上看,任務0-9和任務任務10-19因為是異步并發的原因,彼此是無序的。而由于柵欄函數的存在,導致順序必然是先執行任務0-9,再執行柵欄函數,再去執行任務10-19。
- dispatch_barrier_sync: Submits a barrier block object for execution and waits until that block completes.(提交一個柵欄函數在執行中,它會等待柵欄函數執行完)
-
dispatch_barrier_async: Submits a barrier block for asynchronous execution and returns immediately.(提交一個柵欄函數在異步執行中,它會立馬返回)
而dispatch_barrier_sync和dispatch_barrier_async的區別也就在于會不會阻塞當前線程
比如,上述代碼如果在dispatch_barrier_async后隨便加一條打印,則會先去執行該打印,再去執行任務0-9和柵欄函數;而如果是dispatch_barrier_sync,則會在任務0-9和柵欄函數后去執行這條打印。
3、則可以這樣設計多讀單寫:
- (id)readDataForKey:(NSString *)key
{
__block id result;
dispatch_sync(_concurrentQueue, ^{
result = [self valueForKey:key];
});
return result;
}
- (void)writeData:(id)data forKey:(NSString *)key
{
dispatch_barrier_async(_concurrentQueue, ^{
[self setValue:data forKey:key];
});
}
五、dispatch_group_async
場景:在n個耗時并發任務都完成后,再去執行接下來的任務。比如,在n個網絡請求完成后去刷新UI頁面。
dispatch_queue_t concurrentQueue = dispatch_queue_create("test1", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
for (NSInteger i = 0; i < 10; i++) {
dispatch_group_async(group, concurrentQueue, ^{
sleep(1);
NSLog(@"%zd:網絡請求",i);
});
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"刷新頁面");
});
深入理解GCD之dispatch_group
六、Dispatch Semaphore
GCD 中的信號量是指 Dispatch Semaphore,是持有計數的信號。
Dispatch Semaphore 提供了三個函數
1.dispatch_semaphore_create:創建一個Semaphore并初始化信號的總量
2.dispatch_semaphore_signal:發送一個信號,讓信號總量加1
3.dispatch_semaphore_wait:可以使總信號量減1,當信號總量為0時就會一直等待(阻塞所在線程),否則就可以正常執行。
Dispatch Semaphore 在實際開發中主要用于:
- 保持線程同步,將異步執行任務轉換為同步執行任務
- 保證線程安全,為線程加鎖
1、保持線程同步:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSInteger number = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
number = 100;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end,number = %zd",number);
dispatch_semaphore_wait加鎖阻塞了當前線程,dispatch_semaphore_signal解鎖后當前線程繼續執行
2、保證線程安全,為線程加鎖:
在線程安全中可以將dispatch_semaphore_wait看作加鎖,而dispatch_semaphore_signal看作解鎖
首先創建全局變量
_semaphore = dispatch_semaphore_create(1);
注意到這里的初始化信號量是1。
- (void)asyncTask
{
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
count++;
sleep(1);
NSLog(@"執行任務:%zd",count);
dispatch_semaphore_signal(_semaphore);
}
異步并發調用asyncTask
for (NSInteger i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self asyncTask];
});
}
然后發現打印是從任務1順序執行到100,沒有發生兩個任務同時執行的情況。
原因如下:
在子線程中并發執行asyncTask,那么第一個添加到并發隊列里的,會將信號量減1,此時信號量等于0,可以執行接下來的任務。而并發隊列中其他任務,由于此時信號量不等于0,必須等當前正在執行的任務執行完畢后調用dispatch_semaphore_signal將信號量加1,才可以繼續執行接下來的任務,以此類推,從而達到線程加鎖的目的。
六、延時函數(dispatch_after)
dispatch_after能讓我們添加進隊列的任務延時執行,該函數并不是在指定時間后執行處理,而只是在指定時間追加處理到dispatch_queue
//第一個參數是time,第二個參數是dispatch_queue,第三個參數是要執行的block
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"dispatch_after");
});
由于其內部使用的是dispatch_time_t管理時間,而不是NSTimer。
所以如果在子線程中調用,相比performSelector:afterDelay,不用關心runloop是否開啟
七、使用dispatch_once實現單例
+ (instancetype)shareInstance {
static dispatch_once_t onceToken;
static id instance = nil;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}