1. 程式人生 > >ios開發進階之多執行緒01 執行緒 GCD

ios開發進階之多執行緒01 執行緒 GCD

一 多執行緒基礎

  • 什麼是程序?

    • 程序是指在系統中正在執行的一個應用程式。
    • 每個程序之間是獨立的,每個程序均執行在其專用且受保護的記憶體空間內。
  • 什麼是執行緒?

    • 1個程序要想執行任務,必須得有執行緒(每1個程序至少要有1條執行緒)。
    • 1個執行緒中任務的執行是序列的(執行完上一個才能執行下一個)。
  • 什麼是多執行緒?

    • 1個程序中可以開啟多條執行緒,多條執行緒可以並行(同時)執行不同的任務。
    • 執行緒可以並行, 但是每個執行緒中的任務還是序列。
  • 多執行緒原理

    • 多執行緒併發(同時)執行,其實是CPU快速地在多條執行緒之間排程(切換)。
  • 多執行緒優缺點

    • 優點
      • 能適當提高程式的執行效率;
      • 能適當提高資源利用率(CPU、記憶體利用率);
    • 缺點
      • 執行緒越多,CPU在排程執行緒上的開銷就越大;
      • 如果開啟大量的執行緒,會降低程式的效能;
      • 程式設計更加複雜:比如執行緒之間的通訊、多執行緒的資料共享。

二 多執行緒在ios開發中的應用

  • 什麼是主執行緒?

    • 一個iOS程式執行後,預設會開啟1條執行緒,稱為“主執行緒”或“UI執行緒”。
  • 主執行緒的主要作用

    • 顯示\重新整理UI介面;
    • 處理UI事件(比如點選事件、滾動事件、拖拽事件等);
  • 主執行緒的使用注意

    • 別將比較耗時的操作放到主執行緒中;
    • 耗時操作會卡住主執行緒,嚴重影響UI的流暢度,給使用者一種“卡”的壞體驗;
1.如何獲取主執行緒
    /*
    // 如果是主執行緒, 那麼名稱叫做main/ number = 1
// 如果不是主執行緒, 那麼名稱就不叫做main / number != 1 // 注意: currentThread代表拿到當前執行緒, 如果當前執行的方法是被主執行緒執行的, 那麼拿到的就是主執行緒, 如果不是被主執行緒執行的, 那麼拿到的就不是主執行緒 NSLog(@"%@", [NSThread currentThread]); // 明確告訴系統, 需要拿到主執行緒 NSLog(@"%@", [NSThread mainThread]); */ // 2.如何判斷當前方法是否實在主執行緒中執行的 /* if ([NSThread isMainThread]) { NSLog(@"當前方法是在主執行緒中執行的"
); }

三 ios中多執行緒的實現方法

一. pthread

  • 型別: C語言中型別的結尾通常 _t/Ref,而且不需要使用 *
  • 建立C語言物件,一般都用creat
/*
 引數:
     1. 執行緒代號的地址
     2. 執行緒的屬性
     3. 呼叫函式的指標
        - void *(*)(void *)
        - 返回值 (函式指標)(引數)
        - void * 和 OC 中的 id 是等價的
     4. 傳遞給該函式的引數
返回值:
     如果是0,表示正確
     如果是非0,表示錯誤碼
*/
NSString *str = @"lnj";
    pthread_t thid;
    int res = pthread_create(&thid, NULL, &demo, (__bridge void *)(str));
    if (res == 0) {
        NSLog(@"OK");
    } else {
        NSLog(@"error %d", res);
    }

二. NSThread

  • 一個NSThread物件就代表一條執行緒。

  • 建立執行緒的幾種方式

    • alloc/init
    // 1.建立執行緒
    NJThread *thread = [[NJThread alloc] initWithTarget:self selector:@selector(demo:) object:@"lnj"];
    // 設定執行緒名稱
    [thread setName:@"lmg"];
    // 設定執行緒的優先順序,取值範圍是0.0~1.0,預設是0.5
    // 優先順序僅僅說明被CPU呼叫的可能性更大
    [thread setThreadPriority:1.0];
    // 2.啟動執行緒
    [thread start];
  • detach/performSelector
    • 優點:簡單快捷
    • 缺點:無法對執行緒進行更詳細的設定
  1.建立執行緒(建立執行緒後自動啟動執行緒)
[NSThread detachNewThreadSelector:@selector(demo:) toTarget:self withObject:@"lnj"];

// 1.建立執行緒(隱式建立並啟動執行緒)
// 注意: Swift中不能使用, 蘋果認為這個方法不安全
    [self performSelectorInBackground:@selector(demo:) withObject:@"lnj"];

三. GCD
四. NSOperation

四 執行緒的狀態

這裡寫圖片描述

1.啟動執行緒
- (void)start;
// 進入就緒狀態 -> 執行狀態。當執行緒任務執行完畢,自動進入死亡狀態

2.阻塞(暫停)執行緒
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)time;
// 進入阻塞狀態

[NSThread sleepForTimeInterval:2.0]; //執行緒睡2s
[NSThread sleepUntilDate:[NSDate distantFuture]]; //執行緒一直睡

3.強制停止執行緒
+ (void)exit;
// 進入死亡狀態

[NSThread exit];

注意:一旦執行緒停止(死亡)了,就不能再次開啟任務

五 多執行緒的安全隱患

  • 資源共享

    • 1塊資源可能會被多個執行緒共享,也就是多個執行緒可能會訪問同一塊資源
    • 比如多個執行緒訪問同一個物件、同一個變數、同一個檔案
  • 當多個執行緒訪問同一塊資源時,很容易引發資料錯亂和資料安全問題

  • 解決方法–互斥鎖:

@synchronized(鎖物件) { // 需要鎖定的程式碼  }
// 售票方法
- (void)saleTicket
{
    while (1) {

        NSLog(@"歡迎光臨");
        //  只要被synchronized{}擴住, 就能實現同一時刻, 只能有一個執行緒操作
        /*
        // 注意:
        // 1. 如果多條執行緒訪問同一個資源, 那麼必須使用同一把鎖才能鎖住
        // 2. 在開發中, 儘量不要加鎖, 如果必須要加鎖, 一定記住, 鎖的範圍不能太大, 哪裡會有安全隱患就加在哪裡
         */
        /*
        技巧: 開發中如果需要加鎖, 一般都使用self
         */
        // 執行緒2: 等待,  執行緒3: 等待
        @synchronized(self){ // 鎖住
            // 1.查詢剩餘的票數
            NSUInteger count = self.totalCount;
            // 2.判斷是否還有餘票
            if (count > 0) {
                // 執行緒1 100
                [NSThread sleepForTimeInterval:0.1];
                // 2.1賣票
                self.totalCount = count - 1; // 99
                NSLog(@"%@賣了一張票, 還剩%zd票", [NSThread currentThread].name, self.totalCount);
            }else
            {
                // 3.提示客戶, 沒有票了
                NSLog(@"對不起, 沒有票了");
                break;
            }
        } // 解鎖
    }
}
  • 互斥鎖的優缺點
    優點:能有效防止因多執行緒搶奪資源造成的資料安全問題
    缺點:需要消耗大量的CPU資源

  • 互斥鎖注意點

    • 鎖定1份程式碼只用1把鎖,用多把鎖是無效的
    • 鎖定範圍越大, 效能越差
  • 原子和非原子屬性

    • atomic:執行緒安全,需要消耗大量的資源
    • nonatomic:非執行緒安全,適合記憶體小的移動裝置
  • 自旋鎖 & 互斥鎖

    Synchronized: 互斥鎖 —– Atomic: 自旋鎖

    • 共同點
      都能夠保證同一時間,只有一條執行緒執行鎖定範圍的程式碼
    • 不同點
      • 互斥鎖:如果發現有其他執行緒正在執行鎖定的程式碼,執行緒會進入”休眠”狀態,等待其他執行緒執行完畢,開啟鎖之後,執行緒會被”喚醒”
      • 自旋鎖:如果發現有其他執行緒正在執行鎖定的程式碼,執行緒會”一直等待”鎖定程式碼執行完成!
        自旋鎖更適合執行非常短的程式碼,比較適合做一些不耗時的操作!

六 執行緒間通訊

  • 什麼叫做執行緒間通訊

    • 在1個程序中,執行緒往往不是孤立存在的,多個執行緒之間需要經常進行通訊
  • 執行緒間通訊的體現

    • 1個執行緒傳遞資料給另1個執行緒
    • 在1個執行緒中執行完特定任務後,轉到另1個執行緒繼續執行任務
  • 執行緒間通訊常用方法:

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
  • 子執行緒做耗時操作, 主執行緒更新資料
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
 // 開啟一個子執行緒下載圖片
    [self performSelectorInBackground:@selector(downlod) withObject:nil];
}
- (void)downlod
{
    NSLog(@"%@", [NSThread currentThread]);
    // 1.下載圖片
     NSURL *url = [NSURL URLWithString:@"http://pic.4j4j.cn/upload/pic/20130531/07ed5ea485.jpg"];
     NSData *data = [NSData dataWithContentsOfURL:url];
    // 2.將二進位制轉換為圖片
    UIImage *image = [UIImage imageWithData:data];

    // 3.跟新UI
    /*
     waitUntilDone是否等待被呼叫方法執行完成,有可能也會等待呼叫方法的執行完成!
     YES: 等待被呼叫執行緒執行完畢再執行後面的程式碼
     NO : 不用等待被呼叫執行緒執行完畢就可以執行後面的程式碼
     */
     // 可以在指定的執行緒中, 呼叫指定物件的指定方法
[self performSelectorOnMainThread:@selector(showImage:) withObject:[UIImage imageWithData:data] waitUntilDone:YES];
}

七 多執行緒GCD

  • 什麼是GCD
    • 全稱是Grand Central Dispatch,可譯為“牛逼的中樞排程器”
  • GCD的優勢

    • GCD是蘋果公司為多核的並行運算提出的解決方案
    • GCD會自動利用更多的CPU核心(比如雙核、四核)
    • GCD會自動管理執行緒的生命週期(建立執行緒、排程任務、銷燬執行緒)
    • 程式設計師只需要告訴GCD想要執行什麼任務,不需要編寫任何執行緒管理程式碼
  • GCD中有2個核心概念

    • 任務:執行什麼操作
    • 佇列:用來存放任務
  • GCD的使用就2個步驟:
    1、定製任務
    * 確定想做的事情
    2、將任務新增到佇列中
    * GCD會自動將佇列中的任務取出,放到對應的執行緒中執行
    * 任務的取出遵循佇列的FIFO原則:先進先出,後進後出

  • 執行任務
    GCD中有2個用來執行任務的常用函式:

//用同步的方式執行任務
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
queue:佇列
block:任務
//用非同步的方式執行任務
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
- 同步和非同步的區別
     * 同步:只能在當前執行緒中執行任務,不具備開啟新執行緒的能力
     * 非同步:可以在新的執行緒中執行任務,具備開啟新執行緒的能力
  • 佇列的型別

    • 併發佇列
      • 可以讓多個任務併發(同時)執行(自動開啟多個執行緒同時執行任務)
      • 併發功能只有在非同步(dispatch_async)函式下才有效
    • 序列佇列
      • 讓任務一個接著一個地執行(一個任務執行完畢後,再執行下一個任務)
  • 注意點

    • 同步和非同步主要影響:能不能開啟新的執行緒
      • 同步:只是在當前執行緒中執行任務,不具備開啟新執行緒的能力
      • 非同步:可以在新的執行緒中執行任務,具備開啟新執行緒的能力
    • 併發和序列主要影響:任務的執行方式
      • 併發:允許多個任務併發(同時)執行
      • 序列:一個任務執行完畢後,再執行下一個任務
  • 併發佇列
    1.使用dispatch_queue_create函式建立佇列

dispatch_queue_t
dispatch_queue_create(const char *label, // 佇列名稱 
dispatch_queue_attr_t attr); // 佇列的型別
// 建立併發佇列
dispatch_queue_t queue = dispatch_queue_create("com.520it.queue", DISPATCH_QUEUE_CONCURRENT);

2.系統提供的全域性的併發佇列

// 使用dispatch_get_global_queue函式獲得全域性的併發佇列
dispatch_queue_t dispatch_get_global_queue(
dispatch_queue_priority_t priority, // 佇列的優先順序
unsigned long flags); // 此引數暫時無用,用0即可

// 獲得全域性併發佇列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
  • 序列佇列
    • GCD中獲得序列有2種途徑
      1.使用dispatch_queue_create函式建立序列佇列
// 建立序列佇列(佇列型別傳遞NULL或者DISPATCH_QUEUE_SERIAL)
dispatch_queue_t queue = dispatch_queue_create("com.520it.queue", NULL); 

2.使用主佇列(跟主執行緒相關聯的佇列)
主佇列是GCD自帶的一種特殊的序列佇列
放在主佇列中的任務,都會放到主執行緒中執行

使用dispatch_get_main_queue()獲得主佇列
dispatch_queue_t queue = dispatch_get_main_queue();
  • 各種任務佇列搭配
    • 非同步 + 併發 : 會開啟新的執行緒
 /*
     第一個引數: 佇列的名稱
     第二個引數: 告訴系統需要建立一個併發佇列還是序列佇列
     DISPATCH_QUEUE_SERIAL :序列
     DISPATCH_QUEUE_CONCURRENT 併發
     */
    dispatch_queue_t queue = dispatch_queue_create("com.520it", DISPATCH_QUEUE_CONCURRENT);

 dispatch_async(queue, ^{
        NSLog(@"任務1  == %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務2  == %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務3  == %@", [NSThread currentThread]);
    });

系統內部提供一個現成的併發佇列

/*
     第一個引數: iOS8以前是優先順序, iOS8以後是服務質量
     iOS8以前
     *  - DISPATCH_QUEUE_PRIORITY_HIGH          高優先順序 2
     *  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      預設的優先順序 0
     *  - DISPATCH_QUEUE_PRIORITY_LOW:          低優先順序 -2
     *  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:

     iOS8以後
     *  - QOS_CLASS_USER_INTERACTIVE  0x21 使用者互動(使用者迫切想執行任務)
     *  - QOS_CLASS_USER_INITIATED    0x19 使用者需要
     *  - QOS_CLASS_DEFAULT           0x15 預設
     *  - QOS_CLASS_UTILITY           0x11 工具(低優先順序, 蘋果推薦將耗時操作放到這種型別的佇列中)
     *  - QOS_CLASS_BACKGROUND        0x09 後臺
     *  - QOS_CLASS_UNSPECIFIED       0x00 沒有設定

     第二個引數: 廢物
     */

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  • 非同步 + 序列:會開啟新的執行緒,但是隻會開啟一個新的執行緒
// 1.建立序列佇列
    dispatch_queue_t queue = dispatch_queue_create("com.520it", DISPATCH_QUEUE_SERIAL);
    /*
     能夠建立新執行緒的原因:使用"非同步"函式呼叫
     只建立1個子執行緒的原因:佇列是序列佇列
     */
    // 2.將任務新增到佇列中
    dispatch_async(queue, ^{
        NSLog(@"任務1  == %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務2  == %@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任務3  == %@", [NSThread currentThread]);
    });
  • 同步 + 序列:如果是呼叫同步函式, 那麼會等同步函式中的任務執行完畢, 才會執行後面的程式碼
dispatch_queue_t queue = dispatch_queue_create("com.520it", NULL);

    // 2.將任務新增到佇列中
    dispatch_sync(queue, ^{
        NSLog(@"任務1  == %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任務2  == %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任務3  == %@", [NSThread currentThread]);
    });
  • 同步 + 併發 : 不會開啟新的執行緒
// 1.建立一個併發佇列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    // 2.將任務新增到佇列中
    dispatch_sync(queue, ^{
        NSLog(@"任務1  == %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任務2  == %@", [NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"任務3  == %@", [NSThread currentThread]);
    });
  • 非同步 + 主佇列 : 不會建立新的執行緒, 並且任務是在主執行緒中執行
// 主佇列特點: 只要將任務新增到主佇列中, 那麼任務"一定"會在主執行緒中執行 \
    無論你是呼叫同步函式還是非同步函式:

    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        NSLog(@"%@", [NSThread currentThread]);
    });
  • 同步函式 + 主佇列
    • 如果是在主執行緒中呼叫同步函式 + 主佇列, 那麼會導致死鎖
      導致死鎖的原因:
      如果是呼叫同步函式, 那麼會等同步函式中的任務執行完畢, 才會執行後面的程式碼;
      sync函式是在主執行緒中執行的, 並且會等待block執行完畢. 先呼叫
      block是新增到主佇列的, 也需要在主執行緒中執行. 後呼叫
    • 在子執行緒中呼叫 同步函式 + 主佇列

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        // block會在子執行緒中執行
        //        NSLog(@"%@", [NSThread currentThread]);

        dispatch_queue_t queue = dispatch_get_main_queue();
        dispatch_sync(queue, ^{
            // block一定會在主執行緒執行
            NSLog(@"%@", [NSThread currentThread]);
        });
    });

八 GCD執行緒間通訊

// 1.除主佇列以外, 隨便搞一個佇列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    // 2.呼叫非同步函式
    dispatch_async(queue, ^{
        // 1.下載圖片
        NSURL *url = [NSURL URLWithString:@"http://pic.4j4j.cn/upload/pic/20130531/07ed5ea485.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 2.將二進位制轉換為圖片
        UIImage *image = [UIImage imageWithData:data];

        // 3.回到主執行緒更新UI
//        self.imageView.image = image;
        /*
         技巧:
         如果想等UI更新完畢再執行後面的程式碼, 那麼使用同步函式
         如果不想等UI更新完畢就需要執行後面的程式碼, 那麼使用非同步函式
         */
        dispatch_sync(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
        });
        NSLog(@"設定圖片完畢 %@", image);
    });

九 GCD其它常用方法

  1. 延時執行
    方法一:呼叫NSObject的方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// 2秒後再呼叫self的run方法

方法二:使用GCD函式

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 2秒後執行這裡的程式碼...
});
  1. 一次性程式碼
    使用dispatch_once函式能保證某段程式碼在程式執行過程中只被執行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只執行1次的程式碼(這裡面預設是執行緒安全的)
});
  1. 快速迭代
    使用dispatch_apply函式能進行快速迭代遍歷
dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index){
    // 執行10次程式碼,index順序不確定
});

應用場景–拷貝檔案:

// 1.定義變數記錄原始資料夾和目標資料夾的路徑
    NSString *sourcePath = @"/Users/xiaomage/Desktop/test";
    NSString *destPath = @"/Users/xiaomage/Desktop/lnj";
    // 2.取出原始資料夾中所有的檔案
    NSFileManager *manager = [NSFileManager defaultManager];
    NSArray *files = [manager subpathsAtPath:sourcePath];
    //    NSLog(@"%@", files);
    // 3.開始拷貝檔案
    dispatch_apply(files.count, dispatch_get_global_queue(0, 0), ^(size_t index) {
        NSString *fileName = files[index];
        // 3.1生產原始檔案的絕對路徑
        NSString *sourceFilePath = [sourcePath stringByAppendingPathComponent:fileName];
        // 3.2生產目標檔案的絕對路徑
        NSString *destFilePath = [destPath stringByAppendingPathComponent:fileName];
        //        NSLog(@"%@", sourceFilePath);
        //        NSLog(@"%@", destFilePath);
        // 3.3利用NSFileManager拷貝檔案
        [manager moveItemAtPath:sourceFilePath toPath:destFilePath error:nil];
    });
  1. 控制任務之間的關係
    • 有這麼1種需求
      • 首先:分別非同步執行2個耗時的操作
      • 其次:等2個非同步操作都執行完畢後,再回到主執行緒執行操作

如果想要快速高效地實現上述需求,可以考慮用佇列組

dispatch_group_t group =  dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 執行1個耗時的非同步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 執行1個耗時的非同步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 等前面的非同步操作都執行完畢後,回到主執行緒...
});