iOS多線程

GCD

  • 同步/異步 和 串行/ 并發(fā)
  • dispatch_barrier_async
  • dispatch_group

同步/異步 和 串行/ 并發(fā)

  • dispatch_sync(serial_queue, ^{//任務});
  • dispatch_async(serial_queue, ^{//任務});
  • dispatch_sync(concurrent_queue, ^{//任務});
  • dispatch_async(concurrent_queue, ^{//任務});

同步串行

//頭條面試題
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^{
        [self doSomething];
    });
}

上面代碼的問題:
這段代碼的邏輯會產生死鎖,死鎖的原因隊列引起的循環(huán)等待.

隊列循環(huán)等待.png

viewDidLoad的執(zhí)行過程中需要依賴于調用Block任務,而Block在隊列中的排列(即棧的先進先出,因為Block任務在棧頂)導致他需要依賴viewDidLoad執(zhí)行完畢,這種彼此依賴對方先完成,就導致了死鎖發(fā)生。

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(serialQueue, ^{
        [self doSomething];
    });
}

代碼執(zhí)行正常,沒有問題:

同步串行02.png
//美團
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_sync(global_queue, ^{
        NSLog(@"2");
        dispatch_sync(global_queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}//12345

只要同步去提交任務,無論隊列是串行還是并發(fā),最終都會在當前線程去執(zhí)行

異步并發(fā)

//騰訊視頻
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_async(global_queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(printLog) withObject:nil afterDelay:0];
        NSLog(@"3");
    });
  //輸出結果13
}
- (void)printLog {
    NSLog(@"2");
}

performSelector:withObject:afterDelay:因為需要當前線程有runloop去執(zhí)行timer事件,但GCD底層是不創(chuàng)建runloop的沒有runloop,即使時間是0,方法也會失效。所以結果是13

dispatch_barrier_async()

隊列循環(huán)等待.png

怎樣利用GCD實現(xiàn)多讀單寫?(滴滴,美團面試題)

多讀單寫.png
#import "UserCenter.h"

@interface UserCenter()
{
    // 定義一個并發(fā)隊列
    dispatch_queue_t concurrent_queue;
    
    // 用戶數(shù)據(jù)中心, 可能多個線程需要數(shù)據(jù)訪問
    NSMutableDictionary *userCenterDic;
}

@end

// 多讀單寫模型
@implementation UserCenter

- (id)init
{
    self = [super init];
    if (self) {
        // 通過宏定義 DISPATCH_QUEUE_CONCURRENT 創(chuàng)建一個并發(fā)隊列
        concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 創(chuàng)建數(shù)據(jù)容器
        userCenterDic = [NSMutableDictionary dictionary];
    }
    
    return self;
}

- (id)objectForKey:(NSString *)key
{
    __block id obj;
    // 同步讀取指定數(shù)據(jù)
    dispatch_sync(concurrent_queue, ^{
        obj = [userCenterDic objectForKey:key];
    });
    
    return obj;
}

- (void)setObject:(id)obj forKey:(NSString *)key
{
    // 異步柵欄調用設置數(shù)據(jù)
    dispatch_barrier_async(concurrent_queue, ^{
        [userCenterDic setObject:obj forKey:key];
    });
}

@end

dispatch_group_async()

使用GCD實現(xiàn)三個需求:A、B、C三個任務并發(fā),完成后執(zhí)行任務D?(愛奇藝面試題)

#import "GroupObject.h"

@interface GroupObject()
{
    dispatch_queue_t concurrent_queue;
    NSMutableArray <NSURL *> *arrayURLs;
}

@end

@implementation GroupObject

- (id)init
{
    self = [super init];
    if (self) {
        // 創(chuàng)建并發(fā)隊列
        concurrent_queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
        arrayURLs = [NSMutableArray array];
    }

    return self;
}

- (void)handle
{
    // 創(chuàng)建一個group
    dispatch_group_t group = dispatch_group_create();
    
    // for循環(huán)遍歷各個元素執(zhí)行操作
    for (NSURL *url in arrayURLs) {
        
        // 異步組分派到并發(fā)隊列當中
        dispatch_group_async(group, concurrent_queue, ^{
            
            //根據(jù)url去下載圖片
            
            NSLog(@"url is %@", url);
        });
    }
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 當添加到組中的所有任務執(zhí)行完成之后會調用該Block
        NSLog(@"所有圖片已全部下載完成");
    });
}
@end

NSOperation

需要和NSOperationQueue配合使用來實現(xiàn)多線程方案

優(yōu)勢和特點:

  • 添加任務依賴
  • 任務執(zhí)行狀態(tài)控制
  • 最大并發(fā)量

任務執(zhí)行狀態(tài)控制

  • isReady - 當前任務是否就緒
  • isExecuting - 當前任務是否處于正在執(zhí)行
  • isFinished - 當前任務是否完成
  • isCancelled - 當前任務是否取消

狀態(tài)控制

  • 如果只重寫main方法,底層控制變更任務執(zhí)行完成狀態(tài),以及任務退出。
  • 如果只重寫start方法,自行控制任務狀態(tài)

系統(tǒng)是怎樣移除一個isFinished=YES的NSOperation的?

系統(tǒng)是通過KVO監(jiān)聽實現(xiàn)的。

NSThread

啟動流程

NSThread.png

源碼來自gnustep-base

- (void) start
{
  pthread_attr_t    attr;
  pthread_t     thr;

  if (_active == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-$@] called on active thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }
  if (_cancelled == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-$@] called on cancelled thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }
  if (_finished == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-$@] called on finished thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }

  /* Make sure the notification is posted BEFORE the new thread starts.
   */
  gnustep_base_thread_callback();

  /* The thread must persist until it finishes executing.
   */
  [self retain];

  /* Mark the thread as active whiul it's running.
   */
  _active = YES;

  errno = 0;
  pthread_attr_init(&attr);
  /* Create this thread detached, because we never use the return state from
   * threads.
   */
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
  /* Set the stack size when the thread is created.  Unlike the old setrlimit
   * code, this actually works.
   */
  if (_stackSize > 0)
    {
      pthread_attr_setstacksize(&attr, _stackSize);
    }
    //指定線程的氣筒函數(shù)為nsthreadLauncher
  if (pthread_create(&thr, &attr, nsthreadLauncher, self))
    {
      DESTROY(self);
      [NSException raise: NSInternalInconsistencyException
                  format: @"Unable to detach thread (last error %@)",
                  [NSError _last]];
    }
}
static void *nsthreadLauncher(void* thread)
{
    NSThread *t = (NSThread*)thread;//獲取啟動線程t
    setThreadForCurrentThread(t);
#if __OBJC_GC__
    objc_registerThreadWithCollector();
#endif
#if GS_WITH_GC && defined(HAVE_GC_REGISTER_MY_THREAD)
  {
    struct GC_stack_base    base;

    if (GC_get_stack_base(&base) == GC_SUCCESS)
      {
    int result;

    result = GC_register_my_thread(&base);
    if (result != GC_SUCCESS && result != GC_DUPLICATE)
      {
        fprintf(stderr, "Argh ... no thread support in garbage collection library\n");
      }
      }
    else
      {
    fprintf(stderr, "Unable to determine stack base to register new thread for garbage collection\n");
      }
  }
#endif

  /*
   * Let observers know a new thread is starting.
   */
  if (nc == nil)
    {
      nc = RETAIN([NSNotificationCenter defaultCenter]);
    }
  //發(fā)送通知
  [nc postNotificationName: NSThreadDidStartNotification
            object: t
          userInfo: nil];

  [t main];

  [NSThread exit];
  // Not reached
  return NULL;
}
- (void) main
{
  if (_active == NO)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-$@] called on inactive thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }

  [_target performSelector: _selector withObject: _arg];

}

多線程和鎖

iOS當中都有哪些鎖?

  • @synchronized
  • atomic
  • OSSpinLock
  • NSRecursiveLock
  • NSLock
  • dispatch_semaphore_t

@synchronized

  • 一般在創(chuàng)建單例對象的時候使用,保證多線程環(huán)境下創(chuàng)建對象是唯一的

@synchronized 的作用是創(chuàng)建一個互斥鎖,保證此時沒有其它線程對self對象進行修改。這個是objective-c的一個鎖定令牌,防止self對象在同一時間內被其它線程訪問,起到線程的保護作用。 一般在公用變量的時候使用,如單例模式或者操作類的static變量中使用。

指令@synchronized()需要一個參數(shù)。該參數(shù)可以使任何的Objective-C對象,包括self。這個對象就是互斥信號量。他能夠讓一個線程對一段代碼進行保護,避免別的線程執(zhí)行該段代碼。

互斥鎖使用格式

@synchronized(鎖對象){ //需要鎖定的代碼 }

鎖定一份代碼只用1把鎖,用多把鎖是無效的。

互斥鎖的優(yōu)缺點

優(yōu)點:能有效防止因多線程搶奪資源造成的數(shù)據(jù)安全問題

缺點:需要消耗大量的CPU資源

線程同步

多條線程在同一條線上執(zhí)行(按順序地執(zhí)行任務)

互斥鎖,就是使用了線程同步技術。

例如:一個電影院,有3個售票員。一場電影的總數(shù)量固定。3個售票員售票時,要判斷是非還有余票。

#import "ViewController.h"

@interface ViewController ()
/** 售票員01 */
@property (nonatomic, strong) NSThread *thread01;
/** 售票員02 */
@property (nonatomic, strong) NSThread *thread02;
/** 售票員03 */
@property (nonatomic, strong) NSThread *thread03;

/** 票的總數(shù) */
@property (nonatomic, assign) NSInteger ticketCount;

/** 鎖對象 */
//@property (nonatomic, strong) NSObject *locker;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
//    self.locker = [[NSObject alloc] init];
    
    self.ticketCount = 100;
    
    self.thread01 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread01.name = @"售票員01";
    
    self.thread02 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread02.name = @"售票員02";
    
    self.thread03 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread03.name = @"售票員03";
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self.thread01 start];
    [self.thread02 start];
    [self.thread03 start];
}

- (void)saleTicket
{
    while (1) {
        @synchronized(self) {
            // 先取出總數(shù)
            NSInteger count = self.ticketCount;
            if (count > 0) {
                self.ticketCount = count - 1;
                NSLog(@"%@賣了一張票,還剩下%zd張", [NSThread currentThread].name, self.ticketCount);
            } else {
                NSLog(@"票已經賣完了");
                break;
            }
        }
    }
}

@end
atomic
  • 修飾屬性的關鍵字
  • 對被修飾對象進行原子操作(不負責使用)
@property(atomic) NSMutableArray * array;

self.array = [NSMutableArray array];//?賦值是線程安全的

[self.array addObject:obj];//?操作數(shù)組是不能保證線程安全的
atomic和nonatomic的對比

1、atomic和nonatomic用來決定編譯器生成的getter和setter是否為原子操作。

2、atomic:系統(tǒng)生成的 getter/setter 會保證 get、set 操作的完整性,不受其他線程影響。getter 還是能得到一個完好無損的對象(可以保證數(shù)據(jù)的完整性),但這個對象在多線程的情況下是不能確定的,比如上面的例子。

也就是說:如果有多個線程同時調用setter的話,不會出現(xiàn)某一個線程執(zhí)行完setter全部語句之前,另一個線程開始執(zhí)行setter情況,相當于函數(shù)頭尾加了鎖一樣,每次只能有一個線程調用對象的setter方法,所以可以保證數(shù)據(jù)的完整性。

atomic所說的線程安全只是保證了getter和setter存取方法的線程安全,并不能保證整個對象是線程安全的。

3、nonatomic:就沒有這個保證了,nonatomic返回你的對象可能就不是完整的value。因此,在多線程的環(huán)境下原子操作是非常必要的,否則有可能會引起錯誤的結果。但僅僅使用atomic并不會使得對象線程安全,我們還要為對象線程添加lock來確保線程的安全。

4、nonatomic的速度要比atomic的快。

5、atomic與nonatomic的本質區(qū)別其實也就是在setter方法上的操作不同

atomic對象setter和getter方法的實現(xiàn):

- (void)setCurrentImage:(UIImage *)currentImage
{
    @synchronized(self) {
        if (_currentImage != currentImage) {
            [_currentImage release];
            _currentImage = [currentImage retain];
        }
    }
}

- (UIImage *)currentImage
{
    @synchronized(self) {
        return _currentImage;
    }
}

OSSpinLock - 自旋鎖

  • 循環(huán)等待詢問,不釋放當前資源
  • 用于輕量級數(shù)據(jù)訪問。比如簡單的int值+1/-1操作

runtime中有使用到,進行引用計數(shù)的+1/-1操作

NSLock

一般用于解決一些細粒度的線程同步問題,用來保證各個線程互斥來進入自己的臨界區(qū)。

//螞蟻金服
- (void)methodA {
  [nslock lock];
  [self methodB];
  [nslock unlock];
}
- (void)methodB {
  [nslock lock];
//操作邏輯
  [nslock unlock];
}

以上代碼的問題:死鎖

methodA中在某一線程的調用下,已經對線程加了lock,如果在methodB再次調用lock,由于鎖已經被使用了且沒有解鎖,所以它需要等待鎖被解除,這樣就導致了死鎖,線程被阻塞住了。

NSRecursiveLock - 遞歸鎖

NSRecursiveLock實際上定義的是一個遞歸鎖,這個鎖可以被同一線程多次請求,而不會引起死鎖。這主要是用在循環(huán)或遞歸操作中。

遞歸鎖會跟蹤它被lock的次數(shù)。每次成功的lock都必須平衡調用unlock操作。只有所有達到這種平衡,鎖最后才能被釋放,以供其它線程使用。

上面的問題可以通過使用遞歸鎖進行解決:

- (void)methodA {
  [recursiveLock lock];
  [self methodB];
  [recursiveLock unlock];
}
- (void)methodB {
  [recursiveLock lock];
//操作邏輯
  [recursiveLock unlock];
}

dispatch_semaphore_t

dispatch_semaphore_create(long value); // 創(chuàng)建信號量
dispatch_semaphore_signal(dispatch_semaphore_t semaphore); // 發(fā)送信號量
dispatch_semaphore_wait(dispatch_semaphore_t semaphore, dispatch_time_t timeout); // 等待信號量
  • dispatch_semaphore_create(long value);和GCD的group等用法一致,這個函數(shù)是創(chuàng)建一個dispatch_semaphore_類型的信號量,并且創(chuàng)建的時候需要指定信號量的大小。
  • dispatch_semaphore_signal(dispatch_semaphore_t semaphore);發(fā)送信號量。該函數(shù)會對信號量的值進行加1操作。
  • dispatch_semaphore_wait(dispatch_semaphore_t semaphore, dispatch_time_t timeout);等待信號量。如果信號量值為0,那么該函數(shù)就會一直等待,也就是不返回(相當于阻塞當前線程),直到該函數(shù)等待的信號量的值大于等于1,該函數(shù)會對信號量的值進行減1操作,然后返回。

dispatch_semaphore_create(long value);在底層會生成:

struct semaphore {
  int value; //信號量的值
  List <thread>; //一個線程列表
}

dispatch_semaphore_wait(dispatch_semaphore_t semaphore, dispatch_time_t timeout)在底層的實現(xiàn)邏輯大致如下:

dispatch_semaphore_wait() 
{
  S.value -= 1;
  if S.value < 0 then Block(S.list);//阻塞-主動行為
}

dispatch_semaphore_signal()底層實現(xiàn)邏輯大致如下:

dispatch_semaphore_signal()
{
  S.value += 1;
  if S.value <= 0 then wakeup(S.list);//喚醒-被動行為
}

通常等待信號量和發(fā)送信號量的函數(shù)是成對出現(xiàn)的。并發(fā)執(zhí)行任務時候,在當前任務執(zhí)行之前,用dispatch_semaphore_wait函數(shù)進行等待(阻塞),直到上一個任務執(zhí)行完畢后且通過dispatch_semaphore_signal函數(shù)發(fā)送信號量(使信號量的值加1),dispatch_semaphore_wait函數(shù)收到信號量之后判斷信號量的值大于等于1,會再對信號量的值減1,然后當前任務可以執(zhí)行,執(zhí)行完畢當前任務后,再通過dispatch_semaphore_signal函數(shù)發(fā)送信號量(使信號量的值加1),通知執(zhí)行下一個任務......如此一來,通過信號量,就達到了并發(fā)隊列中的任務同步執(zhí)行的要求。

總結

  • 怎樣用GCD事項多讀單寫?

  • iOS系統(tǒng)為我們提供的幾種多線程技術各自的特點是怎樣的?

在iOS系統(tǒng)當中主要提供了三種多線程技術,分別為GCD, NSOperation, NSThread,一般使用GCD來解決一些簡單的線程同步,包括一些子線程的分派,包括實現(xiàn)一些例如多讀單寫這種場景的問題的解決。對于NSOperation,比如AFNetworking,SDWebimageView,他們內部都會涉及到NSOperation,由于他的特點是可以方便我們對任務的狀態(tài)進行控制,包括可以控制依賴的添加和移除依賴。對于NSThread往往我們用他來實現(xiàn)同一個常駐線程。

  • NSOperation對象在Finished之后是怎樣從隊列中移除掉的?

NSOperation對象在Finished之后,會在內部用KVO的形式通知NSOperationQueue達到對NSOperation的移除

你都用過哪些鎖?結合實際談談你是怎樣使用的?

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • iOS多線程編程 基本知識 1. 進程(process) 進程是指在系統(tǒng)中正在運行的一個應用程序,就是一段程序的執(zhí)...
    陵無山閱讀 6,345評論 1 14
  • 多線程的四種解決方案:pthread,NSThread,GCD,NSOperation 一、多線程的基本概念進程:...
    陽明AI閱讀 551評論 0 3
  • 進程與線程 進程:計算機操作系統(tǒng)分配資源的單位,是指系統(tǒng)中正在運行的應用程序,進程之間相互獨立,運行在受保護的內存...
    三十六_閱讀 407評論 1 1
  • 一、前言 上一篇文章iOS多線程淺匯-原理篇中整理了一些有關多線程的基本概念。本篇博文介紹的是iOS中常用的幾個多...
    nuclear閱讀 2,142評論 6 18
  • 本文轉載自:行走的少年郎的簡書:iOS多線程:『GCD』詳盡總結 本文用來介紹 iOS 多線程中 GCD 的相關知...
    遠遊旳遊子閱讀 1,157評論 0 10

友情鏈接更多精彩內容