iOS 多線程(1)-基本概念

1. 線程和進程

在了解多線程之前,我們現(xiàn)在熟悉兩個概念,線程和進程.

一. 線程

線程:進程的基本執(zhí)行單元,一個進程的所有任務都是在線程中執(zhí)行.
進程要想執(zhí)行任務,必須得有線程,進程至少有一條線程.
程序啟動會默認開始一條線程,這條線程被稱為主線程.

二.進程

進程:在系統(tǒng)中正在運行的一個應用程序.
每個進程之間是獨立的,每個進程均運行在其專用的且受保護的內(nèi)存中.

三. 進程與線程的區(qū)別:

  1. 地址空間:同一進程的線程共享本進程的地址空間,而進程之間則是獨立的地址空間.
  2. 資源擁有:同一進程內(nèi)的線程共享本進程的資源如內(nèi)存,I/O,CPU等,但是進程之前的資源是獨立的.
  3. 一個進程崩潰后,在保護模式下不會對其他進程產(chǎn)生影響,但是一個線程崩潰整個進程都死掉.所以多進程要比多線程健壯.
  4. 進程切換時,消耗的資源大,效率低.所以設計到頻繁的切換時,使用線程要高于進程.同樣如果要求同時進行又要共享某些變量的并發(fā)操作,只能用線程不能用進程.
  5. 執(zhí)行過程:每個獨立的進程有一個程序運行的入口,順序執(zhí)行序列.但是線程不能獨立執(zhí)行,必須依存在應用程序中,由應用程序提供多個線程執(zhí)行控制.
  6. 線程是處理器調(diào)度的基本單元,但是進程不是.

2. iOS中的多線程

我們知道了進程和線程之間的關系和區(qū)別,我們現(xiàn)在再來說一下iOS中有哪些多線程.多線程有哪些?他們之前的區(qū)別是什么?我們在開發(fā)的時候選擇哪種方式會更好一些?

技術方案 簡介 語言 線程生命周期 使用頻率
pthread 1.一套通用的多線程API.
2.適用于Unix\Linux\windows等系統(tǒng).
3.可快平臺\可移植.
4.使用難度大.
C 程序員管理 幾乎不使用
(在加鎖的時候使用較多)
NSThread 1.使用更加面向?qū)ο?
2.簡單易懂,可直接操作線程對象.
OC 程序員管理 偶爾使用
GCD 1.旨在替代NSThread等線程技術.
2.充分利用設備的多核.
C 系統(tǒng)管理 經(jīng)常使用
NSOperation 1.基于GCD(底層是GCD).
2.比GCD多了一些更簡單實用的功能.
3.使用更加面向?qū)ο?
OC 系統(tǒng)管理 經(jīng)常使用

NSThread的底層是pthread,NSPperation的底層是GCD.GCD、NSPperation透葛、NSThread創(chuàng)建線程都是依賴于pethread.

一. pthread的使用

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //創(chuàng)建線程
    pthread_t thread;
    /*
     第一個參數(shù)pthread_t *restrict:線程對象
     第二個參數(shù)const pthread_attr_t *restrict:線程屬性
     第三個參數(shù)void *(*)(void *) :指向函數(shù)的指針
     第四個參數(shù)void *restrict:函數(shù)的參數(shù)
     */
    pthread_create(&thread, NULL,eat ,NULL);
}
//void *(*)(void *)
void *eat(void *param)
{
   NSLog(@"調(diào)用了該方法");
}

二. NSThread的使用

A. NSThread創(chuàng)建
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
   // 方法一:創(chuàng)建線程乡括,需要自己開啟線程
  NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(eat) object:nil];
  // 開啟線程
  [thread start];

  // 方法二:創(chuàng)建線程后自動啟動線程
  [NSThread detachNewThreadSelector:@selector(eat) toTarget:self withObject:nil];

  // 方法三:隱式創(chuàng)建并啟動線程
  [self performSelectorInBackground:@selector(eat) withObject:nil];
}

后面兩種方法都不用我們開啟線程,相對方便快捷殿怜,但是沒有辦法拿到子線程對象酷勺,沒有辦法對子線程進行更詳細的設置.

B. NSThread的方法
// 獲取當前線程
 + (NSThread *)currentThread;
 // 創(chuàng)建啟動線程
 + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
 // 判斷是否是多線程
 + (BOOL)isMultiThreaded;
 // 線程休眠 NSDate 休眠到什么時候
 + (void)sleepUntilDate:(NSDate *)date;
 // 線程休眠時間
 + (void)sleepForTimeInterval:(NSTimeInterval)ti;
 // 結束/退出當前線程
 + (void)exit;
 // 獲取當前線程優(yōu)先級
 + (double)threadPriority;
 // 設置線程優(yōu)先級 默認為0.5 取值范圍為0.0 - 1.0 
 // 1.0優(yōu)先級最高
 // 設置優(yōu)先級
 + (BOOL)setThreadPriority:(double)p;
 // 獲取指定線程的優(yōu)先級
 - (double)threadPriority NS_AVAILABLE(10_6, 4_0);
 - (void)setThreadPriority:(double)p NS_AVAILABLE(10_6, 4_0);
 
 // 設置線程的名字
 - (void)setName:(NSString *)n NS_AVAILABLE(10_5, 2_0);
 - (NSString *)name NS_AVAILABLE(10_5, 2_0);

 // 判斷指定的線程是否是 主線程
 - (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0);
 // 判斷當前線程是否是主線程
 + (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
 // 獲取主線程
 + (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);
 
 - (id)init NS_AVAILABLE(10_5, 2_0);    // designated initializer
 // 創(chuàng)建線程
 - (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);
 // 指定線程是否在執(zhí)行
 - (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0);
 // 線程是否完成
 - (BOOL)isFinished NS_AVAILABLE(10_5, 2_0);
 // 線程是否被取消 (是否給當前線程發(fā)過取消信號)
 - (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0);
 // 發(fā)送線程取消信號的 最終線程是否結束 由 線程本身決定
 - (void)cancel NS_AVAILABLE(10_5, 2_0);
 // 啟動線程
 - (void)start NS_AVAILABLE(10_5, 2_0);
 
 // 線程主函數(shù)  在線程中執(zhí)行的函數(shù) 都要在-main函數(shù)中調(diào)用导梆,自定義線程中重寫-main方法
 - (void)main NS_AVAILABLE(10_5, 2_0);    // thread body method
C.NSThread線程的狀態(tài)
NSThread線程的狀態(tài).png
//啟動線程
- (void)start; 
// 進入就緒狀態(tài) -> 運行狀態(tài)。當線程任務執(zhí)行完畢控妻,自動進入死亡狀態(tài)

//阻塞(暫停)線程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 進入阻塞狀態(tài)

//強制停止線程
+ (void)exit;
// 進入死亡狀態(tài)
D. NSThread線程之間的通信
  • 線程間通信
    在一個進程中州袒,線程往往不是孤立存在的,多個線程之間需要經(jīng)常進行通信.
  • 線程間通信的應用
    一個線程傳遞數(shù)據(jù)給另一個線程,在一個線程中執(zhí)行完特定任務后弓候,轉(zhuǎn)到另一個線程繼續(xù)執(zhí)行任務.

線程間通信常用的方法

// 返回主線程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// 返回指定線程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

demo:

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [NSThread detachNewThreadSelector:@selector(donwLoadImage) toTarget:self withObject:nil];
}
-(void)donwLoadImage
{
    // 獲取圖片url地址 http://www.itunes123.com/uploadfile/2016/0421/20160421014340186.jpg
    NSURL *url = [NSURL URLWithString:@"http://www.itunes123.com/uploadfile/2016/0421/20160421014340186.jpg"];
    // 下載圖片二進制文件
    NSData *data = [NSData dataWithContentsOfURL:url];
    // 將圖片二進制文件轉(zhuǎn)化為image;
    UIImage *image = [UIImage imageWithData:data];
    // 參數(shù) waitUntilDone 是否等@selector(showImage:) 執(zhí)行完畢以后再執(zhí)行下面的操作  YES :等 NO:不等
    // 返回主線程顯示圖片
    // [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
    // self.imageView 也可以直接調(diào)用這個方法 直接選擇 setImage方法郎哭,傳入?yún)?shù)image即可
    // [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
    // 返回特定的線程,[NSThread mainThread] 獲得主線程
    [self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
}
-(void)showImage:(UIImage *)image
{
    self.imageView.image = image;
}
@end

三. GCD的使用

A. GCD的任務和隊列
a. GCD的任務

GCD任務:執(zhí)行什么操作菇存,任務有兩種執(zhí)行方式: 同步函數(shù) (dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);)和 異步函數(shù)(dispatch_async(dispatch_queue_t queue, dispatch_block_t block);).
同步:只能在當前線程中執(zhí)行任務夸研,不具備開啟新線程的能力,任務立刻馬上執(zhí)行依鸥,會阻塞當前線程并等待 Block中的任務執(zhí)行完畢亥至,然后當前線程才會繼續(xù)往下運行.
異步:可以在新的線程中執(zhí)行任務,具備開啟新線程的能力,但不一定會開新線程姐扮,當前線程會直接往下執(zhí)行絮供,不會阻塞當前線程.

b. GCD的隊列

GCD的隊列:用來存放任務,分為串行隊列 和 并行隊列.
串行隊列(Serial Dispatch Queue):讓任務一個接著一個地執(zhí)行(一個任務執(zhí)行完畢后茶敏,再執(zhí)行下一個任務).
并發(fā)隊列(Concurrent Dispatch Queue):可以讓多個任務并發(fā)(同時)執(zhí)行(自動開啟多個線程同時執(zhí)行任務),并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效.

GCD的使用就2個步驟:

  1. 定制任務:確定想做的事情.
  2. 將任務添加到隊列中:GCD會自動將隊列中的任務取出壤靶,放到對應的線程中執(zhí)行.
  3. 任務的取出遵循隊列的FIFO原則:先進先出,后進后出.
B. GCD的創(chuàng)建

a. 隊列的創(chuàng)建:

// 第一個參數(shù)const char *label : C語言字符串惊搏,用來標識
// 第二個參數(shù)dispatch_queue_attr_t attr : 隊列的類型
// 并發(fā)隊列:DISPATCH_QUEUE_CONCURRENT
// 串行隊列:DISPATCH_QUEUE_SERIAL 或者 NULL
dispatch_queue_t queue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);

創(chuàng)建并發(fā)隊列:

dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_CONCURRENT);

創(chuàng)建串行隊列:

dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);

GCD默認已經(jīng)提供了全局并發(fā)隊列贮乳,供整個應用使用,可以無需手動創(chuàng)建:

 /** 
     第一個參數(shù):優(yōu)先級 也可直接填后面的數(shù)字
     #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
     #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默認
     #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
     #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后臺
     第二個參數(shù): 預留參數(shù)  0
     */
    dispatch_queue_t quque1 =  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

獲得主隊列:

dispatch_queue_t  queue = dispatch_get_main_queue();

b. 任務的執(zhí)行:
開啟同步函數(shù),同步函數(shù):在當前線程中執(zhí)行任務恬惯,不具備開啟新線程的能力,要求立刻馬上開始執(zhí)行.

/*
     第一個參數(shù):隊列
     第二個參數(shù):block,在里面封裝任務
     */
    dispatch_sync(queue, ^{
        
    });

開啟異步函數(shù),異步函數(shù) :在新的線程中執(zhí)行任務向拆,具備開啟新線程的能力(如果在主隊的話,是不能開啟新線程的),等主線程執(zhí)行完畢之后,回過頭開線程執(zhí)行任務.

 dispatch_async(queue, ^{
        
    });

c. 任務和隊列的組合
任務:同步函數(shù) 異步函數(shù).
隊列:串行 并行.
異步函數(shù)+并發(fā)隊列:會開啟新的線程,并發(fā)執(zhí)行.

dispatch_queue_t queue = dispatch_get_global_queue(0,0);
dispatch_async(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"執(zhí)行任務1-%@",[NSThread crruentThraed]);  
    }
});
dispatch_async(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"執(zhí)行任務2-%@",[NSThread crruentThraed]);  
    }
});
//因為是并發(fā)隊列的異步執(zhí)行,所以開啟新的線程去完成queue中的任務.
//所以執(zhí)行任務1和執(zhí)行任務2是交替進行的.

異步函數(shù)+串行隊列:會開啟一條線程,任務串行執(zhí)行.

dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"執(zhí)行任務1-%@",[NSThread crruentThraed]);  
    }
});
dispatch_async(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"執(zhí)行任務2-%@",[NSThread crruentThraed]);  
    }
});
//因為是串行隊列,所以會在子線程執(zhí)行,會在任務1執(zhí)行結束采取執(zhí)行任務2

同步函數(shù)+并發(fā)隊列:不會開線程,任務串行執(zhí)行

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
   dispatch_sync(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
   });

同步函數(shù)+串行隊列:不會開線程,任務串行執(zhí)行

dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"執(zhí)行任務1-%@",[NSThread crruentThraed]);  
    }
});
dispatch_sync(queue, ^{
    for(int i; i < 10;i++ ){
       NSLog(@"執(zhí)行任務2-%@",[NSThread crruentThraed]);  
    }
});
//因為是串行隊列,所以會在主線程執(zhí)行,會在任務1執(zhí)行結束采取執(zhí)行任務2

異步函數(shù)+主隊列:不會開線程,任務串行執(zhí)行
使用主隊列(跟主線程相關聯(lián)的隊列).
主隊列是GCD自帶的一種特殊的串行隊列酪耳,放在主隊列中的任務浓恳,都會放到主線程中執(zhí)行.

//1.獲得主隊列
dispatch_queue_t queue =  dispatch_get_main_queue();
//2.異步函數(shù)
dispatch_async(queue, ^{
        NSLog(@"---download1---%@",[NSThread currentThread]);
    });

同步函數(shù)+主隊列:死鎖

//1.獲得主隊列
dispatch_queue_t queue =  dispatch_get_main_queue();
//2.同步函數(shù)
dispatch_sync(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
});

因為這個方法在主線程中,給主線程中添加任務葡兑,而同步函數(shù)要求立刻馬上執(zhí)行奖蔓,因此就會相互等待而發(fā)生死鎖赞草。將這個方法放入子線程中讹堤,則不會發(fā)生死鎖,任務串行執(zhí)行厨疙。

并發(fā)隊列 手動創(chuàng)建串行隊列 主隊列
同步(sync) 1.沒有開啟新線程.
2.串行執(zhí)行任務.
1.沒有開啟新線程.
2.串行執(zhí)行任務.
1.沒有開啟新線程.
2.串行執(zhí)行任務.
異步(async) 1.有開啟新線程.
2.并發(fā)執(zhí)行任務.
1.有開啟新線程.
2.串行執(zhí)行任務.
1.沒有開啟新線程.
2.串行執(zhí)行任務.

d.同步函數(shù)和異步函數(shù)的執(zhí)行順序
同步函數(shù):立刻馬上執(zhí)行洲守,會阻塞當前線程

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self syncConcurrent];
}
//同步函數(shù)+并發(fā)隊列:不會開線程,任務串行執(zhí)行
-(void)syncConcurrent
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    NSLog(@"--syncConcurrent--start-");
    dispatch_sync(queue, ^{
        NSLog(@"---download1---%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"---download2---%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"---download3---%@",[NSThread currentThread]);
    });
    dispatch_sync(queue, ^{
        NSLog(@"---download4---%@",[NSThread currentThread]);
    });
    NSLog(@"--syncConcurrent--end-");
}

我們看一下Log

同步函數(shù)會阻塞線程,立即執(zhí)行.png

異步函數(shù):當前線程會直接往下執(zhí)行沾凄,不會阻塞當前線程

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
      [self syncConcurrent];
}
//異步函數(shù)+并發(fā)隊列:會開啟新的線程,并發(fā)執(zhí)行
-(void)asyncCONCURRENT
{
    NSLog(@"--asyncCONCURRENT--start-");
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"---download1---%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"---download2---%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"---download3---%@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"---download4---%@",[NSThread currentThread]);
    });
      NSLog(@"--asyncCONCURRENT--end-");
}

我們來看一下Log

異步函數(shù)不會阻塞當前線程.png

注意:GCD中開多少條線程是由系統(tǒng)根據(jù)CUP繁忙程度決定的梗醇,如果任務很多,GCD會開啟適當?shù)淖泳€程撒蟀,并不會讓所有任務同時執(zhí)行叙谨。

C. GCD線程間的通信

demo

import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        // 獲得圖片URL
        NSURL *url = [NSURL URLWithString:@"http://upload-images.jianshu.io/upload_images/2301429-d5cc0a007447e469.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
        // 將圖片URL下載為二進制文件
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 將二進制文件轉(zhuǎn)化為image
        UIImage *image = [UIImage imageWithData:data];
        NSLog(@"%@",[NSThread currentThread]);
        // 返回主線程 這里用同步函數(shù)不會發(fā)生死鎖,因為這個方法在子線程中被調(diào)用保屯。
        // 也可以使用異步函數(shù)
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            NSLog(@"%@",[NSThread currentThread]);
        });
    }); 
}
@end

GCD線程間的通信非常簡單手负,使用同步或異步函數(shù),傳入主隊列即可姑尺。

D. GCD其他常用函數(shù)

a. 柵欄函數(shù)(控制任務的執(zhí)行順序)

dispatch_barrier_async(queue, ^{
        NSLog(@"--dispatch_barrier_async-");
    });

我們來看一下柵欄函數(shù)的作用

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
     [self barrier];
}
-(void)barrier
{
    //1.創(chuàng)建隊列(并發(fā)隊列)
    dispatch_queue_t queue = dispatch_queue_create("com.xxccqueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download1--%@",i,[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download2--%@",i,[NSThread currentThread]);
        }
    });
    //柵欄函數(shù)
    dispatch_barrier_async(queue, ^{
        NSLog(@"我是一個柵欄函數(shù)");
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download3--%@",i,[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download4--%@",i,[NSThread currentThread]);
        }
    });
}

我們來看一下Log

柵欄函數(shù)demo的輸出.png

柵欄函數(shù)可以控制任務執(zhí)行的順序竟终,柵欄函數(shù)之前的執(zhí)行完畢之后,執(zhí)行柵欄函數(shù)切蟋,然后在執(zhí)行柵欄函數(shù)之后的.

b. 延遲執(zhí)行(延遲·控制在哪個線程執(zhí)行)

/*
     第一個參數(shù):延遲時間
     第二個參數(shù):要執(zhí)行的代碼
     如果想讓延遲的代碼在子線程中執(zhí)行统捶,也可以更改在哪個隊列中執(zhí)行 dispatch_get_main_queue() -> dispatch_get_global_queue(0, 0)
     */
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"---%@",[NSThread currentThread]);
    });

延遲執(zhí)行的其他方法:

// 2s中之后調(diào)用run方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// repeats:YES 是否重復
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

c.單例

-(void)once
    {
        //整個程序運行過程中只會執(zhí)行一次
        //onceToken用來記錄該部分的代碼是否被執(zhí)行過
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            NSLog(@"-----");
        });
    }

d.快速迭代(開多個線程并發(fā)完成迭代操作)

    /*
     第一個參數(shù):迭代的次數(shù)
     第二個參數(shù):在哪個隊列中執(zhí)行
     第三個參數(shù):block要執(zhí)行的任務
     */
dispatch_apply(10, queue, ^(size_t index) {
});

快速迭代:開啟多條線程,并發(fā)執(zhí)行,相比于for循環(huán)在耗時操作中極大的提高效率和速度.

e.隊列組(同柵欄函數(shù))

// 創(chuàng)建隊列組
    dispatch_group_t group = dispatch_group_create();
    // 創(chuàng)建并行隊列 
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 執(zhí)行隊列組任務
    dispatch_group_async(group, queue, ^{   
    });
    //隊列組中的任務執(zhí)行完畢之后喘鸟,執(zhí)行該函數(shù)
    dispatch_group_notify(group, queue, ^{
    });

下面看一了實例使用group下載兩張圖片然后合成在一起

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (nonatomic, strong) UIImage  *image1; /**< 圖片1 */
@property (nonatomic, strong) UIImage  *image2; /**< 圖片2 */
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self group];
}
-(void)group
{
    //下載圖片1
    //創(chuàng)建隊列組
    dispatch_group_t group =  dispatch_group_create();
    //1.開子線程下載圖片
    //創(chuàng)建隊列(并發(fā))
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_async(group, queue, ^{
        //1.獲取url地址
        NSURL *url = [NSURL URLWithString:@"http://www.huabian.com/uploadfile/2015/0914/20150914014032274.jpg"];
        //2.下載圖片
        NSData *data = [NSData dataWithContentsOfURL:url];
        //3.把二進制數(shù)據(jù)轉(zhuǎn)換成圖片
        self.image1 = [UIImage imageWithData:data];
        NSLog(@"1---%@",self.image1);
    });
    //下載圖片2
    dispatch_group_async(group, queue, ^{
        //1.獲取url地址
        NSURL *url = [NSURL URLWithString:@"http://img1.3lian.com/img2011/w12/1202/19/d/88.jpg"]; 
        //2.下載圖片
        NSData *data = [NSData dataWithContentsOfURL:url];
        //3.把二進制數(shù)據(jù)轉(zhuǎn)換成圖片
        self.image2 = [UIImage imageWithData:data];
        NSLog(@"2---%@",self.image2);
    });
    //合成匆绣,隊列組執(zhí)行完畢之后執(zhí)行
    dispatch_group_notify(group, queue, ^{  
        //開啟圖形上下文
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));
        //畫1
        [self.image1 drawInRect:CGRectMake(0, 0, 200, 100)];
        //畫2
        [self.image2 drawInRect:CGRectMake(0, 100, 200, 100)];
        //根據(jù)圖形上下文拿到圖片
        UIImage *image =  UIGraphicsGetImageFromCurrentImageContext();
        //關閉上下文
        UIGraphicsEndImageContext();
        //回到主線程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            NSLog(@"%@--刷新UI",[NSThread currentThread]);
        });
    });
}

四. NSOperation的使用

NSOperation 是蘋果公司對 GCD 的封裝,完全面向?qū)ο竺允兀⒈菺CD多了一些更簡單實用的功能犬绒,所以使用起來更加方便易于理解。NSOperation 和NSOperationQueue 分別對應 GCD 的 任務 和 隊列兑凿。

NSOperation和NSOperationQueue實現(xiàn)多線程的具體步驟:
1.將需要執(zhí)行的操作封裝到一個NSOperation對象中.
2.將NSOperation對象添加到NSOperationQueue中
系統(tǒng)會自動將NSOperationQueue中的NSOperation取出來凯力,并將取出的NSOperation封裝的操作放到一條新線程中執(zhí)行.

A. NSOperation的創(chuàng)建

NSOperation是個抽象類,并不具備封裝操作的能力礼华,必須使用它的子類.
使用NSOperation子類的方式有3種:
a.NSInvocationOperation

    /*
     第一個參數(shù):目標對象
     第二個參數(shù):選擇器,要調(diào)用的方法
     第三個參數(shù):方法要傳遞的參數(shù)
     */
NSInvocationOperation *op  = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];
//啟動操作
[op start];

b.NSBlockOperation(最常用)

//1.封裝操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
       //要執(zhí)行的操作咐鹤,在主線程中執(zhí)行
       NSLog(@"1------%@",[NSThread currentThread]); 
}];
//2.追加操作,追加的操作在子線程中執(zhí)行圣絮,可以追加多條操作
[op addExecutionBlock:^{
        NSLog(@"---download2--%@",[NSThread currentThread]);
    }];
[op start];

c.自定義子類繼承NSOperation祈惶,實現(xiàn)內(nèi)部相應的方法

// 重寫自定義類的main方法實現(xiàn)封裝操作
-(void)main
{
    // 要執(zhí)行的操作
}
// 實例化一個自定義對象,并執(zhí)行操作
CLOperation *op = [[CLOperation alloc]init];
[op start];

自定義類封裝性高扮匠,復用性高捧请。

B. NSOperationQueue的使用

NSOperation中的兩種隊列.

主隊列:通過mainQueue獲得,凡是放到主隊列中的任務都將在主線程執(zhí)行.
非主隊列:直接alloc init出來的隊列棒搜。非主隊列同時具備了并發(fā)和串行的功能疹蛉,通過設置最大并發(fā)數(shù)屬性來控制任務是并發(fā)執(zhí)行還是串行執(zhí)行.

NSOperationQueue的作用.

NSOperation可以調(diào)用start方法來執(zhí)行任務,但默認是同步執(zhí)行的
如果將NSOperation添加到NSOperationQueue(操作隊列)中力麸,系統(tǒng)會自動異步執(zhí)行NSOperation中的操作.

添加操作到NSOperationQueue中

- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;

注意:將操作添加到NSOperationQueue中可款,就會自動啟動,不需要再自己啟動了addOperation 內(nèi)部調(diào)用 start方法. start方法 內(nèi)部調(diào)用 main方法.

C. NSOperation和NSOperationQueue結合使用創(chuàng)建多線程
    注:這里使用NSBlockOperation示例克蚂,其他兩種方法一樣
    // 1. 創(chuàng)建非主隊列 同時具備并發(fā)和串行的功能闺鲸,默認是并發(fā)隊列
    NSOperationQueue *queue =[[NSOperationQueue alloc]init];
    //NSBlockOperation 不論封裝操作還是追加操作都是異步并發(fā)執(zhí)行
    // 2. 封裝操作
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"download1 -- %@",[NSThread currentThread]);
    }];
    // 3. 將封裝操作加入主隊列
    // 也可以不獲取封裝操作對象 直接添加操作到隊列中
    //[queue addOperationWithBlock:^{
    // 操作
    //}];
    [queue addOperation:op1];
D. NSOperation和NSOperationQueue的重要屬性和方法

NSOperation
a. NSOperation的依賴 - (void)addDependency:(NSOperation *)op;

// 操作op1依賴op5,即op1必須等op5執(zhí)行完畢之后才會執(zhí)行
// 添加操作依賴,注意不能循環(huán)依賴埃叭,如果循環(huán)依賴會造成兩個任務都不會執(zhí)行
// 也可以夸隊列依賴摸恍,依賴別的隊列的操作
    [op1 addDependency:op5];

b.NSOperation操作監(jiān)聽void (^completionBlock)(void);

// 監(jiān)聽操作的完成
// 當op1線程完成之后,立刻就會執(zhí)行block塊中的代碼
// block中的代碼與op1不一定在一個線程中執(zhí)行赤屋,但是一定在子線程中執(zhí)行
op1.completionBlock = ^{
        NSLog(@"op1已經(jīng)完成了---%@",[NSThread currentThread]);
    };

NSOperationQueue
a. maxConcurrentOperationCount

 //1.創(chuàng)建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    /*
     默認是并發(fā)隊列,如果最大并發(fā)數(shù)>1,并發(fā)
     如果最大并發(fā)數(shù)==1,串行隊列
     系統(tǒng)的默認是最大并發(fā)數(shù)-1 ,表示不限制
     設置成0則不會執(zhí)行任何操作
     */
     queue.maxConcurrentOperationCount = 1;

b. suspended

//當值為YES的時候暫停,為NO的時候是恢復
queue.suspended = YES;

c.-(void)cancelAllOperations;

//取消所有的任務立镶,不再執(zhí)行,不可逆
[queue cancelAllOperations];

注意:暫停和取消只能暫鸵娑校或取消處于等待狀態(tài)的任務谜慌,不能暫停或取消正在執(zhí)行中的任務莺奔,必須等正在執(zhí)行的任務執(zhí)行完畢之后才會暫停欣范,如果想要暫捅湫梗或者取消正在執(zhí)行的任務,可以在每個任務之間即每當執(zhí)行完一段耗時操作之后恼琼,判斷是否任務是否被取消或者暫停妨蛹。如果想要精確的控制,則需要將判斷代碼放在任務之中晴竞,但是不建議這么做蛙卤,頻繁的判斷會消耗太多時間.

E. NSOperation和NSOperationQueue的一些其他屬性和方法

NSOperation

// 開啟線程
- (void)start;
- (void)main;
// 判斷線程是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// 取消當前線程
- (void)cancel;
//NSOperation任務是否在運行
@property (readonly, getter=isExecuting) BOOL executing;
//NSOperation任務是否已結束
@property (readonly, getter=isFinished) BOOL finished;
// 添加依賴
- (void)addDependency:(NSOperation *)op;
// 移除依賴
- (void)removeDependency:(NSOperation *)op;
// 優(yōu)先級
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};
// 操作監(jiān)聽
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);
// 阻塞當前線程,直到該NSOperation結束噩死〔眩可用于線程執(zhí)行順序的同步
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);
// 獲取線程的優(yōu)先級
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);
// 線程名稱
@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);
@end

NSOperationQueue

// 獲取隊列中的操作
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
// 隊列中的操作數(shù)
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
// 最大并發(fā)數(shù),同一時間最多只能執(zhí)行三個操作
@property NSInteger maxConcurrentOperationCount;
// 暫停 YES:暫停 NO:繼續(xù)
@property (getter=isSuspended) BOOL suspended;
// 取消所有操作
- (void)cancelAllOperations;
// 阻塞當前線程直到此隊列中的所有任務執(zhí)行完畢
- (void)waitUntilAllOperationsAreFinished;
F. NSOperation線程之間的通信

NSOperation線程之間的通信方法

// 回到主線程刷新UI
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
      self.imageView.image = image;
}];

我們同樣使用下載多張圖片合成綜合案例

#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property(nonatomic,strong)UIImage *image1;
@property(nonatomic,strong)UIImage *image2;
@end
@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 創(chuàng)建非住隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    // 下載第一張圖片
    NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:@"http://img2.3lian.com/2014/c7/12/d/77.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        self.image1 = [UIImage imageWithData:data];
    }];
    // 下載第二張圖片
    NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
        NSURL *url = [NSURL URLWithString:@"http://img2.3lian.com/2014/c7/12/d/77.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        self.image2 = [UIImage imageWithData:data];
    }];
    // 合成操作
    NSBlockOperation *combie = [NSBlockOperation blockOperationWithBlock:^{
        // 開啟圖形上下文
        UIGraphicsBeginImageContext(CGSizeMake(375, 667));
        // 繪制圖片1
        [self.image1 drawInRect:CGRectMake(0, 0, 375, 333)];
        // 繪制圖片2
        [self.image2 drawInRect:CGRectMake(0, 334, 375, 333)];
        // 獲取合成圖片
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        // 關閉圖形上下文
        UIGraphicsEndImageContext();
        // 回到主線程刷新UI
        [[NSOperationQueue mainQueue]addOperationWithBlock:^{
            self.imageView.image = image;
        }];
    }];
    // 添加依賴已维,合成圖片需要等圖片1行嗤,圖片2都下載完畢之后合成
    [combie addDependency:download1];
    [combie addDependency:download2];
    // 添加操作到隊列
    [queue addOperation:download1];
    [queue addOperation:download2];
    [queue addOperation:combie];
}
@end

注意:子線程執(zhí)行完操作之后就會立即釋放,即使我們使用強引用引用子線程使子線程不被釋放垛耳,也不能給子線程再次添加操作栅屏,或者再次開啟。

3. 關于多線程的面試題

一. 面試題1

- (void)interview01
{
    // 問題:以下代碼是在主線程執(zhí)行的堂鲜,會不會產(chǎn)生死鎖栈雳?會!
    NSLog(@"執(zhí)行任務1");//①
    //隊列的特點FIFO,先進先出
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"執(zhí)行任務2");//②
    });
    NSLog(@"執(zhí)行任務3");//③
    // dispatch_sync立馬在當前線程同步執(zhí)行任務,執(zhí)行完畢才能繼續(xù)向下執(zhí)行.
}

執(zhí)行的順序是①

二. 面試題2

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

執(zhí)行的順序是①③②

三. 面試題3

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

執(zhí)行的順序是①⑤②

四. 面試題4

- (void)interview04
{
    // 問題:以下代碼是在主線程執(zhí)行的抛虫,會不會產(chǎn)生死鎖?不會简僧!
    NSLog(@"執(zhí)行任務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í)行任務2");
        dispatch_sync(queue2, ^{ // 1
            NSLog(@"執(zhí)行任務3");
        });
        NSLog(@"執(zhí)行任務4");
    });
    NSLog(@"執(zhí)行任務5");
}

執(zhí)行的順序是①⑤②③④

五. 面試題5

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

執(zhí)行的順序是①⑤②③④

六. 面試題6

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
    NSLog(@"1");
    // 這句代碼的本質(zhì)是往Runloop中添加定時器
    [self performSelector:@selector(test) withObject:nil afterDelay:0];
    NSLog(@"3");
    });
- (void)test
{
    NSLog(@"2");
} 

打印結果是1,3,原因是performSelector:withObject:afterDelay:的本質(zhì)是在Runloop中的timer中執(zhí)行的,由于子線程的Runloop沒有啟動所以test不會執(zhí)行.

七. 面試題7

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");
    }];
    [thread start];
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test
{
    NSLog(@"2");
} 

打印的結果是1,原因是線程打印1之后,線程thread就退出了,然后在執(zhí)行performSelector:onThread:withObject:waitUntilDone:就會崩潰,因為thread已經(jīng)退出了.

                            想了解更多iOS學習知識請聯(lián)系:QQ(814299221)
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末棉姐,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子啦逆,更是在濱河造成了極大的恐慌伞矩,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件夏志,死亡現(xiàn)場離奇詭異乃坤,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門湿诊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狱杰,“玉大人,你說我怎么就攤上這事厅须》禄” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵朗和,是天一觀的道長错沽。 經(jīng)常有香客問我,道長眶拉,這世上最難降的妖魔是什么甥捺? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮镀层,結果婚禮上镰禾,老公的妹妹穿的比我還像新娘。我一直安慰自己唱逢,他們只是感情好吴侦,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坞古,像睡著了一般备韧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上痪枫,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天织堂,我揣著相機與錄音,去河邊找鬼奶陈。 笑死易阳,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的吃粒。 我是一名探鬼主播潦俺,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼徐勃!你這毒婦竟也來了事示?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤僻肖,失蹤者是張志新(化名)和其女友劉穎肖爵,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臀脏,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡劝堪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年法挨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖怎爵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情荐糜,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布葛超,位于F島的核電站暴氏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏绣张。R本人自食惡果不足惜答渔,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望侥涵。 院中可真熱鬧沼撕,春花似錦、人聲如沸芜飘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嗦明。三九已至笼沥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間娶牌,已是汗流浹背奔浅。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留诗良,地道東北人汹桦。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像累榜,于是被迫代替她去往敵國和親营勤。 傳聞我的和親對象是個殘疾皇子灵嫌,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355