多線程(英語:multithreading),是指從軟件或者硬件上實現多個線程並發執行的技術。具有多線程能力的計算機因有硬件支持而能夠在同一時間執行多於一個線程,進而提升整體處理性能。具有這種能力的系統包括對稱多處理機、多核心處理器以及芯片級多處理(Chip-level multithreading)或同時多線程(Simultaneous multithreading)處理器.在一個程序中,這些獨立運行的程序片段叫做線程(Thread).利用它編程的概念就叫做多線程.具有多線程能力的計算機因有硬件支持而能夠在同一時間執行多個一個線程,提升整體處理性能.
一.什么叫進程?
進程是指在系統中正在運行的一個應用程序. 每個進程之間是獨立的.每個進程均運行在其賺佣且受保護的內存空間內.
二.什么是線程?
1.用來執行進程的任務的叫線程.
2.每個進程必須至少要有一個線程.
3.線程是進程的基本執行單元.
4.一個進程的所有任務都在線程中執行.
5.一個程序只有一個進程,一個進程可能會有一個或者多個線程.進程包含線程.
6.主線程是系統開辟的,其他任何線程都是手動開辟的.
三.線程的串行和並行分別是什么?
1.串行.
每一個線程同一時間內只能執行一個任務.就像Boss指揮一個人做完A事情,再做B事情,最后做C事情.
2.並行.
但是因為時間原因,Boss嫌一個人來做事件A,B,C時間太長.想要同時指揮多個人一起來做這三件事件.
四.多線程.
三個人一起完成這三件事用專業術語將就叫多線程.
1.多線程的原理.
同一時間,CPU只能處理一條線程,只有一個線程在工作.但是CPU如果快速的在多個線程之間切換的話,就能讓多條線程同時執行.如果CPU切換的比較快的話,可以看成多個線程並發執行.但是CPU的工作能力畢竟有限,同時執行很多個線程,每條線程被切換到的頻率就會降低,時間就會變長.所以要合理使用多線程.
2.多線程的優缺點
優點 |
缺點 |
1.能適當的提高傳程序的執行效率 2. 能適當的提高資源的利用效率 |
1. 開啟多線程需要占用一定的內存空間,主線程占用1M,子線程占用512KB. 2. 線程越多,CPU在調度線程上的開銷就越大. 3. 程序設計更復雜.比如:線程之間的通訊,多線程的數據共享等
|
3. 多線程在開發中的應用
主線程:一個iOS程序運行后,默認會開啟1條線程,稱為"主線程"或者"UI線程". 在iOS中除了主線程,其他子線程都是獨立於Cocoa Touch的,所以只有主線程可以更新UI界面.
主線程的作用: 顯示/刷新UI界面,處理UI事件(點擊事件,滾動事件,拖拽事件等)
注意點: 不要將刷新比較耗時的放到主線程中.耗時操作會卡住主線程,嚴重影響UI的流暢度,給用戶一種“卡”的壞體驗.
4. 多線程的使用.
如果我有一個個按鈕A和一個UITextView. A按鈕在當前線程(UI線程)下做一個for循環(循環10萬次輸出)點擊完A按鈕,立即拖拽UITextView,許久后才有反應.
5.任務的概念.
有兩種執行方式:同步執行和異步執行.
(1) 同步執行(sync): 會阻塞當前線程並等待Block執行完畢,然后在當前線程才會繼續往下執行. 會盡可能的在當前線程派發任務,但如果在其他隊列往主隊列中同步派發,任務會在主線程中執行.
(2) 異步執行(async): 當前線程繼續執行,不會阻塞當前線程. 不一定會新建一個線程,例如在主線程異步派發到主線程,派發依舊是異步線程的,任務也會在主線程中執行.
同步異步的區別,不在於是否會開辟一個線程,在於派發方法是否需要等待Block完成后才返回.
6.隊列的概念.
用於存放任務.分為串行隊列和並行隊列.
(1)串行隊列:放到串行隊列中的任務,GCD會FIFO(先進先出)的取出一個,執行一個,然后取出下一個,這樣一個一個的執行.
(2)並行隊列:放到並行隊列中任務,GCD也會FIFO的取出來,但不同的是,取出來一個任務就會放到別的線程中去,燃火取出來另一個又放到另一個線程中.由於取的動作很快,可以忽略不計,看起來所有的任務都是一起執行的.不過需要注意,GCD會根據系統資源控制並行的數量,所以任務很多也不會把所有的任務都執行.
無論串行還是並行隊列,任務啟動順序都是按照FIFO的,只是並發隊列允許同一時間有多個任務都在執行.
FIFO是 First Input First Output的縮寫,先入先出隊列,這是一種傳統的按序執行方法,先進入的指令先完成並引退,跟着才執行第二條指令。
7.組合方式:
(1)串行隊列同步執行:
(2)串行隊列異步執行:
(3)並行隊列同步執行:
(4)並行隊列異步執行:
四.目前有四種多線程的實現方法.
1.Pthreads.
基於C的,適合做跨平台的SDK.
2.NSThread.
NSThread是輕量級的多線程開發,使用起來也並不復雜,但是使用NSThread需要自己管理線程生命周期.
目前我就用到
[NSThread currentThread]獲取當前線程.主要用於調試.
[NSThread sleepForTimeInterval:<#(NSTimeInterval)#>]; 延時多少秒
3.NSOperation & NSOperationQueue
待整理
4.GCD.
queue--線程 thread--隊列 diapatch--派遣 serial--連續的 concurrent--並發的
(1)這四總方式是隨着iOS的發展逐漸引進的,所以后者比前者使用更加簡單,並且GCD也是目前蘋果官方比較推薦的(充分利用了多核運算性能).
(2)GCD的全拼 --> Grand Central Dispatch --> 集中調度,是iOS開發的一個多核編程解決方案.會自己管理線程的生命周期(創建線程,調度任務,銷毀線程),不需要自己手動管理,只要要求它做就行了.使用靈活方便.
(3)在GCD中一個操作是多線程還是單線程執行取決於當前隊列類型和執行方法,只有隊列類型為並行隊列並且使用異步方法執行才能在多個線程中執行.
(4)串行隊列是可以按照順序執行的,並行隊列的異步方法是無法按照順序執行的.
(5)UI界面的更新最好采用同步方法,其他采用異步方法.
(6)GCD中多線程操作方法不需要使用@autoreleasepool,GCD會管理內存.
(7)GCD是完成面向過程的.
(8)GCD的工作原理:讓程序平行排隊的特定任務,根據可用的處理資源,安排他們在任何可用的處理器核心上執行任務.
(9)一個任務可以是一個函數(function)或者是一個Block,GCD的底層依然是用線程實現的,不過這樣可以讓程序員不用關注實現的細節.
(10) dispatch queue 分為三種
serial(連續的) | 又稱為private dispatch queues,同時只執行一個任務.Serial queue 通常用於同步訪問特定的資源或者數據.當你創建多個Serial queue時候,雖然他們各自是同步的,但是Serial queue之間是並發的. |
Concurrent(並發的) | 又稱為Global dispatch queue,可以並發的執行多個任務,但是執行任務的順序是隨機的. |
Main dispatch queue (主隊列) |
它是全局可用的serial queue,它是在應用程序主線程上執行任務的. |
So GCD is the leading role of today.
五.代碼示例
1.獲取主線程
/** 獲取主線程 1. 所有的刷新UI界面的任務都要在主線程執行. 2. 將消耗時間的任務放在別的線程中出來,盡量不要在主線程中處理. */ dispatch_queue_t main_queue = dispatch_get_main_queue(); NSLog(@"main_queue:\n %@",main_queue);
/*! * @function dispatch_get_main_queue * * @abstract * Returns the default queue that is bound to the main thread. * * @discussion * In order to invoke blocks submitted to the main queue, the application must * call dispatch_main(), NSApplicationMain(), or use a CFRunLoop on the main * thread. * * @result * Returns the main queue. This queue is created automatically on behalf of * the main thread before main() is called. */ DISPATCH_INLINE DISPATCH_ALWAYS_INLINE DISPATCH_CONST DISPATCH_NOTHROW dispatch_queue_t dispatch_get_main_queue(void) { return DISPATCH_GLOBAL_OBJECT(dispatch_queue_t, _dispatch_main_q); }
翻譯一波
dispatch_get_main_queue的功能
1. 摘要
返回綁定到主線程的默認隊列
2. 討論
為了請求blocks提交到主線程,主隊列的申請必須呼叫到dispatch_main,NSApplicationMain(),或者用一個CFRunLoop.
3.結果
返回主線程.為了主隊列能再main()之前被呼叫,這個隊列應該被自動的創建.
dispatch_get_main_queue 也是一種dispatch_queue_t
2.自己創建隊列
/** 自己創建的隊列 dispatch_queue_create 參數1: 第一個參數是標識符.用於DEBUG的時候標志唯一的隊列,可以為空. 參數2: 第二個參數用來表示創建的隊列是串行的還是並行的.傳入DISPATCH_QUEUE_SERIAL或者NULL表示創建的是串行隊列.傳入DISPATCH_QUEUE_CONCURRENT表示創建的並行隊列. (SERIAL--> serial連續的/CONCURRENT--> concurrent,並發的,一致的) */
// 創建串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create(nil, NULL); NSLog(@"serialQueue:\n %@",serialQueue); // 創建並行隊列: 這應該是唯一一個並行隊列,只要是並行任務一般都加入到這個隊列
dispatch_queue_t concurrentQueue = dispatch_queue_create(nil, DISPATCH_QUEUE_CONCURRENT); NSLog(@"concurrentQueue:\n %@",concurrentQueue);
3.創建任務
dispatch_queue_t serialQueue = dispatch_queue_create(nil, NULL);
// 創建任務
/** 同步任務 (sync) 1. 會阻塞當前線程. */ dispatch_sync( serialQueue, ^{ for (int i = 0; i < 10000; i ++) { NSLog(@"同步任務: \n%@",[NSThread currentThread]); } }); /** 異步任務 (async) 1. 不會阻塞當前線程. */ dispatch_async(serialQueue, ^{ NSLog(@"異步任務: %@",[NSThread currentThread]); });
4.創建隊列組
dispatch_group_async可以實現監聽一組任務是否完成,完成后得到通知執行其他的操作。這個方法很有用,比如你執行三個下載任務,當三個任務都下載完成后你才通知界面說完成的了。下面是一段例子代碼
//1. 創建隊列組
dispatch_group_t group = dispatch_group_create(); /**2. 創建隊列 dispatch_get_global_queue 會獲取一個全局隊列,我們姑且理解為系統為我們開啟的一些全局線程。我們用priority指定隊列的優先級,而flag作為保留字段備用(一般為0)。並行隊列的執行順序與其加入隊列的順序相同. #define DISPATCH_QUEUE_PRIORITY_HIGH 2 #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 #define DISPATCH_QUEUE_PRIORITY_LOW (-2) */ dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //3. 多次使用隊列中的方法執行任務,只有異步任務 //3.1 執行三次循環
dispatch_group_async(group, queue, ^{ for (int i = 0; i < 3; i ++) { NSLog(@"group-01 - %@",[NSThread currentThread]); } }); //3.2 主隊列執行8次循環
dispatch_group_async(group, dispatch_get_main_queue(), ^{ for (int i = 0; i < 8; i ++) { NSLog(@"group-02 - %@",[NSThread currentThread]); } }); //3.3 執行5次循環
dispatch_group_async(group, queue, ^{ for (int i = 0; i < 5; i ++) { NSLog(@"group-03 - %@",[NSThread currentThread]); } }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"完成 - %@",[NSThread currentThread]); });
5. 死鎖現象
(1)現象1
NSLog(@"之前==> %@",[NSThread currentThread]); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"sync==> %@",[NSThread currentThread]); }); NSLog(@"之后==> %@",[NSThread currentThread]); /** 解釋 1. 只會打印第一句:之前==> <NSThread: 0x7fe66b700610>{number = 1, name = main} ,然后主線程就卡死了,你可以在界面上放一個按鈕,你就會發現點不了了。 2. 打印完第一句,dispatch_sync(因為是一個同步任務,會阻塞當前的線程)會阻塞當前的主線程,然后把Block中的任務放到main_queue中,main_queue中的任務會被取出來放到主線程中執行,但主線程種鴿時候已經被阻塞了,所以Block種鴿的任務就不能完成,它不完成,dispatch_sync就會一直阻塞主線程.導致主線程一直卡死.這就是死鎖現象. */
(2)現象2
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL); NSLog(@"輸出1.之前==> %@",[NSThread currentThread]); dispatch_async(queue, ^{ NSLog(@"輸出2.sync之前==> %@",[NSThread currentThread]); dispatch_sync(queue, ^{ NSLog(@"輸出3.sync==> %@",[NSThread currentThread]); }); NSLog(@"輸出4.sync之后==> %@",[NSThread currentThread]); }); NSLog(@"輸出5.之后==> %@",[NSThread currentThread]); /** 解釋 1. 當前線程為默認的主線程 2. 輸出結果為,輸出1,輸出5和輸出2 執行了輸出.輸出3和輸出4沒有被執行. 3. 按照執行順序分析. (1)我們創建的隊列queue是一個串行隊列(DISPATCH_QUEUE_SERIAL).串行隊列的特點是,所持有的任務會取出一個執行一個.當前任務沒有執行完,下一個任務不會被執行. (2)打印出輸出1. (3)在queue隊列中開啟了一個異步任務(async).異步任務的特點是,當前的線程不會被阻塞.所以有了兩條線程,一條是主線程中執行輸出5.另一條是在新開辟的queue線程中執行輸出2. (4)在開辟的queue線程中,又執行了一個同步的任務(sync),同步任務的特點是執行一個任務會阻塞當前的線程.當前的線程是queue,已經被阻塞了.又要求它去執行下一個任務.就造成了死鎖現象.所以 sync 所在的線程被卡死了,輸出3和輸出4自然就不會打印了. */
六.GCD常用方法
1.延遲
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<#delayInSeconds#> * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ <#code to be executed after a specified delay#> });
2.從其他線程回到主線程的方法
dispatch_async(dispatch_get_main_queue(), ^{ // 回到主線程之后,需要執行的代碼
});
3. 一次性執行
static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // code to be executed once
});
&onceToken的意思: &表示取地址符,這個是定義一個靜態變量,然后再dispatch_once函數第一次運行時,寫入數據,之后就不會再次寫入,可以保證后面的block函數內部的代碼只被執行一次.
4.自定義dispatch_queue_t
dispatch_queue_t urls_queue = dispatch_queue_create("blog.devtang.com", NULL); dispatch_async(urls_queue, ^{ // your code
});
5.程序在后台較長時間運行.
// AppDelegate.h文件
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundUpdateTask; // AppDelegate.m文件
- (void)applicationDidEnterBackground:(UIApplication *)application { [self beingBackgroundUpdateTask]; // 在這里加上你需要長久運行的代碼
[self endBackgroundUpdateTask]; } - (void)beingBackgroundUpdateTask { self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ [self endBackgroundUpdateTask]; }]; } - (void)endBackgroundUpdateTask { [[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask]; self.backgroundUpdateTask = UIBackgroundTaskInvalid; }
6.並行隊列異步任務的具體使用
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 執行一些耗時間的操作
dispatch_async(dispatch_get_main_queue(), ^{ // 回到主線程,刷新UI,或者點擊觸發UI事件
}); }); // 實際使用
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSURL * url = [NSURL URLWithString:@"https://gd2.alicdn.com/imgextra/i1/0/TB1p6QnOFXXXXbFXFXXXXXXXXXX_!!0-item_pic.jpg"]; NSData * data = [[NSData alloc]initWithContentsOfURL:url]; UIImage *image = [[UIImage alloc]initWithData:data]; if (data != nil) { dispatch_async(dispatch_get_main_queue(), ^{ self.imageView_one.image = image; }); } });
7. dispatch_barrier_sync
barrier 障礙物
dispatch_barrier_async是在前面的任務執行結束后它才執行,而且它后面的任務等它執行完成之后才會執行
dispatch_queue_t queue = dispatch_queue_create("customIdenrifier", NULL); dispatch_async(queue, ^{ [NSThread sleepForTimeInterval:2]; NSLog(@"dispatch_async1"); }); dispatch_async(queue, ^{ [NSThread sleepForTimeInterval:4]; NSLog(@"dispatch_async2"); }); dispatch_barrier_sync(queue, ^{ NSLog(@"dispatch_barrier_async"); [NSThread sleepForTimeInterval:4]; }); dispatch_async(queue, ^{ [NSThread sleepForTimeInterval:1]; NSLog(@"dispatch_async3"); });
8. dispatch_apply
多次執行里面的代碼塊
參數1: 執行的次數
參數2: 在那個隊列中執行
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_apply(5, queue, ^(size_t index) { NSLog(@"第%zu次執行",index); });
七.代碼實現串行隊列
使用串行隊列時首先要創建一個串行隊列,然后調用異步調用方法,在此方法中傳入串行隊列和線程操作即可自動執行。下面使用線程隊列演示圖片的加載過程,你會發現多張圖片會按順序加載,因為當前隊列中只有一個線程。
#import "Level_four_itemFiveViewController.h"
#import "Masonry.h"
#import "Header.h"
#define ItemWH (ScreenW - MARGIN*4) / 3
#define MARGIN 20
#define MYQUEUE "myThreadQueue1"
@interface Level_four_itemFiveViewController () @property (nonatomic, strong) UIButton * clickedButton; @property (nonatomic, strong) NSMutableArray * imageViewsArrayM; @property (nonatomic, strong) NSMutableArray * imageNamesArrayM; @end
@implementation Level_four_itemFiveViewController #pragma mark - 生命周期
#pragma mark viewDidLoad
- (void)viewDidLoad { [super viewDidLoad]; [self basicSetting]; [self sendNetWorking]; [self initUI]; } #pragma mark - 點擊事件
- (void)clickedButtonClickd { // 多線程下載圖片
NSInteger count = self.imageNamesArrayM.count; //創建一個串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create(MYQUEUE, DISPATCH_QUEUE_SERIAL); //創建多個線程用於填充圖片
for (int i = 0; i < count; ++i) { //異步執行隊列任務
dispatch_async(serialQueue, ^{ [self loadImage:i]; }); } } #pragma mark 加載圖片
-(void)loadImage:(NSUInteger )index{ //請求數據
NSString * urlStr = [self.imageNamesArrayM objectAtIndex:index]; NSURL *url=[NSURL URLWithString:urlStr]; NSData *data=[NSData dataWithContentsOfURL:url]; //更新UI界面,此處調用了GCD主線程隊列的方法
dispatch_queue_t mainQueue= dispatch_get_main_queue(); dispatch_async(mainQueue, ^{ [self updateImageWithData:data andIndex:index]; }); } #pragma mark 將圖片顯示到界面
-(void)updateImageWithData:(NSData *)data andIndex:(NSInteger)index{ UIImage *image=[UIImage imageWithData:data]; UIImageView *imageView= self.imageViewsArrayM[index]; imageView.image=image; } #pragma mark - 網絡請求
- (void)sendNetWorking { //創建圖片鏈接
NSArray * array = @[ @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175036054-1229067335.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175046054-1410401557.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175055570-783207556.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175103820-2144487664.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175109523-327441423.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175115961-859836922.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175131195-2009565896.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175136617-306726060.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175036054-1229067335.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175046054-1410401557.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175055570-783207556.png", ]; self.imageNamesArrayM = [NSMutableArray arrayWithArray:array]; } #pragma mark - 實現方法
#pragma mark 基本設置
- (void)basicSetting { self.title = @"串行隊列"; self.view.backgroundColor = [UIColor whiteColor]; } #pragma mark - UI布局
- (void)initUI { NSInteger total = self.imageNamesArrayM.count; for (int i = 0; i < total; i ++) { NSInteger column = i % 3; // 列數
NSInteger row = i / 3; // 行數
UIImageView * imageView = [[UIImageView alloc] init]; imageView.backgroundColor = [UIColor orangeColor]; [self.view addSubview:imageView]; [imageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.mas_equalTo(self.view).with.offset((MARGIN + ItemWH) * column + MARGIN); make.size.mas_equalTo(CGSizeMake(ItemWH, ItemWH)); make.top.mas_equalTo(self.view).with.offset(74 + (MARGIN + ItemWH) * row); }]; [self.imageViewsArrayM addObject:imageView]; } [self.view addSubview:self.clickedButton]; [self.clickedButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.mas_equalTo(self.view); make.right.mas_equalTo(self.view); make.bottom.mas_equalTo(self.view); make.height.mas_equalTo(40); }]; } #pragma mark - setter & getter
- (UIButton *)clickedButton { if (_clickedButton == nil) { self.clickedButton = [UIButton buttonWithType:UIButtonTypeCustom]; self.clickedButton.backgroundColor = [UIColor orangeColor]; [self.clickedButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; self.clickedButton.titleLabel.font = [UIFont systemFontOfSize:15]; [self.clickedButton setTitle:@"加載圖片" forState:UIControlStateNormal]; [self.clickedButton addTarget:self action:@selector(clickedButtonClickd) forControlEvents:UIControlEventTouchUpInside]; } return _clickedButton; } - (NSMutableArray *)imageViewsArrayM { if (_imageViewsArrayM == nil) { self.imageViewsArrayM = [NSMutableArray arrayWithCapacity:0]; } return _imageViewsArrayM; } - (NSMutableArray *)imageNamesArrayM { if (_imageNamesArrayM == nil) { self.imageNamesArrayM = [NSMutableArray arrayWithCapacity:0]; } return _imageNamesArrayM; } @end
八.代碼實現並行隊列
並發隊列同樣是使用dispatch_queue_create()方法創建,只是最后一個參數指定為DISPATCH_QUEUE_CONCURRENT進行創建,但是在實際開發中我們通常不會重新創建一個並發隊列而是使用dispatch_get_global_queue()方法取得一個全局的並發隊列(當然如果有多個並發隊列可以使用前者創建)。下面通過並行隊列演示一下多個圖片的加載. 其他代碼和串行隊列的實現相同,只是點擊事件有區別
- (void)clickedButtonClickd { // 多線程下載圖片
NSInteger count = self.imageNamesArrayM.count; //創建一個並行隊列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //創建多個線程用於填充圖片
for (int i = 0; i < count; ++i) { //異步執行隊列任務
dispatch_async(globalQueue, ^{ [self loadImage:i]; }); //同步執行隊列任務
dispatch_sync(globalQueue, ^{ // 所有的圖片都在主線程中加載.主線程被阻塞.導致所有照片一次性被顯示.
}); /** 在GCD中一個操作是多線程執行還是單線程執行取決於當前隊列類型和執行方法,只有隊列類型為並行隊列並且使用異步方法執行時才能在多個線程中執行。 串行隊列可以按順序執行,並行隊列的異步方法無法確定執行順序。 UI界面的更新最好采用同步方法,其他操作采用異步方法。 */ } }
九.線程同步(NSLock,@synchronized代碼塊,GCD信號機制)
說到多線程就不得不提多線程中的鎖機制,多線程操作過程中往往多個線程是並發執行的,同一個資源可能被多個線程同時訪問,造成資源搶奪,這個過程中如果沒有鎖機制往往會造成重大問題。舉例來說,每年春節都是一票難求,在12306買票的過程中,成百上千的票瞬間就消失了。不妨假設某輛車有1千張票,同時有幾萬人在搶這列車的車票,順利的話前面的人都能買到票。但是如果現在只剩下一張票了,而同時還有幾千人在購買這張票,雖然在進入購票環節的時候會判斷當前票數,但是當前已經有100個線程進入購票的環節,每個線程處理完票數都會減1,100個線程執行完當前票數為-99,遇到這種情況很明顯是不允許的。
這里不妨還拿圖片加載來舉例,假設現在有8張圖片,但是有15個線程都准備加載這8張圖片,約定不能重復加載同一張圖片,這樣就形成了一個資源搶奪的情況。在下面的程序中將創建9張圖片,每次讀取照片鏈接時首先判斷當前鏈接數是否大於1,用完一個則立即移除,最多只有8個。
#import "Level_four_itemSevenViewController.h"
#import "Masonry.h"
#import "Header.h"
#define ItemWH (ScreenW - MARGIN*4) / 3
#define MARGIN 20
#define MYQUEUE "myThreadQueue1"
@interface Level_four_itemSevenViewController () { // 使用GCD解決資源搶占問題
dispatch_semaphore_t _semaphore;//定義一個信號量
} @property (nonatomic, strong) UIButton * clickedButton; @property (nonatomic, strong) NSMutableArray * imageViewsArrayM; @property (nonatomic, strong) NSMutableArray * imageNamesArrayM; @property (nonatomic, strong) NSLock * lock; @end
@implementation Level_four_itemSevenViewController /** 說明 拿圖片加載來舉例,假設現在有8張圖片,但是有15個線程都准備加載這8張圖片,約定不能重復加載同一張圖片,這樣就形成了一個資源搶奪的情況。在下面的程序中將創建8張圖片,每次讀取照片鏈接時首先判斷當前鏈接數是否大於1,用完一個則立即移除,最多只有8個. */
#pragma mark - 生命周期
#pragma mark viewDidLoad
- (void)viewDidLoad { [super viewDidLoad]; [self basicSetting]; [self sendNetWorking]; [self initUI]; } #pragma mark - 系統代理
#pragma mark - 點擊事件
- (void)clickedButtonClickd { // 多線程現在圖片
NSInteger count = 15; //創建一個串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create(MYQUEUE, DISPATCH_QUEUE_SERIAL); //創建多個線程用於填充圖片
for (int i = 0; i < count; ++i) { //異步執行隊列任務
dispatch_async(serialQueue, ^{ [self loadImage:i]; }); } } #pragma mark 加載圖片
-(void)loadImage:(NSUInteger )index{ /** 三種方式 一個線程A已經開始獲取圖片鏈接,獲取完之后還沒有來得及從self.imageNamesArrayM中刪除,另一個線程B已經進入相應代碼中,由於每次讀取的都是self.imageNamesArrayM的最后一個元素,因此后面的線程其實和前面線程取得的是同一個圖片鏈接這樣就造成圖中看到的情況。要解決這個問題,只要保證線程A進入相應代碼之后B無法進入,只有等待A完成相關操作之后B才能進入即可。這樣才不會出錯. */
// 1
[self ThreadSynchronization_wayOneWithIndex:index]; // 2 //[self ThreadSynchronization_wayTwoWithIndex:index]; // 3 //[self ThreadSynchronization_wayThreeWithIndex:index];
} - (void)ThreadSynchronization_wayOneWithIndex:(NSInteger)index { NSString * urlStr; NSData *data; /** 線程同步方法1: NSLock 1.線程使用前加鎖,線程使用后解鎖 2.iOS中對於資源搶占的問題可以使用同步鎖NSLock來解決,使用時把需要加鎖的代碼(以后暫時稱這段代碼為”加鎖代碼“)放到NSLock的lock和unlock之間,一個線程A進入加鎖代碼之后由於已經加鎖,另一個線程B就無法訪問,只有等待前一個線程A執行完加鎖代碼后解鎖,B線程才能訪問加鎖代碼。需要注意的是lock和unlock之間的”加鎖代碼“應該是搶占資源的讀取和修改代碼,不要將過多的其他操作代碼放到里面,否則一個線程執行的時候另一個線程就一直在等待,就無法發揮多線程的作用了 */ [self.lock lock]; if (self.imageNamesArrayM.count > 0) { urlStr = [self.imageNamesArrayM lastObject]; [self.imageNamesArrayM removeObject:urlStr]; } [self.lock unlock]; if (urlStr) { NSURL * url = [NSURL URLWithString:urlStr]; data = [NSData dataWithContentsOfURL:url]; } //更新UI界面,此處調用了GCD主線程隊列的方法
dispatch_queue_t mainQueue= dispatch_get_main_queue(); dispatch_async(mainQueue, ^{ [self updateImageWithData:data andIndex:index]; }); } - (void)ThreadSynchronization_wayTwoWithIndex:(NSInteger)index { /** 線程同步方法2: @synchronized代碼塊 使用@synchronized解決線程同步問題相比較NSLock要簡單一些,日常開發中也更推薦使用此方法。首先選擇一個對象作為同步對象(一般使用self),然后將”加鎖代碼”(爭奪資源的讀取、修改代碼)放到代碼塊中。@synchronized中的代碼執行時先檢查同步對象是否被另一個線程占用,如果占用該線程就會處於等待狀態,直到同步對象被釋放 */ NSString * urlStr; NSData *data; @synchronized (self) { if (self.imageNamesArrayM.count > 0) { urlStr = [self.imageNamesArrayM lastObject]; [self.imageNamesArrayM removeObject:urlStr]; } } if (urlStr) { NSURL * url = [NSURL URLWithString:urlStr]; data = [NSData dataWithContentsOfURL:url]; } //更新UI界面,此處調用了GCD主線程隊列的方法
dispatch_queue_t mainQueue= dispatch_get_main_queue(); dispatch_async(mainQueue, ^{ [self updateImageWithData:data andIndex:index]; }); } - (void)ThreadSynchronization_wayThreeWithIndex:(NSInteger)index { /** 線程同步方法三: GCD信號機制 初始化信號量 參數是信號量初始值 在GCD中提供了一種信號機制,也可以解決資源搶占問題(和同步鎖的機制並不一樣)。GCD中信號量是dispatch_semaphore_t類型,支持信號通知和信號等待。每當發送一個信號通知,則信號量+1;每當發送一個等待信號時信號量-1,;如果信號量為0則信號會處於等待狀態,直到信號量大於0開始執行。根據這個原理我們可以初始化一個信號量變量,默認信號量設置為1,每當有線程進入“加鎖代碼”之后就調用信號等待命令(此時信號量為0)開始等待,此時其他線程無法進入,執行完后發送信號通知(此時信號量為1),其他線程開始進入執行,如此一來就達到了線程同步目的。 */ NSString * urlStr; NSData *data; _semaphore = dispatch_semaphore_create(1); dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); if (self.imageNamesArrayM.count > 0) { urlStr = [self.imageNamesArrayM lastObject]; [self.imageNamesArrayM removeObject:urlStr]; } // 信號通知
dispatch_semaphore_signal(_semaphore); if (urlStr) { NSURL * url = [NSURL URLWithString:urlStr]; data = [NSData dataWithContentsOfURL:url]; } //更新UI界面,此處調用了GCD主線程隊列的方法
dispatch_queue_t mainQueue= dispatch_get_main_queue(); dispatch_async(mainQueue, ^{ [self updateImageWithData:data andIndex:index]; }); } #pragma mark 將圖片顯示到界面
-(void)updateImageWithData:(NSData *)data andIndex:(NSInteger)index{ UIImage *image=[UIImage imageWithData:data]; UIImageView *imageView= self.imageViewsArrayM[index]; imageView.image=image; } #pragma mark - 網絡請求
- (void)sendNetWorking { //創建圖片鏈接
NSArray * array = @[ @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175036054-1229067335.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175046054-1410401557.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175055570-783207556.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175103820-2144487664.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175109523-327441423.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175115961-859836922.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175131195-2009565896.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175136617-306726060.png", ]; self.imageNamesArrayM = [NSMutableArray arrayWithArray:array]; } #pragma mark - 實現方法
#pragma mark 基本設置
- (void)basicSetting { self.title = @"線程同步(NSLock,@synchronized代碼塊,GCD信號機制"; self.view.backgroundColor = [UIColor whiteColor]; } #pragma mark - UI布局
- (void)initUI { NSInteger total = 15; for (int i = 0; i < total; i ++) { NSInteger column = i % 3; // 列數
NSInteger row = i / 3; // 行數
UIImageView * imageView = [[UIImageView alloc] init]; imageView.backgroundColor = [UIColor orangeColor]; [self.view addSubview:imageView]; [imageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.mas_equalTo(self.view).with.offset((MARGIN + ItemWH) * column + MARGIN); make.size.mas_equalTo(CGSizeMake(ItemWH, ItemWH)); make.top.mas_equalTo(self.view).with.offset(74 + (MARGIN + ItemWH) * row); }]; [self.imageViewsArrayM addObject:imageView]; } [self.view addSubview:self.clickedButton]; [self.clickedButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.mas_equalTo(self.view); make.right.mas_equalTo(self.view); make.bottom.mas_equalTo(self.view); make.height.mas_equalTo(40); }]; } #pragma mark - setter & getter
- (UIButton *)clickedButton { if (_clickedButton == nil) { self.clickedButton = [UIButton buttonWithType:UIButtonTypeCustom]; self.clickedButton.backgroundColor = [UIColor orangeColor]; [self.clickedButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; self.clickedButton.titleLabel.font = [UIFont systemFontOfSize:15]; [self.clickedButton setTitle:@"加載圖片" forState:UIControlStateNormal]; [self.clickedButton addTarget:self action:@selector(clickedButtonClickd) forControlEvents:UIControlEventTouchUpInside]; } return _clickedButton; } - (NSMutableArray *)imageViewsArrayM { if (_imageViewsArrayM == nil) { self.imageViewsArrayM = [NSMutableArray arrayWithCapacity:0]; } return _imageViewsArrayM; } - (NSMutableArray *)imageNamesArrayM { if (_imageNamesArrayM == nil) { self.imageNamesArrayM = [NSMutableArray arrayWithCapacity:0]; } return _imageNamesArrayM; } - (NSLock *)lock { if (_lock == nil) { self.lock = [[NSLock alloc] init]; } return _lock; } @end
十.GCD控制線程通訊
由於線程的調度是透明的,程序有時候很難對它進行有效的控制,為了解決這個問題iOS提供了NSCondition來控制線程通信(同前面GCD的信號機制類似)。NSCondition實現了NSLocking協議,所以它本身也有lock和unlock方法,因此也可以將它作為NSLock解決線程同步問題,此時使用方法跟NSLock沒有區別,只要在線程開始時加鎖,取得資源后釋放鎖即可,這部分內容比較簡單在此不再演示。當然,單純解決線程同步問題不是NSCondition設計的主要目的,NSCondition更重要的是解決線程之間的調度關系(當然,這個過程中也必須先加鎖、解鎖)。NSCondition可以調用wati方法控制某個線程處於等待狀態,直到其他線程調用signal(此方法喚醒一個線程,如果有多個線程在等待則任意喚醒一個)或者broadcast(此方法會喚醒所有等待線程)方法喚醒該線程才能繼續。
#import "Level_four_itemEightViewController.h"
#import "Masonry.h"
#import "Header.h"
#define ItemWH (ScreenW - MARGIN*4) / 3
#define MARGIN 20
#define MYQUEUE "myThreadQueue1"
#define ImageCount 8
@interface Level_four_itemEightViewController () { NSMutableArray *_imagesArrayM; } @property (nonatomic, strong) UIButton * createButton; @property (nonatomic, strong) UIButton * loadButton; @property (nonatomic, strong) NSMutableArray * imageViewsArrayM; @property (nonatomic, strong) NSMutableArray * imageNamesArrayM; #pragma mark 當前加載的圖片索引(圖片鏈接地址連續) @property (atomic,assign) int currentIndex; @property (nonatomic, strong) NSCondition * condition; @end
@implementation Level_four_itemEightViewController /** 線程的調度是透明的,程序有時候很難對它進行有效的控制.iOS提供了NSCondition來控制線程通信.NSCondition也遵守NSLocking協議, 所以它本身就有lock和unlock方法.NSCondation可以解決線程同步的問題,但是更重要的是能解決線程之間的調度問題,當然這個過程也需要先加鎖和解鎖. wait方法控制某個線程處於等待狀態. signal方法喚起一個線程,如果有多個線程,則任意喚起一個. broadcast方法喚起所有等待的線程. */
#pragma mark - 生命周期
#pragma mark viewDidLoad
- (void)viewDidLoad { [super viewDidLoad]; [self basicSetting]; [self sendNetWorking]; [self initUI]; } #pragma mark - 系統代理
#pragma mark - 點擊事件
#pragma mark 產生圖片
- (void)createButtonClickd { // 異步創建一張圖片
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 創建圖片鏈接
dispatch_async(globalQueue, ^{ [self createImageName]; }); } - (void)createImageName { [self.condition lock]; // 如果當前有圖片則不創建,線程處於等待狀態
if (self.imageNamesArrayM.count > 0) { NSLog(@"createImageName wait, current:%i",_currentIndex); [self.condition wait]; } else { NSLog(@"createImageName work, current:%i",_currentIndex); //生產者,每次生產1張圖片
NSString * str = [_imagesArrayM objectAtIndex:_currentIndex]; [self.imageNamesArrayM addObject:str]; _currentIndex ++; //創建完圖片則發出信號喚醒其他等待線程
[self.condition signal]; } [self.condition unlock]; } #pragma mark 加載圖片
- (void)loadButtonClickd { if (_currentIndex > 8) { UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"提示" message:@"圖片張數超過8張,返回重試!" preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction * cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { [self.navigationController popViewControllerAnimated:YES]; }]; [alert addAction:cancel]; [self presentViewController:alert animated:YES completion:nil]; } else { // 多線程下載圖片 //創建一個串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create(MYQUEUE, DISPATCH_QUEUE_SERIAL); //創建多個線程用於填充圖片 //異步執行隊列任務
dispatch_async(serialQueue, ^{ [self loadImage:_currentIndex - 1]; }); } } #pragma mark 加載圖片
-(void)loadImage:(NSUInteger)index{ // 加鎖
[self.condition lock]; // 如果當前有圖片資源則加載,否則等待
if (self.imageNamesArrayM.count > 0) { NSLog(@"loadImage work,index is %lu",(unsigned long)index); [self loadAnUpdateImageWithIndex:index]; [_condition broadcast]; }else{ NSLog(@"loadImage wait,index is %lu",(unsigned long)index); //線程等待
[_condition wait]; NSLog(@"loadImage resore,index is %lu",(unsigned long)index); //一旦創建完圖片立即加載
[self loadAnUpdateImageWithIndex:index]; } [self.condition unlock]; } -(void)loadAnUpdateImageWithIndex:(NSUInteger)index{ //請求數據
NSData *data= [self requestData:index]; //更新UI界面,此處調用了GCD主線程隊列的方法
dispatch_queue_t mainQueue= dispatch_get_main_queue(); dispatch_sync(mainQueue, ^{ UIImage *image=[UIImage imageWithData:data]; UIImageView *imageView= self.imageViewsArrayM[index]; imageView.image=image; }); } -(NSData *)requestData:(NSUInteger)index{ NSData *data; NSString *name; name = [self.imageNamesArrayM lastObject]; [self.imageNamesArrayM removeObject:name]; if(name){ NSURL *url=[NSURL URLWithString:name]; data=[NSData dataWithContentsOfURL:url]; } return data; } #pragma mark - 網絡請求
- (void)sendNetWorking { //創建圖片鏈接
NSArray * array = @[ @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175036054-1229067335.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175046054-1410401557.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175055570-783207556.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175103820-2144487664.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175109523-327441423.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175115961-859836922.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175131195-2009565896.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175136617-306726060.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175036054-1229067335.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175046054-1410401557.png", @"http://images2015.cnblogs.com/blog/844918/201612/844918-20161230175055570-783207556.png", ]; _imagesArrayM = [NSMutableArray arrayWithArray:array]; } #pragma mark - 實現方法
#pragma mark 基本設置
- (void)basicSetting { self.title = @"串行隊列"; self.view.backgroundColor = [UIColor whiteColor]; _currentIndex = 0; } #pragma mark - UI布局
- (void)initUI { NSInteger total = 8; for (int i = 0; i < total; i ++) { NSInteger column = i % 3; // 列數
NSInteger row = i / 3; // 行數
UIImageView * imageView = [[UIImageView alloc] init]; imageView.backgroundColor = [UIColor orangeColor]; [self.view addSubview:imageView]; [imageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.mas_equalTo(self.view).with.offset((MARGIN + ItemWH) * column + MARGIN); make.size.mas_equalTo(CGSizeMake(ItemWH, ItemWH)); make.top.mas_equalTo(self.view).with.offset(74 + (MARGIN + ItemWH) * row); }]; [self.imageViewsArrayM addObject:imageView]; } [self.view addSubview:self.createButton]; [self.createButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.mas_equalTo(self.view); make.width.mas_equalTo(ScreenW / 2); make.bottom.mas_equalTo(self.view); make.height.mas_equalTo(40); }]; [self.view addSubview:self.loadButton]; [self.loadButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.mas_equalTo(self.view); make.width.mas_equalTo(ScreenW / 2); make.bottom.mas_equalTo(self.view); make.height.mas_equalTo(40); }]; } #pragma mark - setter & getter
- (UIButton *)createButton { if (_createButton == nil) { self.createButton = [UIButton buttonWithType:UIButtonTypeCustom]; self.createButton.backgroundColor = [UIColor orangeColor]; [self.createButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; self.createButton.titleLabel.font = [UIFont systemFontOfSize:15]; [self.createButton setTitle:@"產生圖片" forState:UIControlStateNormal]; [self.createButton addTarget:self action:@selector(createButtonClickd) forControlEvents:UIControlEventTouchUpInside]; } return _createButton; } - (UIButton *)loadButton { if (_loadButton == nil) { self.loadButton = [UIButton buttonWithType:UIButtonTypeCustom]; self.loadButton.backgroundColor = [UIColor greenColor]; [self.loadButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; self.loadButton.titleLabel.font = [UIFont systemFontOfSize:15]; [self.loadButton setTitle:@"加載圖片" forState:UIControlStateNormal]; [self.loadButton addTarget:self action:@selector(loadButtonClickd) forControlEvents:UIControlEventTouchUpInside]; } return _loadButton; } - (NSMutableArray *)imageViewsArrayM { if (_imageViewsArrayM == nil) { self.imageViewsArrayM = [NSMutableArray arrayWithCapacity:0]; } return _imageViewsArrayM; } - (NSMutableArray *)imageNamesArrayM { if (_imageNamesArrayM == nil) { self.imageNamesArrayM = [NSMutableArray arrayWithCapacity:0]; } return _imageNamesArrayM; } - (NSCondition *)condition { if (_condition == nil) { self.condition = [[NSCondition alloc] init]; } return _condition; } @end
本篇博客所用的Demo地址: https://github.com/mancongiOS/multithreading
技術支持:
簡書.作者:伯恩的遺產. 地址:http://www.jianshu.com/p/0b0d9b1f1f19
博客園.作者:文頂頂. 地址:http://www.cnblogs.com/wendingding/p/3805088.html
博客園.作者:KenshinCui 地址:http://www.cnblogs.com/kenshincui/p/3983982.html