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函式建立序列佇列
- GCD中獲得序列有2種途徑
// 建立序列佇列(佇列型別傳遞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其它常用方法
- 延時執行
方法一:呼叫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秒後執行這裡的程式碼...
});
- 一次性程式碼
使用dispatch_once函式能保證某段程式碼在程式執行過程中只被執行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只執行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種需求
- 首先:分別非同步執行2個耗時的操作
- 其次:等2個非同步操作都執行完畢後,再回到主執行緒執行操作
- 有這麼1種需求
如果想要快速高效地實現上述需求,可以考慮用佇列組
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(), ^{
// 等前面的非同步操作都執行完畢後,回到主執行緒...
});