GCD總結(jié)
NSOperation總結(jié)
iOS面試題(三)多線程開發(fā) - 簡(jiǎn)書
iOS基礎(chǔ)深入補(bǔ)完計(jì)劃--多線程(面試題)匯總 - CocoaChina_讓移動(dòng)開發(fā)更簡(jiǎn)單
可能碰到的iOS筆試面試題(18)--多線程 - 簡(jiǎn)書
進(jìn)程
- 在系統(tǒng)中正在運(yùn)行的一個(gè)應(yīng)用程序沸移,每個(gè)進(jìn)程之間是獨(dú)立的啰脚,每個(gè)進(jìn)程均運(yùn)行在其專用且受保護(hù)的內(nèi)存空間內(nèi)
- 進(jìn)程用于指代一個(gè)可執(zhí)行程序缝彬,他可以包含多個(gè)線程(蘋果官方解釋)
線程
- 1個(gè)進(jìn)程想要執(zhí)行任務(wù),必須得有線程(每一個(gè)進(jìn)程至少要有1條線程),線程是進(jìn)程的基本執(zhí)行單元闽巩,一個(gè)進(jìn)程(程序)的所有任務(wù)都在線程中執(zhí)行线得。
- 線程用于指代一個(gè)獨(dú)立執(zhí)行的代碼路徑(蘋果官方解釋)
主線程
- 一個(gè)iOS程序運(yùn)行后,默認(rèn)會(huì)開啟1條線程肄程,稱為“主線程”或者“UI線程”锣吼。
- 作用
- 顯示/刷新UI界面
- 處理UI事件
- 使用注意
- 別將比較耗時(shí)的操作放在主線程中,操作影響UI的流暢度蓝厌,給用戶一種卡的體驗(yàn)
多線程
- 概念
- 一個(gè)程序開啟多條線程玄叠,每條線程可以并行(同時(shí))執(zhí)行不同的任務(wù)
- 原理
- 多個(gè)線程并發(fā)執(zhí)行,其實(shí)質(zhì)是CPU快速地在多條線程之間調(diào)度(切換)
- 當(dāng)CPU調(diào)度線程的時(shí)間足夠快拓提,就會(huì)造成多線程并發(fā)執(zhí)行的假象
串行
- 定義
- 順序執(zhí)行,一個(gè)個(gè)執(zhí)行读恃,在同一時(shí)間內(nèi),1個(gè)線程只能執(zhí)行一個(gè)任務(wù) 代态。
并行
- 定義
- 可以一起執(zhí)行寺惫,同一時(shí)間內(nèi),一個(gè)進(jìn)程可以執(zhí)行多個(gè)任務(wù)多線程:一個(gè)線程中可以開啟多條線程胆数,每條線程可以并發(fā)(同時(shí))執(zhí)行不同的任務(wù)肌蜻。
- 優(yōu)點(diǎn):
- 提高程序的執(zhí)行效率,能適當(dāng)提高資源利用率必尼。
- 缺點(diǎn)
- 開啟線程需要占一定的內(nèi)存空間蒋搜,如果開啟大量線程,CPU會(huì)在N多線程之間調(diào)度判莉,CPU會(huì)累死豆挽,消耗大量的CPU資源,降低程序性能券盅,程序設(shè)計(jì)更復(fù)雜帮哈。
原理:同一時(shí)間內(nèi),CPU只能處理一條線程锰镀,只有1條線程在工作娘侍。多線程并發(fā)執(zhí)行,其實(shí)是CPU快速的在多條線程之間切換泳炉。
- 開啟線程需要占一定的內(nèi)存空間蒋搜,如果開啟大量線程,CPU會(huì)在N多線程之間調(diào)度判莉,CPU會(huì)累死豆挽,消耗大量的CPU資源,降低程序性能券盅,程序設(shè)計(jì)更復(fù)雜帮哈。
多線程的方式
1.NSThread
優(yōu)點(diǎn):NSThread 輕量級(jí)最低憾筏,相對(duì)簡(jiǎn)單。
缺點(diǎn):手動(dòng)管理所有的線程活動(dòng)花鹅,如生命周期氧腰、線程同步、睡眠等刨肃。
-
新建
//創(chuàng)建線程方式1 //創(chuàng)建 NSThread * th = [[NSThread alloc] initWithTarget:self selector:@selector(nsThreadMethod) object:nil]; //創(chuàng)建線程方式2 //創(chuàng)建并啟動(dòng) [NSThread detachNewThreadSelector:@selector(nsThreadMethod) toTarget:self withObject:nil]; //隱式創(chuàng)建方式 [self performSelectorInBackground:@selector(nsThreadMethod) withObject:nil]; //不傳遞參數(shù)指定函數(shù)在當(dāng)前線程執(zhí)行 [self performSelector:@selector(doSomething)]; //傳遞參數(shù)指定函數(shù)在當(dāng)前線程執(zhí)行 [self performSelector: @selector(doSomething:) withObject:tempStr]; //傳遞參數(shù)指定函數(shù)2秒后在當(dāng)前線程執(zhí)行 [self performSelector:@selector(doSomething:) withObject:tempStr afterDelay:2.0];
-
就緒
- 將線程放入線程池中
-
運(yùn)行
[th start];
-
阻塞
//休眠2秒 [ NSThread sleepForTimeInterval:2]; //休眠到指定時(shí)間 [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]];
-
銷毀
//結(jié)束線程 [NSThread exit];
-
指定在特定線程執(zhí)行
//在其他線程中指定在主線程執(zhí)行 [self performSelectorOnMainThread:@selector(doSomething:) withObject:tempStr waitUntilDone:YES]; //在主線程指定在后臺(tái)線程執(zhí)行 [self performSelectorInBackground:@selector(doSomething:) withObject:tempStr]; //在主線程中指定某個(gè)特定線程執(zhí)行 [self performSelector:@selector(doSomething:) onThread:newThread withObject:tempStr waitUntilDone:YES];
-
線程常用屬性
//獲取主線程 [NSThread mainThread]; //獲取當(dāng)前線程 [NSThread currentThread]; //設(shè)置線程名字 [newThread setName:@"thread - 1"]; //設(shè)置線程優(yōu)先級(jí) [newThread setThreadPriority:1.0]; //IOS 8 之后 推薦使用下面這種方式設(shè)置線程優(yōu)先級(jí) //NSQualityOfServiceUserInteractive:最高優(yōu)先級(jí)古拴,用于用戶交互事件 //NSQualityOfServiceUserInitiated:次高優(yōu)先級(jí),用于用戶需要馬上執(zhí)行的事件 //NSQualityOfServiceDefault:默認(rèn)優(yōu)先級(jí)真友,主線程和沒(méi)有設(shè)置優(yōu)先級(jí)的線程都默認(rèn)為這個(gè)優(yōu)先級(jí) //NSQualityOfServiceUtility:普通優(yōu)先級(jí)黄痪,用于普通任務(wù) //NSQualityOfServiceBackground:最低優(yōu)先級(jí),用于不重要的任務(wù) [newThread setQualityOfService:NSQualityOfServiceUtility]; //判斷線程是否是主線程 [newThread isMainThread]; //線程狀態(tài) //是否已經(jīng)取消 [newThread isCancelled]; //是否已經(jīng)結(jié)束 [newThread isFinished]; //是否正在執(zhí)行 [newThread isExecuting];
-
線程的同步
- 線程同步就是為了解決多線程同時(shí)訪問(wèn)公共共享數(shù)據(jù)盔然,而導(dǎo)致數(shù)據(jù)錯(cuò)亂的問(wèn)題满力,然后使用同步的方式讓公共數(shù)據(jù)同一時(shí)間內(nèi)只能被一個(gè)線程訪問(wèn)來(lái)避免數(shù)據(jù)錯(cuò)亂的問(wèn)題
- 實(shí)現(xiàn)線程同步的幾種方式
- 1.@synchronized(對(duì)象)關(guān)鍵字
-(void)taskRun { while (count>0) { @synchronized(self) { // 需要鎖定的代碼 [NSThread sleepForTimeInterval:0.1]; count--; NSLog(@"threadName:%@ count:%d ",[NSThread currentThread].name, count); } } }
- 2.NSLock同步鎖
-(void)taskRun { while (count>0) { @synchronized(self) { // 需要鎖定的代碼 [NSThread sleepForTimeInterval:0.1]; count--; NSLog(@"threadName:%@ count:%d ",[NSThread currentThread].name, count); } } }
- 3.NSCondition同步鎖和線程檢查器
//鎖主要為了當(dāng)檢測(cè)條件時(shí)保護(hù)數(shù)據(jù)源焕参,執(zhí)行條件引發(fā)的任務(wù);線程檢查器主要是根據(jù)條件決定是否繼續(xù)運(yùn)行線程油额,即線程是否被阻塞。先創(chuàng)建一個(gè)NSCondition對(duì)象 condition=[[NSCondition alloc]init]; //使用同步鎖的方式和NSLock相似 -(void)taskRun { while (count>0) { [condition lock]; [NSThread sleepForTimeInterval:0.1]; count--; NSLog(@"threadName:%@ count:%d ",[NSThread currentThread].name, count); [condition unlock]; } } //NSCondition可以讓線程進(jìn)行等待刻帚,然后獲取到CPU發(fā)信號(hào)告訴線程不用在等待潦嘶,可以繼續(xù)執(zhí)行,上述的例子我們稍作修改崇众,我們讓線程三專門用于發(fā)送信號(hào)源 NSThread *thread1=[[NSThread alloc]initWithTarget:self selector:@selector(taskRun) object:nil]; thread1.name=@"thread-1"; NSThread *thread2=[[NSThread alloc]initWithTarget:self selector:@selector(taskRun) object:nil]; thread2.name=@"thread-2"; NSThread *thread3=[[NSThread alloc]initWithTarget:self selector:@selector(taskRun1) object:nil]; thread3.name=@"thread-3"; [thread1 start]; [thread2 start]; [thread3 start]; //taskRun1函數(shù)用于發(fā)送信號(hào)源 -(void)taskRun1 { while (YES) { [condition lock]; [NSThread sleepForTimeInterval:2]; [condition signal]; [condition unlock]; } } //taskRun函數(shù) 用于執(zhí)行對(duì)count的操作 -(void)taskRun { while (count>0) { [condition lock]; [condition wait]; [NSThread sleepForTimeInterval:0.1]; count--; NSLog(@"threadName:%@ count:%d ",[NSThread currentThread].name, count); [condition unlock]; } }
- 1.@synchronized(對(duì)象)關(guān)鍵字
2.GCD
優(yōu)點(diǎn):最高效掂僵,避開并發(fā)陷阱。
缺點(diǎn):基于C實(shí)現(xiàn)
-
隊(duì)列
- 作用:用來(lái)存放任務(wù)顷歌。
dispatch_queue_t queue = dispatch_queue_create("123", DISPATCH_QUEUE_SERIAL); //“123”:隊(duì)列標(biāo)示锰蓬。 //DISPATCH_QUEUE_SERIAL:串行 //DISPATCH_QUEUE_CONCURRENT:并行
- UI主線程隊(duì)列 main queue
- dispatch_get_main_queue()
- 串行隊(duì)列
- 順序,一個(gè)個(gè)執(zhí)行眯漩。
- 并行隊(duì)列
- 同時(shí)執(zhí)行很多任務(wù)芹扭。
- 全局隊(duì)列
//第一個(gè)參數(shù):線程的優(yōu)先級(jí),有高赦抖,默認(rèn)舱卡,低,后臺(tái)队萤,由高到低轮锥。 //第二個(gè)參數(shù):備用,默認(rèn)0,不能改成其他 dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
- 全局隊(duì)列和并發(fā)隊(duì)列的區(qū)別:
- 全局隊(duì)列沒(méi)有名稱要尔,并發(fā)隊(duì)列有名稱舍杜。
- 全局隊(duì)列是所有應(yīng)用程序共享。
- 全局隊(duì)列和并發(fā)隊(duì)列的區(qū)別:
- 作用:用來(lái)存放任務(wù)顷歌。
-
任務(wù)
- 執(zhí)行的操作
dispatch_sync(queue, ^{ //code }); //queue:隊(duì)列 //dispatch_sync:同步 //dispatch_async:異步
- 同步
- dispatch_sync赵辕,不開線程既绩,在當(dāng)前線程中執(zhí)行,馬上執(zhí)行
- 異步
- dispatch_async匆帚,開辟線程熬词,在另一個(gè)線程中執(zhí)行,稍后執(zhí)行
- 執(zhí)行的操作
-
使用
- 將任務(wù)添加隊(duì)列中吸重,GCD會(huì)自定將隊(duì)列中的任務(wù)取出互拾,放到對(duì)應(yīng)線程中執(zhí)行,任務(wù)的取出遵循隊(duì)列的FIFO原則:先進(jìn)先出嚎幸,后進(jìn)后出颜矿。
- 延時(shí)線程
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ //code });
- 線程調(diào)度組
dispatch_group_enter、dispatch_group_leave
dispatch_group_enter 標(biāo)志著一個(gè)任務(wù)追加到 group嫉晶,執(zhí)行一次骑疆,相當(dāng)于 group 中未執(zhí)行完畢任務(wù)數(shù)+1
dispatch_group_leave 標(biāo)志著一個(gè)任務(wù)離開了 group田篇,執(zhí)行一次,相當(dāng)于 group 中未執(zhí)行完畢任務(wù)數(shù)-1箍铭。
-
當(dāng) group 中未執(zhí)行完畢任務(wù)數(shù)為0的時(shí)候泊柬,才會(huì)使dispatch_group_wait解除阻塞,以及執(zhí)行追加到dispatch_group_notify中的任務(wù)诈火。
// 實(shí)例化一個(gè)調(diào)度組 dispatch_group_t group = dispatch_group_create(); // 創(chuàng)建隊(duì)列 dispatch_queue_t queue2 = dispatch_get_global_queue(0, 0); // 任務(wù)添加到隊(duì)列queue dispatch_group_enter(group); dispatch_group_async(group, queue2, ^{ //code1 dispatch_group_leave(group); }); dispatch_group_async(group, queue2, ^{ //code2 }); dispatch_group_async(group, queue2, ^{ //code3 }); //獲取所有調(diào)度組里面的異步任務(wù)完成的通知 dispatch_group_notify(group, queue2, ^{ //code4 });
- 一次性線程
static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _instance = [[GetToken alloc] init]; });
- 定時(shí)線程
//創(chuàng)建并行隊(duì)列 dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); // 并行queue //創(chuàng)建一個(gè)GCD定時(shí)器 dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, aQueue); //設(shè)置定時(shí)器的參數(shù) //第一個(gè)參數(shù):要給哪個(gè)設(shè)置定時(shí)器兽赁。 //第二個(gè)參數(shù):開始時(shí)間 //第三個(gè)參數(shù):間隔時(shí)間 //第四個(gè)參數(shù):精準(zhǔn)度,一般為0. dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC); //設(shè)置定時(shí)器調(diào)用的方法冷守。 dispatch_source_set_event_handler(timer, ^{ }); 啟動(dòng) dispatch_resume(timer);
- 暫停線程
- 暫停當(dāng)前線程(阻塞當(dāng)前線程)刀崖,等待指定的 group 中的任務(wù)執(zhí)行完成后,才會(huì)往下繼續(xù)執(zhí)行拍摇。
/** * 隊(duì)列組 dispatch_group_wait */ - (void)groupWait { NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當(dāng)前線程 NSLog(@"group---begin"); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 追加任務(wù)1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"1---%@",[NSThread currentThread]); // 打印當(dāng)前線程 } }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 追加任務(wù)2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"2---%@",[NSThread currentThread]); // 打印當(dāng)前線程 } }); // 等待上面的任務(wù)全部完成后亮钦,會(huì)往下繼續(xù)執(zhí)行(會(huì)阻塞當(dāng)前線程) dispatch_group_wait(group, DISPATCH_TIME_FOREVER); NSLog(@"group---end"); }
- 暫停當(dāng)前線程(阻塞當(dāng)前線程)刀崖,等待指定的 group 中的任務(wù)執(zhí)行完成后,才會(huì)往下繼續(xù)執(zhí)行拍摇。
- 柵欄線程
- 我們有時(shí)需要異步執(zhí)行兩組操作,而且第一組操作執(zhí)行完之后充活,才能開始執(zhí)行第二組操作蜂莉。這樣我們就需要一個(gè)相當(dāng)于柵欄一樣的一個(gè)方法將兩組異步執(zhí)行的操作組給分割起來(lái),當(dāng)然這里的操作組里可以包含一個(gè)或多個(gè)任務(wù)堪唐。這就需要用到dispatch_barrier_async方法在兩個(gè)操作組間形成柵欄巡语。dispatch_barrier_async函數(shù)會(huì)等待前邊追加到并發(fā)隊(duì)列中的任務(wù)全部執(zhí)行完畢之后,再將指定的任務(wù)追加到該異步隊(duì)列中淮菠。然后在dispatch_barrier_async函數(shù)追加的任務(wù)執(zhí)行完畢之后男公,異步隊(duì)列才恢復(fù)為一般動(dòng)作,接著追加任務(wù)到該異步隊(duì)列并開始執(zhí)行
/** * 柵欄方法 dispatch_barrier_async */ - (void)barrier { dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ // 追加任務(wù)1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"1---%@",[NSThread currentThread]); // 打印當(dāng)前線程 } }); dispatch_async(queue, ^{ // 追加任務(wù)2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"2---%@",[NSThread currentThread]); // 打印當(dāng)前線程 } }); dispatch_barrier_async(queue, ^{ // 追加任務(wù) barrier for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"barrier---%@",[NSThread currentThread]);// 打印當(dāng)前線程 } }); dispatch_async(queue, ^{ // 追加任務(wù)3 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"3---%@",[NSThread currentThread]); // 打印當(dāng)前線程 } }); dispatch_async(queue, ^{ // 追加任務(wù)4 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"4---%@",[NSThread currentThread]); // 打印當(dāng)前線程 } }); } /* 2018-02-23 20:48:18.297745+0800 YSC-GCD-demo[20188:5059274] 1---<NSThread: 0x600000079d80>{number = 4, name = (null)} 2018-02-23 20:48:18.297745+0800 YSC-GCD-demo[20188:5059273] 2---<NSThread: 0x600000079e00>{number = 3, name = (null)} 2018-02-23 20:48:20.301139+0800 YSC-GCD-demo[20188:5059274] 1---<NSThread: 0x600000079d80>{number = 4, name = (null)} 2018-02-23 20:48:20.301139+0800 YSC-GCD-demo[20188:5059273] 2---<NSThread: 0x600000079e00>{number = 3, name = (null)} 2018-02-23 20:48:22.306290+0800 YSC-GCD-demo[20188:5059274] barrier---<NSThread: 0x600000079d80>{number = 4, name = (null)} 2018-02-23 20:48:24.311655+0800 YSC-GCD-demo[20188:5059274] barrier---<NSThread: 0x600000079d80>{number = 4, name = (null)} 2018-02-23 20:48:26.316943+0800 YSC-GCD-demo[20188:5059273] 4---<NSThread: 0x600000079e00>{number = 3, name = (null)} 2018-02-23 20:48:26.316956+0800 YSC-GCD-demo[20188:5059274] 3---<NSThread: 0x600000079d80>{number = 4, name = (null)} 2018-02-23 20:48:28.320660+0800 YSC-GCD-demo[20188:5059273] 4---<NSThread: 0x600000079e00>{number = 3, name = (null)} 2018-02-23 20:48:28.320649+0800 YSC-GCD-demo[20188:5059274] 3---<NSThread: 0x600000079d80>{number = 4, name = (null)} */
- 我們有時(shí)需要異步執(zhí)行兩組操作,而且第一組操作執(zhí)行完之后充活,才能開始執(zhí)行第二組操作蜂莉。這樣我們就需要一個(gè)相當(dāng)于柵欄一樣的一個(gè)方法將兩組異步執(zhí)行的操作組給分割起來(lái),當(dāng)然這里的操作組里可以包含一個(gè)或多個(gè)任務(wù)堪唐。這就需要用到dispatch_barrier_async方法在兩個(gè)操作組間形成柵欄巡语。dispatch_barrier_async函數(shù)會(huì)等待前邊追加到并發(fā)隊(duì)列中的任務(wù)全部執(zhí)行完畢之后,再將指定的任務(wù)追加到該異步隊(duì)列中淮菠。然后在dispatch_barrier_async函數(shù)追加的任務(wù)執(zhí)行完畢之后男公,異步隊(duì)列才恢復(fù)為一般動(dòng)作,接著追加任務(wù)到該異步隊(duì)列并開始執(zhí)行
- 快速迭代方法
- 通常我們會(huì)用 for 循環(huán)遍歷合陵,但是 GCD 給我們提供了快速迭代的函數(shù)dispatch_apply枢赔。dispatch_apply按照指定的次數(shù)將指定的任務(wù)追加到指定的隊(duì)列中,并等待全部隊(duì)列執(zhí)行結(jié)束
- 如果是在串行隊(duì)列中使用 dispatch_apply拥知,那么就和 for 循環(huán)一樣踏拜,按順序同步執(zhí)行〉吞蓿可這樣就體現(xiàn)不出快速迭代的意義了速梗。
- 我們可以利用并發(fā)隊(duì)列進(jìn)行異步執(zhí)行。比如說(shuō)遍歷 0~5 這6個(gè)數(shù)字襟齿,for 循環(huán)的做法是每次取出一個(gè)元素姻锁,逐個(gè)遍歷。dispatch_apply 可以 在多個(gè)線程中同時(shí)(異步)遍歷多個(gè)數(shù)字猜欺。
- 還有一點(diǎn)位隶,無(wú)論是在串行隊(duì)列,還是異步隊(duì)列中开皿,dispatch_apply 都會(huì)等待全部任務(wù)執(zhí)行完畢涧黄,這點(diǎn)就像是同步操作篮昧,也像是隊(duì)列組中的 dispatch_group_wait方法。
/** * 快速迭代方法 dispatch_apply */ - (void)apply { dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); NSLog(@"apply---begin"); dispatch_apply(6, queue, ^(size_t index) { NSLog(@"%zd---%@",index, [NSThread currentThread]); }); NSLog(@"apply---end"); } /* 2018-02-23 22:03:18.475499+0800 YSC-GCD-demo[20470:5176805] apply---begin 2018-02-23 22:03:18.476672+0800 YSC-GCD-demo[20470:5177035] 1---<NSThread: 0x60000027b8c0>{number = 3, name = (null)} 2018-02-23 22:03:18.476693+0800 YSC-GCD-demo[20470:5176805] 0---<NSThread: 0x604000070640>{number = 1, name = main} 2018-02-23 22:03:18.476704+0800 YSC-GCD-demo[20470:5177037] 2---<NSThread: 0x604000276800>{number = 4, name = (null)} 2018-02-23 22:03:18.476735+0800 YSC-GCD-demo[20470:5177036] 3---<NSThread: 0x60000027b800>{number = 5, name = (null)} 2018-02-23 22:03:18.476867+0800 YSC-GCD-demo[20470:5177035] 4---<NSThread: 0x60000027b8c0>{number = 3, name = (null)} 2018-02-23 22:03:18.476867+0800 YSC-GCD-demo[20470:5176805] 5---<NSThread: 0x604000070640>{number = 1, name = main} 2018-02-23 22:03:18.477038+0800 YSC-GCD-demo[20470:5176805] apply---end */
- semaphore 加鎖
/** * 線程安全:使用 semaphore 加鎖 * 初始化火車票數(shù)量笋妥、賣票窗口(線程安全)懊昨、并開始賣票 */ - (void)initTicketStatusSave { NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當(dāng)前線程 NSLog(@"semaphore---begin"); semaphoreLock = dispatch_semaphore_create(1); self.ticketSurplusCount = 50; // queue1 代表北京火車票售賣窗口 dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL); // queue2 代表上海火車票售賣窗口 dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL); __weak typeof(self) weakSelf = self; dispatch_async(queue1, ^{ [weakSelf saleTicketSafe]; }); dispatch_async(queue2, ^{ [weakSelf saleTicketSafe]; }); } /** * 售賣火車票(線程安全) */ - (void)saleTicketSafe { while (1) { // 相當(dāng)于加鎖 dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER); if (self.ticketSurplusCount > 0) { //如果還有票春宣,繼續(xù)售賣 self.ticketSurplusCount--; NSLog(@"%@", [NSString stringWithFormat:@"剩余票數(shù):%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]); [NSThread sleepForTimeInterval:0.2]; } else { //如果已賣完疚颊,關(guān)閉售票窗口 NSLog(@"所有火車票均已售完"); // 相當(dāng)于解鎖 dispatch_semaphore_signal(semaphoreLock); break; } // 相當(dāng)于解鎖 dispatch_semaphore_signal(semaphoreLock); } }
-
總結(jié)
- 1.串行隊(duì)列同步執(zhí)行:不開線程,在原來(lái)線程里面一個(gè)一個(gè)順序執(zhí)行信认。
- 2.串行隊(duì)列異步執(zhí)行:開一條線程,在新來(lái)線程里面一個(gè)一個(gè)順序執(zhí)行均抽。
- 3.并行隊(duì)列異步執(zhí)行:開多個(gè)線程嫁赏,并發(fā)執(zhí)行
- 4.并行隊(duì)列同步執(zhí)行:不開線程,在原來(lái)線程里面一個(gè)一個(gè)順序執(zhí)行油挥。
- 1.開不開線程潦蝇,由執(zhí)行任務(wù)方法決定,同步不開線程深寥,異步開線程攘乒。
-
2.開多少線程,由隊(duì)列決定惋鹅,串行最多開一個(gè)線程则酝,并發(fā)可以開多個(gè)線程,具體開多少個(gè)闰集,GCD底層決定沽讹,開發(fā)者不能控制。
3.NSOperation
- 優(yōu)點(diǎn):自帶線程周期管理武鲁,操作上可更注重自己邏輯爽雄。
- 缺點(diǎn):面向?qū)ο蟮某橄箢悾荒軐?shí)現(xiàn)它或者使用它定義好的兩個(gè)子類:NSInvocationOperation 和 NSBlockOperation沐鼠。
- NSOperation挚瘟、NSOperationQueue 簡(jiǎn)介
- 是蘋果提供給我們的一套多線程解決方案。實(shí)際上 NSOperation饲梭、NSOperationQueue 是基于 GCD 更高一層的封裝乘盖,完全面向?qū)ο蟆5潜?GCD 更簡(jiǎn)單易用排拷、代碼可讀性也更高侧漓。
- 為什么要使用 NSOperation、NSOperationQueue监氢?
- 可添加完成的代碼塊布蔗,在操作完成后執(zhí)行藤违。
- 添加操作之間的依賴關(guān)系,方便的控制執(zhí)行順序纵揍。
- 設(shè)定操作執(zhí)行的優(yōu)先級(jí)顿乒。
- 可以很方便的取消一個(gè)操作的執(zhí)行。
- 使用 KVO 觀察對(duì)操作執(zhí)行狀態(tài)的更改:isExecuteing泽谨、isFinished璧榄、isCancelled。
- 操作(Operation)
- 執(zhí)行操作的意思吧雹,換句話說(shuō)就是你在線程中執(zhí)行的那段代碼骨杂。
- 在 GCD 中是放在 block 中的。在 NSOperation 中雄卷,我們使用 NSOperation 子類 NSInvocationOperation搓蚪、NSBlockOperation,或者自定義子類來(lái)封裝操作丁鹉。
- 操作隊(duì)列(Operation Queues)
- 這里的隊(duì)列指操作隊(duì)列妒潭,即用來(lái)存放操作的隊(duì)列。不同于 GCD 中的調(diào)度隊(duì)列 FIFO(先進(jìn)先出)的原則揣钦。NSOperationQueue 對(duì)于添加到隊(duì)列中的操作雳灾,首先進(jìn)入準(zhǔn)備就緒的狀態(tài)(就緒狀態(tài)取決于操作之間的依賴關(guān)系),然后進(jìn)入就緒狀態(tài)的操作的開始執(zhí)行順序(非結(jié)束執(zhí)行順序)由操作之間相對(duì)的優(yōu)先級(jí)決定(優(yōu)先級(jí)是操作對(duì)象自身的屬性)冯凹。
- 操作隊(duì)列通過(guò)設(shè)置最大并發(fā)操作數(shù)(maxConcurrentOperationCount)來(lái)控制并發(fā)谎亩、串行。
- NSOperationQueue 為我們提供了兩種不同類型的隊(duì)列:主隊(duì)列和自定義隊(duì)列谈竿。主隊(duì)列運(yùn)行在主線程之上团驱,而自定義隊(duì)列在后臺(tái)執(zhí)行。
- 使用步驟
- NSOperation 需要配合 NSOperationQueue 來(lái)實(shí)現(xiàn)多線程空凸。因?yàn)槟J(rèn)情況下嚎花,NSOperation 單獨(dú)使用時(shí)系統(tǒng)同步執(zhí)行操作,配合 NSOperationQueue 我們能更好的實(shí)現(xiàn)異步執(zhí)行呀洲。
- 1.創(chuàng)建操作:先將需要執(zhí)行的操作封裝到一個(gè) NSOperation 對(duì)象中紊选。
- 2.創(chuàng)建隊(duì)列:創(chuàng)建 NSOperationQueue 對(duì)象。
- 3.將操作加入到隊(duì)列中:將 NSOperation 對(duì)象添加到 NSOperationQueue 對(duì)象中道逗。
- 基本使用
- 使用子類 NSInvocationOperation
/** * 使用子類 NSInvocationOperation */ - (void)useInvocationOperation { // 1.創(chuàng)建 NSInvocationOperation 對(duì)象 NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil]; // 2.調(diào)用 start 方法開始執(zhí)行操作 [op start]; } /** * 任務(wù)1 */ - (void)task1 { for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"1---%@", [NSThread currentThread]); // 打印當(dāng)前線程 } }
- 使用子類 NSBlockOperation
/** * 使用子類 NSBlockOperation */ - (void)useBlockOperation { // 1.創(chuàng)建 NSBlockOperation 對(duì)象 NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"1---%@", [NSThread currentThread]); // 打印當(dāng)前線程 } }]; //追加 [op addExecutionBlock:^{ //code }]; // 2.調(diào)用 start 方法開始執(zhí)行操作 [op start]; }
- 自定義繼承自 NSOperation 的子類兵罢,通過(guò)實(shí)現(xiàn)內(nèi)部相應(yīng)的方法來(lái)封裝操作。
- 使用子類 NSInvocationOperation
- 隊(duì)列
- 主隊(duì)列
// 主隊(duì)列獲取方法 NSOperationQueue * queue = [NSOperationQueue mainQueue];
- 非主隊(duì)列(默認(rèn)是并發(fā)隊(duì)列)
// 自定義隊(duì)列創(chuàng)建方法 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //獲取當(dāng)前隊(duì)列 NSOperationQueue * queue = [NSOperationQueue currentQueue];
- 主隊(duì)列
- 將操作加入到隊(duì)列中
- NSInvocationOperation
NSInvocationOperation // 創(chuàng)建隊(duì)列 NSOperationQueue * queue = [[NSOperationQueue alloc] init]; // 封裝操作 NSInvocationOperation * op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test) object:nil]; // 把操作添加到隊(duì)列中 [queue addOperation:op];
- NSBlockOperation
// 創(chuàng)建隊(duì)列 NSOperationQueue * queue = [[NSOperationQueue alloc] init]; // 封裝操作 NSBlockOperation * bl = [NSBlockOperation blockOperationWithBlock:^{ //code }]; [bl addExecutionBlock:^{ //code,追加操作滓窍,這一步卖词,可加可不加。 }] // 把操作添加到隊(duì)列中 [queue addOperation:bl]; 簡(jiǎn)寫方式 [queue addOperationWithBlock:^{ //code }];
- NSInvocationOperation
- 串行和并行
- 這里有個(gè)關(guān)鍵參數(shù)maxConcurrentOperationCount,叫做最大并發(fā)數(shù)此蜈。
- maxConcurrentOperationCount默認(rèn)情況下為-1即横,表示不進(jìn)行限制,默認(rèn)為并發(fā)執(zhí)行裆赵。
- 當(dāng)maxConcurrentOperationCount為1時(shí)东囚,隊(duì)列為串行隊(duì)列。只能串行執(zhí)行战授。
- 當(dāng)maxConcurrentOperationCount大于1時(shí)页藻,隊(duì)列為并發(fā)隊(duì)列。操作并發(fā)執(zhí)行植兰,當(dāng)然這個(gè)值不應(yīng)超過(guò)系統(tǒng)限制份帐,即使自己設(shè)置一個(gè)很大的值,系統(tǒng)也會(huì)自動(dòng)調(diào)整楣导。
/** * 設(shè)置 MaxConcurrentOperationCount(最大并發(fā)操作數(shù)) */ - (void)setMaxConcurrentOperationCount { // 1.創(chuàng)建隊(duì)列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 2.設(shè)置最大并發(fā)操作數(shù) queue.maxConcurrentOperationCount = 1; // 串行隊(duì)列 // queue.maxConcurrentOperationCount = 2; // 并發(fā)隊(duì)列 // queue.maxConcurrentOperationCount = 8; // 并發(fā)隊(duì)列 // 3.添加操作 [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"1---%@", [NSThread currentThread]); // 打印當(dāng)前線程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"2---%@", [NSThread currentThread]); // 打印當(dāng)前線程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"3---%@", [NSThread currentThread]); // 打印當(dāng)前線程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"4---%@", [NSThread currentThread]); // 打印當(dāng)前線程 } }]; }
- 這里有個(gè)關(guān)鍵參數(shù)maxConcurrentOperationCount,叫做最大并發(fā)數(shù)此蜈。
- 線程依賴
- NSOperation和NSOperationQueue最吸引人的地方是它能添加操作之間的依賴關(guān)系弥鹦。比如說(shuō)有A、B兩個(gè)操作爷辙,其中A執(zhí)行完操作,B才能執(zhí)行操作朦促,那么就需要讓B依賴于A膝晾。
- -(void)addDependency:(NSOperation *)op; 添加依賴,使當(dāng)前操作依賴于操作 op 的完成务冕。
- -(void)removeDependency:(NSOperation *)op; 移除依賴血当,取消當(dāng)前操作對(duì)操作 op 的依賴。
- @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在當(dāng)前操作開始執(zhí)行之前完成執(zhí)行的所有操作對(duì)象數(shù)組禀忆。
/** * 操作依賴 * 使用方法:addDependency: */ - (void)addDependency { // 1.創(chuàng)建隊(duì)列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 2.創(chuàng)建操作 NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"1---%@", [NSThread currentThread]); // 打印當(dāng)前線程 } }]; NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模擬耗時(shí)操作 NSLog(@"2---%@", [NSThread currentThread]); // 打印當(dāng)前線程 } }]; // 3.添加依賴 [op2 addDependency:op1]; // 讓op2 依賴于 op1臊旭,則先執(zhí)行op1,在執(zhí)行op2 // 4.添加操作到隊(duì)列中 [queue addOperation:op1]; [queue addOperation:op2]; }
- 線程優(yōu)先級(jí)
- NSOperation 提供了queuePriority(優(yōu)先級(jí))屬性箩退,queuePriority屬性適用于同一操作隊(duì)列中的操作离熏,不適用于不同操作隊(duì)列中的操作。默認(rèn)情況下戴涝,所有新創(chuàng)建的操作對(duì)象優(yōu)先級(jí)都是NSOperationQueuePriorityNormal滋戳。但是我們可以通過(guò)setQueuePriority:方法來(lái)改變當(dāng)前操作在同一隊(duì)列中的執(zhí)行優(yōu)先級(jí)。
// 優(yōu)先級(jí)的取值 typedef NS_ENUM(NSInteger, NSOperationQueuePriority) { NSOperationQueuePriorityVeryLow = -8L, NSOperationQueuePriorityLow = -4L, NSOperationQueuePriorityNormal = 0, NSOperationQueuePriorityHigh = 4, NSOperationQueuePriorityVeryHigh = 8 };
- 什么樣的操作才是進(jìn)入就緒狀態(tài)的操作呢啥刻?
- 當(dāng)一個(gè)操作的所有依賴都已經(jīng)完成時(shí)奸鸯,操作對(duì)象通常會(huì)進(jìn)入準(zhǔn)備就緒狀態(tài),等待執(zhí)行可帽。
- 舉個(gè)例子娄涩,現(xiàn)在有4個(gè)優(yōu)先級(jí)都是 NSOperationQueuePriorityNormal(默認(rèn)級(jí)別)的操作:op1,op2映跟,op3蓄拣,op4扬虚。其中 op3 依賴于 op2,op2 依賴于 op1弯蚜,即 op3 -> op2 -> op1】字幔現(xiàn)在將這4個(gè)操作添加到隊(duì)列中并發(fā)執(zhí)行。
- 因?yàn)?op1 和 op4 都沒(méi)有需要依賴的操作碎捺,所以在 op1路鹰,op4 執(zhí)行之前,就是處于準(zhǔn)備就緒狀態(tài)的操作收厨。
- 而 op3 和 op2 都有依賴的操作(op3 依賴于 op2晋柱,op2 依賴于 op1),所以 op3 和 op2 都不是準(zhǔn)備就緒狀態(tài)下的操作诵叁。
- 理解了進(jìn)入就緒狀態(tài)的操作雁竞,那么我們就理解了queuePriority 屬性的作用對(duì)象
- queuePriority 屬性決定了進(jìn)入準(zhǔn)備就緒狀態(tài)下的操作之間的開始執(zhí)行順序。并且拧额,優(yōu)先級(jí)不能取代依賴關(guān)系碑诉。
- 如果一個(gè)隊(duì)列中既包含高優(yōu)先級(jí)操作,又包含低優(yōu)先級(jí)操作侥锦,并且兩個(gè)操作都已經(jīng)準(zhǔn)備就緒进栽,那么隊(duì)列先執(zhí)行高優(yōu)先級(jí)操作。比如上例中恭垦,如果 op1 和 op4 是不同優(yōu)先級(jí)的操作快毛,那么就會(huì)先執(zhí)行優(yōu)先級(jí)高的操作。
- 如果番挺,一個(gè)隊(duì)列中既包含了準(zhǔn)備就緒狀態(tài)的操作唠帝,又包含了未準(zhǔn)備就緒的操作,未準(zhǔn)備就緒的操作優(yōu)先級(jí)比準(zhǔn)備就緒的操作優(yōu)先級(jí)高玄柏。那么襟衰,雖然準(zhǔn)備就緒的操作優(yōu)先級(jí)低,也會(huì)優(yōu)先執(zhí)行粪摘。優(yōu)先級(jí)不能取代依賴關(guān)系右蒲。如果要控制操作間的啟動(dòng)順序,則必須使用依賴關(guān)系赶熟。
- NSOperation 提供了queuePriority(優(yōu)先級(jí))屬性箩退,queuePriority屬性適用于同一操作隊(duì)列中的操作离熏,不適用于不同操作隊(duì)列中的操作。默認(rèn)情況下戴涝,所有新創(chuàng)建的操作對(duì)象優(yōu)先級(jí)都是NSOperationQueuePriorityNormal滋戳。但是我們可以通過(guò)setQueuePriority:方法來(lái)改變當(dāng)前操作在同一隊(duì)列中的執(zhí)行優(yōu)先級(jí)。
- 線程同步
/** * 線程安全:使用 NSLock 加鎖 * 初始化火車票數(shù)量瑰妄、賣票窗口(線程安全)、并開始賣票 */ - (void)initTicketStatusSave { NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印當(dāng)前線程 self.ticketSurplusCount = 50; self.lock = [[NSLock alloc] init]; // 初始化 NSLock 對(duì)象 // 1.創(chuàng)建 queue1,queue1 代表北京火車票售賣窗口 NSOperationQueue *queue1 = [[NSOperationQueue alloc] init]; queue1.maxConcurrentOperationCount = 1; // 2.創(chuàng)建 queue2,queue2 代表上河匙火車票售賣窗口 NSOperationQueue *queue2 = [[NSOperationQueue alloc] init]; queue2.maxConcurrentOperationCount = 1; // 3.創(chuàng)建賣票操作 op1 NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{ [self saleTicketSafe]; }]; // 4.創(chuàng)建賣票操作 op2 NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{ [self saleTicketSafe]; }]; // 5.添加操作间坐,開始賣票 [queue1 addOperation:op1]; [queue2 addOperation:op2]; } /** * 售賣火車票(線程安全) */ - (void)saleTicketSafe { while (1) { // 加鎖 [self.lock lock]; if (self.ticketSurplusCount > 0) { //如果還有票,繼續(xù)售賣 self.ticketSurplusCount--; NSLog(@"%@", [NSString stringWithFormat:@"剩余票數(shù):%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]); [NSThread sleepForTimeInterval:0.2]; } // 解鎖 [self.lock unlock]; if (self.ticketSurplusCount <= 0) { NSLog(@"所有火車票均已售完"); break; } } }
- NSOperation 常用屬性和方法
-
取消操作方法
- -(void)cancel; 可取消操作,實(shí)質(zhì)是標(biāo)記 isCancelled 狀態(tài)竹宋。
-
判斷操作狀態(tài)方法
- -(BOOL)isFinished; 判斷操作是否已經(jīng)結(jié)束劳澄。
- -(BOOL)isCancelled; 判斷操作是否已經(jīng)標(biāo)記為取消。
- -(BOOL)isExecuting; 判斷操作是否正在在運(yùn)行蜈七。
- -(BOOL)isReady; 判斷操作是否處于準(zhǔn)備就緒狀態(tài)秒拔,這個(gè)值和操作的依賴關(guān)系相關(guān)。
-
操作同步
- -(void)waitUntilFinished; 阻塞當(dāng)前線程飒硅,直到該操作結(jié)束砂缩。可用于線程執(zhí)行順序的同步三娩。
- -(void)setCompletionBlock:(void (^)(void))block; completionBlock 會(huì)在當(dāng)前操作執(zhí)行完畢時(shí)執(zhí)行 completionBlock庵芭。
- -(void)addDependency:(NSOperation *)op; 添加依賴,使當(dāng)前操作依賴于操作 op 的完成雀监。
- -(void)removeDependency:(NSOperation *)op; 移除依賴双吆,取消當(dāng)前操作對(duì)操作 op 的依賴。
- @property (readonly, copy) NSArray<NSOperation *> *dependencies; 在當(dāng)前操作開始執(zhí)行之前完成執(zhí)行的所有操作對(duì)象數(shù)組会前。
-
- NSOperationQueue 常用屬性和方法
- 取消/暫停/恢復(fù)操作
- -(void)cancelAllOperations; 可以取消隊(duì)列的所有操作好乐。
- -(BOOL)isSuspended; 判斷隊(duì)列是否處于暫停狀態(tài)。 YES 為暫停狀態(tài)瓦宜,NO 為恢復(fù)狀態(tài)曹宴。
- -(void)setSuspended:(BOOL)b; 可設(shè)置操作的暫停和恢復(fù),YES 代表暫停隊(duì)列歉提,NO 代表恢復(fù)隊(duì)列。
- 操作同步
- -(void)waitUntilAllOperationsAreFinished; 阻塞當(dāng)前線程区转,直到隊(duì)列中的操作全部執(zhí)行完畢苔巨。
- 添加/獲取操作`
- -(void)addOperationWithBlock:(void (^)(void))block; 向隊(duì)列中添加一個(gè) NSBlockOperation 類型操作對(duì)象。
- -(void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait; 向隊(duì)列中添加操作數(shù)組废离,wait 標(biāo)志是否阻塞當(dāng)前線程直到所有操作結(jié)束
- -(NSArray *)operations; 當(dāng)前在隊(duì)列中的操作數(shù)組(某個(gè)操作執(zhí)行結(jié)束后會(huì)自動(dòng)從這個(gè)數(shù)組清除)侄泽。
- -(NSUInteger)operationCount; 當(dāng)前隊(duì)列中的操作數(shù)庸毫。
- 獲取隊(duì)列
- +(id)currentQueue; 獲取當(dāng)前隊(duì)列沟于,如果當(dāng)前線程不是在 NSOperationQueue 上運(yùn)行則返回 nil梳码。
- +(id)mainQueue; 獲取主隊(duì)列予颤。
- 取消/暫停/恢復(fù)操作
- 注意:
- 1.這里的暫停和取消(包括操作的取消和隊(duì)列的取消)并不代表可以將當(dāng)前的操作立即取消膘流,而是當(dāng)當(dāng)前的操作執(zhí)行完畢之后不再執(zhí)行新的操作冰悠。
- 2.暫停和取消的區(qū)別就在于:暫停操作之后還可以恢復(fù)操作鸟缕,繼續(xù)向下執(zhí)行鸭限;而取消操作之后俯画,所有的操作就清空了析桥,無(wú)法再接著執(zhí)行剩下的操作。