iOS 面試 -- 多線程

1、進(jìn)程

? ? ? ? 1)進(jìn)程是一個具有一定獨立功能的程序關(guān)于某次數(shù)據(jù)集合的一次運行活動,它是操作系統(tǒng)分配資源的基本單元。

? ? ? ? 2)進(jìn)程是指在系統(tǒng)中正在運行的一個應(yīng)用程序,就是一段程序的執(zhí)行過程,可以理解為手機上的一個app。

? ? ? ? 3)每個進(jìn)程之間是獨立的,每個進(jìn)程均運行在某專用且受保護(hù)的內(nèi)存空間內(nèi),擁有獨立運行所需的全部資源。

2、線程

? ? ? ? 1)程序執(zhí)行流的最小單元,線程是進(jìn)程中的一個實體。

? ? ? ? 2)一個進(jìn)程要想執(zhí)行任務(wù),必須至少有一條線程,應(yīng)用程序啟動的時候,系統(tǒng)會默認(rèn)開啟一條進(jìn)程,也就是主線程。

3、進(jìn)程和線程的關(guān)系

? ? ? ? 1)線程是進(jìn)程的執(zhí)行單元,進(jìn)程的所有任務(wù)都在線程中執(zhí)行。

? ? ? ? 2)線程是CPU分配資源和調(diào)度的最小單位

? ? ? ? 3)一個程序可以對應(yīng)多個進(jìn)程(多進(jìn)程),一個進(jìn)程中可有多個線程,但至少要有一條線程。

? ? ? ? 4)同一個進(jìn)程內(nèi)的線程共享進(jìn)程資源。

4、多進(jìn)程

? ? ? ? 打開Mac的活動監(jiān)視器,可以卡到很多個進(jìn)程同時運行。

? ? ? ? 1)進(jìn)程是程序在計算機上的一次執(zhí)行活動。當(dāng)你運行一個程序,你就啟動了一個進(jìn)程。顯然,程序是死的(靜態(tài)的),進(jìn)程是活的(動態(tài)的)。

? ? ? ? 2)進(jìn)程可以分為系統(tǒng)進(jìn)程和用戶進(jìn)程。凡是用于完成操作系統(tǒng)的各種功能的進(jìn)程就是系統(tǒng)進(jìn)程,它們是處于運行狀態(tài)下的操作系統(tǒng)本身。所有由用戶啟動的進(jìn)程都是用戶進(jìn)程,進(jìn)程是操作系統(tǒng)進(jìn)行資源分配的單位。

? ? ? ? 3)進(jìn)程又被細(xì)化為線程,也就是一個進(jìn)程下有多個能獨立運行的更小的單位。在同一個時間里,同一臺計算機系統(tǒng)中如果允許兩個或兩個以上的進(jìn)程處于運行狀態(tài),這便是多進(jìn)程。

5、多線程

? ? ? ? 1)同一時間,CPU只能處理1條線程,只有1條線程在執(zhí)行。多線程并發(fā)執(zhí)行,其實就是CPU快速地在多條線程之間調(diào)度(切換)。如果CPU調(diào)度線程的實踐足夠快,就造成了多線程并發(fā)執(zhí)行的假象。

? ? ? ? 2)如果線程非常非常多,CPU會在N多線程之間調(diào)度,消耗大量CPU資源,每條線程被調(diào)度執(zhí)行的頻次會降低(線程的執(zhí)行效率降低)。

? ? ? ? 3)多線程的優(yōu)點:

? ? ? ? ? ? ? ? 能適當(dāng)提高程序的執(zhí)行效率。

? ? ? ? ? ? ? ? 能適當(dāng)提高資源利用率(CPU、內(nèi)存利用率)。

? ? ? ? 4)多線程的缺點:

? ? ? ? ? ? ? ? 開啟線程需要占用一定的內(nèi)存空間(默認(rèn)情況下,主線程占用1M,子線程占用512KB),如果開啟大量的線程,會占用大量的內(nèi)存空間,降低程序的性能。線程越多,CPU在調(diào)度線程上的開銷就越大。

? ? ? ? ? ? ? ? 程序設(shè)計更加復(fù)雜:比如線程之間的通信、多線程的數(shù)據(jù)共享等。

6、任務(wù)

? ? ????就是執(zhí)行操作的意思,也就是在線程中執(zhí)行的那段代碼。在GCD中是放在block中的。執(zhí)行任務(wù)有兩種方式:同步執(zhí)行(sync)和異步執(zhí)行(async)。

? ? ? ? 1)同步(Sync):同步添加任務(wù)到指定的隊列中,在添加的任務(wù)執(zhí)行結(jié)束之前,會一直等待,直到隊列里面的任務(wù)完成之后再繼續(xù)執(zhí)行,即會阻塞線程。只能在當(dāng)前線程中執(zhí)行任務(wù)(是當(dāng)前線程,不一定是主線程),不具備開啟新線程的能力。 ? ?

? ? ? ? 2)異步(Async):線程會立即返回,無需等待就會繼續(xù)執(zhí)行下面的任務(wù),不阻塞當(dāng)前線程??梢栽谛碌木€程中執(zhí)行任務(wù),具備開啟新線程的negligence(并不一定開啟新線程)。如果不是添加到主隊列上,異步會在子線程中執(zhí)行任務(wù)。

7、隊列

? ? ? ? 隊列(Dispatch Queue):這里的隊列指執(zhí)行任務(wù)的等待隊列,即用來存放任務(wù)的隊列。隊列是一種特殊的線性表,采用FIFO(先進(jìn)先出)的原則,即新任務(wù)總是被插入到隊列的末尾,而讀取任務(wù)的時候總是從隊列的頭部開始讀取。每讀取一個任務(wù),則從隊列中釋放一個任務(wù)。

? ? ? ? 在GCD中有兩個隊列:串行隊列和并發(fā)隊列。兩者都符合FIFO(先進(jìn)先出)的原則。

? ? ? ? 兩者的主要區(qū)別是:執(zhí)行順序不同,以及開啟線程數(shù)不同。

? ? ? ? 1)串行隊列:(Serial Despatch Queue):

? ? ? ? ? ? ? ? 同一時間,隊列中只能執(zhí)行一個任務(wù),只有當(dāng)前的任務(wù)執(zhí)行完成之后,才能執(zhí)行下一個任務(wù)。(只開啟一個線程,一個任務(wù)執(zhí)行完畢后,再執(zhí)行下一個任務(wù))。主隊列是主線程上的一個串行隊列,是系統(tǒng)自動為我們創(chuàng)建的。

? ? ? ? 2)并發(fā)隊列(Concurrent Dispatch Queue):

? ? ? ? ? ? ? ? 同時允許多個任務(wù)并發(fā)執(zhí)行。(可以開啟多個線程,并且同時執(zhí)行任務(wù))。并發(fā)隊列的并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效


IOS多線程:NSThread、NSOperationQueue、GCD

1)NSThread:輕量級別的多線程技術(shù)

是我們自己手動開辟的子線程,如果使用的是初始化方式就需要我們自己啟動,如果使用的是構(gòu)造方式它就會自動啟動。只需要我們手動開辟的線程,都需要我們自己管理該線程,不只是啟動,還有該線程使用完畢后的資源回收。

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object@"手動創(chuàng)建"];

// 當(dāng)使用初始化方法出來的線程需要start啟動

[thread start];

// 可以為開辟的線程起名字

thread.name = @"自己的線程";

// 調(diào)整Thread的權(quán)限,線程權(quán)限的范圍是0~1。越大權(quán)限越高,先執(zhí)行的概率就會越高,由于概率高,所以并不能很準(zhǔn)確的實現(xiàn)我們想要的執(zhí)行順序,默認(rèn)值時0.5

thread.threadPriority = 1;

// 取消當(dāng)前已經(jīng)啟動的線程

[thread cancel];

// 通過遍歷構(gòu)造器開辟線程

[NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"構(gòu)造器方法"];

performSelector...只要是NSObject的子類或者對象都可以通過調(diào)用此方法進(jìn)入子線程和主線程,其實這些方法所開辟的子線程也是NSThread的另一種體現(xiàn)方式。

在編譯階段并不會去檢查方法是否有效存在,如果不存在,只會給出警告。

// 在當(dāng)前線程,延遲1s執(zhí)行。響應(yīng)了OC語言的動態(tài)性:延遲到運行時才綁定方法

[self performSelector:@selector(test) withObject:nil afterDelay:1];

// 回到主線程。waitUntilDone:是否將該回調(diào)方法執(zhí)行完再執(zhí)行后面的代碼。如果是YES:就必須等回調(diào)方法執(zhí)行完成之后才能執(zhí)行后面的代碼,會阻塞當(dāng)前的線程;如果是NO:就是不等回調(diào)方法結(jié)束,不會阻塞當(dāng)前線程

[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];

// 開辟子線程

[self performSelectorInBackground:@selector(test) withObject:nil];

// 在指定線程執(zhí)行

[self performSelector:@selector(test) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES];

需要注意的是:如果是帶有afterDelay的延時函數(shù),會在內(nèi)部創(chuàng)建一個Timer,然后添加到當(dāng)前線程的RunLoop中,也就是如果當(dāng)前線程沒有開啟runloop,該方法會失效。在子線程中,需要啟動runloop(注意調(diào)用順序)。

[self performSelector:@selector(test) withObject:nil afterDelay:1];

[[NSRunLoop currentRunLoop] run];

而performSelector: withObject: 只是一個單純的消息發(fā)送,和時間沒有一點關(guān)系。所以不需要添加到子線程的RunLoop中也能執(zhí)行。

2)GCD和NSOperationQueue

GCD是面向底層的C語言的API,NSOperationQueue是用GCD構(gòu)建封裝的,是GCD的高級抽象。

1、GCD執(zhí)行效率更高,而卻由于隊列中執(zhí)行的是block構(gòu)成的任務(wù),這是一個輕量級的數(shù)據(jù)結(jié)構(gòu),寫起來更方便。

2、GCD只支持FIFI的隊列,而NSOperationQueue可以通過設(shè)置最大并發(fā)數(shù),設(shè)置優(yōu)先級,添加依賴關(guān)系等調(diào)整執(zhí)行順序。

3、NSOperationQueue甚至可以跨隊列設(shè)置依賴關(guān)系,但是GCD只能通過設(shè)置串行隊列,或者在隊列內(nèi)添加barrier(dispatch_barrier_async)任務(wù),才能控制執(zhí)行順序,較為復(fù)雜。

4、NSOperationQueue是面向?qū)ο?,所以支持KVO,可以監(jiān)聽operation是否正在執(zhí)行(isExecuted)、是否結(jié)束(isFinished)、是否取消(isCancelled)。

? ? ? ? 1)實際項目開發(fā)中,很多時候只會用到異步操作,不會有特別復(fù)雜的線程關(guān)系管理,所以蘋果推崇的且優(yōu)化完善、運行快速的GCD是首選。

? ? ? ? 2)如果考慮異步操作之間的事務(wù)性,順序性,依賴關(guān)系,比如多線程并發(fā)下載,GCD需要自己寫更多的代碼來實現(xiàn),而NSOperationQueue已經(jīng)內(nèi)建了這些支持

? ? ? ? 3)不論是GCD還是NSOperationQueue,我么接觸的都是任務(wù)和隊列,都沒有直接接觸到線程,事實上線程管理也的確不需要我么操心,系統(tǒng)對于線程的創(chuàng)建,調(diào)度管理和釋放都做得很好。而NSThread需要我們自己去管理線程的生命周期,還要考慮線程頭部、加鎖問題,造成一些性能上的開銷。

3)死鎖

死鎖就是隊列引起的循環(huán)等待

(1)一個比較常見的死鎖:主隊列同步? ?

?- (void)viewDidLoad {

????????[super viewDidLoad];

? ? ? ? dispatch_sync(dispatch_get_main_queue(), ^{ ?

? ? ? ? ? ? ? ? NSLog(@"1");

????????});

? ? ? ? NSLog(@"2");

}

在主線程中運用主隊列同步,也就是把任務(wù)放到了主線程的隊列中。

同步對于任務(wù)是立刻執(zhí)行的,那么當(dāng)吧任務(wù)放到主隊列時,它就會立馬執(zhí)行,只有執(zhí)行完這個任務(wù),viewDidLoad才會繼續(xù)向下執(zhí)行。

而viewDidLoad和任務(wù)都是在主隊列上的,由于隊列的FIFO(先進(jìn)先出)原則,任務(wù)又需等待viewDidLoad執(zhí)行完畢后才能繼續(xù)執(zhí)行,viewDidLoad和這個任務(wù)就形成了相互循環(huán)等待,就造成了死鎖。

想避免這種死鎖,可以將同步改成異步dispatch_async或者將dispatch_get_main_queue換成其他串行或并行隊列,都可以解決

(2)下面的代碼也會造成死鎖:

dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{

? ? ? ? dispatch_sync(serialQueue, ^{

? ? ? ? ? ? ? ? NSLog(@"111");

????????});

? ? ????NSLog(@"222");

});

外面的函數(shù)無論是同步還是異步都會造成死鎖。

這是因為里面的任務(wù)和外面的任務(wù)都是在同一個serialQueue隊列內(nèi),又是同步,這就和上面主隊列同步的例子一樣造成了死鎖。

解決方案和上面的一樣,將里面的同步改成異步dispatch_async,或者將serialQueue換成其他串行或者并行隊列,都可以解決。

dispatch_queue_t serialQueue1 = dispatch_queue_create("test", ?DISPATCH_QUEUE_SERIAL);

dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue1, ^{

? ? ? ? dispatch_sync(serialQueue2, ^{

? ? ? ? ? ? ? ? NSLog(@"111");

????????}) ;

? ? ? ? NSLog(@"222");

});

這樣是不會死鎖的,并且serialQueue1和serialQueue2是在同一個線程中。

4)GCD —— 隊列

GCD有三種隊列類型:

1、main queue:通過dispatch_get_main_queue()獲得,這是一個與主線程相關(guān)的串行隊列。

2、global queue:全局隊列,是并發(fā)隊列,由整個進(jìn)程共享。存放著高、中、低三種優(yōu)先級的全局隊列。調(diào)用dispatch_get_global_queue并傳入優(yōu)先級來訪問隊列。

3、自定義隊列:通過函數(shù)dispatch_queue_create創(chuàng)建的隊列。

(1)串行隊列先異步后同步

dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

NSLog(@"1");

dispatch_async(serialQueue, ^{

? ? ? ? NSLog(@"2");

});

NSLog(@"3");

dispatch_sync(serialQueue, ^{

? ? ? ? NSLog(@"4");

});

NSLog(@"5");

打印順序:13245。

原因:首先先打印1;接下來將任務(wù)2添加到串行隊列上,由于任務(wù)2是異步,不會阻塞線程,繼續(xù)向下執(zhí)行,打印3;然后是任務(wù)4,將任務(wù)4添加到串行隊列上,因為任務(wù)4和任務(wù)2在同一個串行隊列,根據(jù)隊列FIFI原則,任務(wù)4必須等待任務(wù)2執(zhí)行完后才能執(zhí)行,有因為任務(wù)4是同步任務(wù),會阻塞線程,只有執(zhí)行完任務(wù)4才能繼續(xù)向下執(zhí)行打印5。

這里的任務(wù)4在主線程中執(zhí)行,而任務(wù)2在子線程中執(zhí)行。如果任務(wù)4是添加到另一個串行隊列或者并行隊列,則任務(wù)2和任務(wù)4無序執(zhí)行。

(2)performSelector

dispatch_async(dispatch_get_global_queue(0 ,0 ) ^{

? ? ? ? ? ? [self performSelector:@selector(test) withObject:nil afterDelay:0];

});

這里的test方法不會去執(zhí)行,原因是

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

這個方法要創(chuàng)建任務(wù)提交到runloop上,而gcd框架繼承的線程默認(rèn)沒有開啟對應(yīng)runloop的,所以這個方法會失效。

而如果將dispatch_get_global_queue改成主隊列,由于主隊列所在的主線程是默認(rèn)開啟了runloop的,就會去執(zhí)行。

將dispatch_async改成同步,因為同步是在當(dāng)前線程執(zhí)行,那么如果當(dāng)前線程是主線程,test方法也會去執(zhí)行。

在performSelector 下面添加[[NSRunLoop currentRunLoop] run]; 開啟當(dāng)前線程,test也會去執(zhí)行

(3)dispatch_barrier_async

1> 怎么用GCD實現(xiàn)多讀單寫?

多度單寫的意思就是:可以多個讀者同時讀取數(shù)據(jù),而在讀的時候,不能取寫入數(shù)據(jù)。并且,在寫的過程中,不能有其他寫者去寫,即讀者之間是并發(fā)的,寫者與讀者或者其他者時互斥的。

?處理

這里的寫處理可以用 dispatch_barrier_sync(柵欄函數(shù))去實現(xiàn)

2> dispatch_barrier_async

dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);

for (NSInteger i = 0; i < 10;?I++) {

? ? ? ? dispatch_sync(concurrentQueue, ^{

? ? ? ? ? ? ? ? NSLog(@"%zd", i);

????????});

}

dispatch_barrier_sync(concurrentQueue, ^{

?????????NSLog(@"barrier");

});

for (NSInteger i = 10; i < 20; i++) {

? ? ? ? dispatch_sync(concurrentQueue, ^{

? ? ? ? ? ? ? ? NSLog(@"%zd", i);

? ? ? ??});

}

這里的dispatch_barrier_sync上的隊列要和需要阻塞的任務(wù)在同一隊列上,否則是無效的。

從打印上看,任務(wù)0-9和熱舞10-19因為是異步并發(fā)的原因,彼此是無序的。而由于柵欄函數(shù)的存在,導(dǎo)致順序必然是先執(zhí)行任務(wù)0-9,再執(zhí)行柵欄函數(shù),再執(zhí)行任務(wù)10-19.

dispatch_barrier_sync和dispatch_barrier_async的區(qū)別在于會不會阻塞當(dāng)前的線程。

比如,上述代碼如果在dispatch_barrier_async后隨便加一條打印,則會先去執(zhí)行該打印,再去執(zhí)行任務(wù)0-9和柵欄函數(shù);額如果是dispatch_barrier_sync,則會在任務(wù)0-9和柵欄函數(shù)執(zhí)行完后再去執(zhí)行這條打印。

3> 設(shè)計多讀單寫

- (id)readDataForKey:(NSString *)key {

????????__block id result;

? ? ? ? dispatch_sync(_concurrentQueue, ^ {

? ? ? ? ? ? ? ? result = [self valueForKey:key];

????????});

? ? ? ? return result;

}

- (void)writeData:(id)data forKey:(NSString *)key {

? ? ? ? dispatch_barrier_async(_concurrentQueue, ^{

? ? ? ? ? ? ? ? [self setValue:data forKey:key];

????????});

}

(4)dispatch_group_async

場景:在n個耗時并發(fā)任務(wù)都完成后再去執(zhí)行接下來的任務(wù)。比如,在n個網(wǎng)絡(luò)請求完成后刷新UI頁面。

dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUQUE_CONCURRENT);

dispatch_group_t group ?= dispatch_group_create();

for (NSInteger i = 0; i < 10; i++) {

? ? ? ? dispatch_group_async(group, concurrentQueue, ^{

? ? ? ? ? ? ? ? sleep(1);

? ? ? ? ? ? ? ? NSLog(@"%zd:網(wǎng)絡(luò)請求", i);

????????});

}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{

????????NSLog(@"刷新頁面");

});

(5)Dispatch Semaphore

GCD中的信號量是指:Dispatch Semaphore,是持有計數(shù)的信號。

Dispatch Semaphore提供了三個函數(shù)

1、dispatch_semaphore_create:創(chuàng)建一個Semaphore并初始化信號的總量。

2、dispatch_semaphore_signal:發(fā)送一個信號,讓信號總量加1。

3、dispatch_semaphore_wait:可以使總信號量減1,當(dāng)信號總量為0時就會一直等待(阻塞所在線程),否則就可以正常執(zhí)行。

Dispatch Semaphore在開發(fā)中主要用于:

1> 保證線程同步,將異步執(zhí)行的任務(wù)轉(zhuǎn)換成同步執(zhí)行的任務(wù)。

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

__block NSInteger number = 0;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

? ? ? ? number = 100;

? ? ? ? dispatch_semaphore_signal(semaphore);

});

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

NSLog(@"semaphore-- end, number = %zd", number);

dispatch_semaphore_wait加鎖阻塞了當(dāng)前線程,dispatch_semaphore_signal解鎖后,當(dāng)前線程才能繼續(xù)執(zhí)行。

????????2> 保證線程安全,為線程加鎖。

在線程安全中可以將dispatch_semaphore_wait看作是加鎖,而dispatch_semaphore_signal看作是解鎖。

// 首先創(chuàng)建全局變量

_semaphore = dispatch_semaphore_create(1);

__block NSInteger count = 0;

// 這里信號量是1。

- (void)asyncTask {

? ? ? ? dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);

? ? ? ? count++;

? ? ? ? sleep(1);

? ? ? ? NSLog(@"執(zhí)行任務(wù):%zd", count);

? ? ? ? dispatch_semaphore_signal(_semaphore);

}

// 異步并發(fā)調(diào)用asyncTask

for (NSInteger i = 0; i < 100; i++) {???????

????????dispatch_async(dispatch_get_global_queue(0, 0), ^{ ? ? ?????? ??????

??????????????[self asyncTask];?????????

????????});

}

打印的是任務(wù)從1順序執(zhí)行到100,沒有發(fā)生兩個任務(wù)同時執(zhí)行的情況。

原因:在子線程中并發(fā)執(zhí)行asyncTask,那么第一個添加到并發(fā)隊列里的,會將信號量減1,此時信號量等于0,可以執(zhí)行接下來的任務(wù)。而并發(fā)隊列中的其他任務(wù),由于此時信號量不等于0,必須等當(dāng)前正在執(zhí)行的任務(wù)執(zhí)行完畢后調(diào)用dispatch_semaphore_signal將信號量加1,才可以繼續(xù)執(zhí)行接下來的任務(wù),以此類推,從而達(dá)到線程加鎖的目的。

(6)延時函數(shù)(dispatch_after)

dispatch_after能讓我么添加進(jìn)隊列的任務(wù)延時執(zhí)行,該函數(shù)并不是在指定時間后執(zhí)行處理,而只是在指定時間追加處理到dispatch_queue。

// 第一個參數(shù)是time,第二個參數(shù)是dispatch_queue,第三個參數(shù)是要執(zhí)行的block

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

NSLog(@"dispatch_after");

});

由于其內(nèi)部使用的是dispatch_time_t管理時間,而不是NSTimer。所有如果在子線程中調(diào)用,相比于performSelector: afterDelay,不用關(guān)心runloop是否開啟。

7)單例(dispatch_once)

static id _instance;

+ (instancetype)shareInstance {

static dispatch_once_t ?onceToken;

? ? ? ? dispatch_once(&onceToken, ^{

? ? ? ? ? ? ? ? _instance = [[self alloc] init];

????????});

? ? ? ? return _instance;

}

+ (id)allocWithZone:(struct _NSZone *)zone {

static dispatch_one_t onceToken;

? ? ? ? dispatch_once(&onceToken, ^{

? ? ? ? ? ? ? ? _instance = [super allocWithZone:zone];

????????});

? ? ? ? return _instance;

}

// 如果遵守了NSCopying協(xié)議

- (id)copyWithZone:(nullable NSZone *)zone {

????????return _instance;

}

5)NSOperationQueue

NSOperation、NSOperationQueue是蘋果提供給我們的一套多線程解決方案,實際上NSOperation、NSOperationQueue是基于GCD更高一層的封裝,完全面向?qū)ο蟆5潜菺CD更簡單易用、代碼可讀性也更高。

1、可添加完成的代碼塊,在操作完成后執(zhí)行。

2、可以添加操作之間的依賴,方便控制執(zhí)行順序。

3、可以設(shè)置操作執(zhí)行的優(yōu)先級。

4、可以很方便的取消一個操作的執(zhí)行。

5、使用KVO觀察對操作執(zhí)行狀態(tài)的更改:isExecuting、isFinished、isCancelled。

????1)如果只是重寫NSOperation的main方法,由底層控制變更任務(wù)執(zhí)行及完成狀態(tài),以及任務(wù)退出。

????2)如果重寫了NSOperation的start方法,自行控制任務(wù)狀態(tài)。

????3)系統(tǒng)通過KVO的方式移出isFinished == NSOperation。

6、可以設(shè)置最大并發(fā)數(shù)量

既然是基于GCD的更高一層的封裝。那么,GCD中的一些概念同樣適用于NSOperation、NSOperationQueue。在NSOperation、NSOperationQueue中也有類似的任務(wù)和隊列。

操作(Operation):

1、執(zhí)行操作的意思,換句話說就是你在線程中執(zhí)行的那段代碼。

2、在GCD中是放在block里面的。在NSOperation中,我們使用NSOperation子類NSInvocationOperation、NSBlockOperation,或者自定義子類來封裝操作。

操作隊列(OperationQueue)

1、用來存放操作的隊列。不同于GCD中的調(diào)度隊列FIFO(先進(jìn)先出)原則。NSOperationQueue對于添加到隊列中的操作,首先進(jìn)入準(zhǔn)備就緒的狀態(tài)(就緒狀態(tài)取決于操作之間的依賴關(guān)系),然后進(jìn)入就緒狀態(tài)的操作的開始執(zhí)行順序(非結(jié)束執(zhí)行順序),由操作之間相對的優(yōu)先級決定的(優(yōu)先級是操作對象自身的屬性)。

2、操作隊列通過設(shè)置最大并發(fā)數(shù)(maxConcurrentOperationCount)來控制并發(fā)、串行數(shù)量。

3、NSOperationQueue為我們提供了兩種不同類型的隊列:主隊列和自定義隊列。主隊列運行在主線程上,自定義隊列在后臺執(zhí)行。

NSOperation、NSOperationQueue使用步驟:

NSOperation需要配合NSOperationQueue來實現(xiàn)多線程。因為默認(rèn)情況下,NSOperation單獨使用時,系統(tǒng)同步執(zhí)行操作,配合NSOperationQueue才能更好實現(xiàn)異步執(zhí)行。

1、創(chuàng)建操作:先將需要執(zhí)行的操作封裝到一個NSOperation對象中。

2、創(chuàng)建隊列:創(chuàng)建NSOperationQueue對象

3、將操作加入到隊列中:將NSOperation對象添加到NSOperationQueue對象中。

創(chuàng)建操作

NSOperation是個抽象類,不能用來封裝操作。我們只能使用它的子類來封裝操作。

1、NSInvocationOperation。

2、NSBlockOperation。

3、自定義繼承自NSOperation的子類,通過實現(xiàn)內(nèi)部相應(yīng)的方法來封裝操作。

-- NSInvocationOperation

- (void)useInvocationOperation {

? ? ? ? // 1、創(chuàng)建 NSInvocationOperation對象

? ? ? ? NSInvocationOperation * op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task) object:nil];

? ? ? ? // 2、調(diào)用 start 方法開始執(zhí)行操作

? ? ? ? [op start];

}

- (void)task {

? ? ? ? for (int i = 0; i < 10; i++) {

? ? ? ? ? ? ? ? [NSThread sleepForTimeInterval:0.5] // 模擬耗時操作

? ? ? ? ? ? ? ? NSLog(@"111 ------ %@", [NSThread currentThread]); / / 打印當(dāng)前線程

????????}

}

在沒有使用NSOperationQueue,在主線程中單獨使用子類NSInvocationOperation執(zhí)行一個操作時,操作是在當(dāng)前線程執(zhí)行的,并沒有開啟新線程。

切換到其他線程

// 在其他線程使用子類NSInvocationOperation

[NSThread detachNewThreadSelector:@selector(useInvocationOperation) toTarget:self withObject:nil];

在其他線程中單獨使用子類NSInvocationOperation,操作是在當(dāng)前調(diào)用的其他線程上執(zhí)行的,并沒有開啟新線程。

-- NSBlockOperation

- (void)useBlockOperation {

// 1、創(chuàng)建 NSBlockOperation對象

? ? ? ? NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{

? ? ? ? ? ? ? ? for (int i = 0; i < 10; i ++ ) {

? ? ? ? ? ? ? ? ? ? ? ? [NSThread sleepForTimeInterval:0.5];

? ? ? ? ? ? ? ? ? ? ? ? NSLog(@"111 --- %@", [NSThread currentThread]);

????????????????}

????????}];

? ? ? ? // 2、調(diào)用 start 方法開始執(zhí)行操作

? ? ? ? [op start];

}

和上面的NSInvocationOperation使用一樣。因為代碼是在主線程中調(diào)用的,所有打印結(jié)果為主線程。如果在其他線程中執(zhí)行操作,則打印結(jié)果為其他線程。

但是,NSBlockOperation還提供了一個方法 addExecutionBlock:,通過 addExecutionBlock:就可以為NSBlockOperation添加額外的操作。這些操作(包括blockOperationWithBlock中的操作)可以在不同的線程中同時(并發(fā))執(zhí)行。只有當(dāng)所有相關(guān)的操作已經(jīng)完全執(zhí)行時,才視為完成。

如果添加的操作過多的話,blockOperationWithBlock:中的操作也可能會在其他線程(非當(dāng)前線程)中執(zhí)行,這是由系統(tǒng)決定的,并不是說添加到 blockOperationWithBlock:中的操作一定會在當(dāng)前線程中執(zhí)行。

- (void)useBlockOperationAndExecutionBlock {

? ? ? ? NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{

? ? ? ? ? ? ? ? for (int i = 0; i < 10; i++) {

? ? ? ? ? ? ? ? ? ? ? ? [NSThread sleepForTimeInterval:0.2];

? ? ? ? ? ? ? ? ? ? ? ? NSLog(@"1 --- %@", [NSThread currentThread]);

????????????????}

}];

? ? ? ? [op addExecutionBlock:^{

? ? ? ? ? ? ? ? for (int i = 0; i < 10; i++) {

? ? ? ? ? ? ? ? ? ? ? ? [NSThread sleepForTimeInterval:0.2];

? ? ? ? ? ? ? ? ? ? ? ? [NSLog(@"2 ---- %@", [NSThread currentThread])];

????????????????}

????????}];

? ? ? ? // ......

}

使用子類NSBlockOperation,并調(diào)用方法 AddExecutionBlock:的情況下,blockOperationWithBlock:方法中的操作和 addExecutionBlock:中的操作是在不同的線程中異步執(zhí)行的。

一般情況,如果一個NSBlockOperation對象封裝了多個操作。NSBlockOperation是否開啟新線程,取決于操作的個數(shù)。如果操作個數(shù)多,就可能會自動開啟新線程。開啟的線程數(shù)是由系統(tǒng)決定的。

-- 自定義繼承自NSOperation 的子類

通過重寫main或者start方法來定義自己的NSOperation對象。

// KKOperation.h

#import <Foundation/Foundation.h>

@interface KKOperation : NSOperation

@end

#import "KKOperation.h"

@implementation KKOperation

- (void)main {

? ? ? ? if (self.isCancelled) {

? ? ? ? ? ? ? ? if (int i = 0 ; i < 10; i++) {

? ? ? ? ? ? ? ? ? ? ? ? [NSThread sleepForTimeInterval:0.2];

? ? ? ? ? ? ? ? ? ? ? ?NSLog(@"1 ---- %@", [NSThread currentThread]);

????????????????}

????????}

}

@end

#import "KKOperation.h"

- (void)useCustomOperation {

KKOperation *op = [[KKOperation alloc] init];

? ? ? ? [op start];

}

在沒有使用NSOperationQueue,在主線程單獨使用自定義繼承自NSOperation的子類的去年高考下,是在主線程執(zhí)行的操作,并沒有開啟新線程

創(chuàng)建隊列

NSOperationQueue一共有兩種隊列:主隊列、自定義隊列。其中自定義隊列公司包含了串行、并發(fā)功能。

1、主隊列

凡是添加到主隊列中的操作,都會放到主線程中執(zhí)行(注:不包括操作使用addExecutionBlock:添加的額外操作,額外操作可能在其他線程執(zhí)行)。

NSOperationQueue *queue = [NSOperationQueue mainQueue];

2、自定義隊列

????1)添加到這種隊列中的操作,會自動放到子線程中執(zhí)行

? ? 2)同時包含了:串行、并發(fā)功能

? ? ? ? ? ? NSOperationQueue *queue = [[NSOperationQueue alloc] init]; ? ?

-- 將操作放入隊列中

(1) - (void)addOperation:(NSOperation *)op;

需要先創(chuàng)建操作。再將創(chuàng)建好的操作放入到建好的隊列中去

- (void)addOperationToQueue {

NSOperationQueue *queue = [[NSOperation alloc] init];

? ? ? ? NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil];

? ? ? ? NSInvocationOperation *op2 = [[NSInvocation alloc] initWithTarget:self selector:@selector(task2) withObject:nil];

? ? ? ? NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{

? ? ? ? ? ? ? ? for (int i = 0; i < 10; i++) ?{

? ? ? ? ? ? ? ? ? ? ? ? [NSThread sleepForTimeInterval:0.2];

? ? ? ? ? ? ? ? ? ? ? ? NSLog(@"3 --- %@", [NSThread currentThread]);

????????????????}

????????}];

? ? ? ? [op3 addExecutionBlock:^{

? ? ? ? ? ? ? ? for (int i = 0; i < 10; i++) {

? ? ? ? ? ? ? ? ? ? ? ? [NSThread sleepTimeInterval:0.2];

? ? ? ? ? ? ? ? ? ? ? ? NSLog(@"4 --- %@", [NSThread currentThread]);

????????????????}

????????}];

? ? ? ? [queue addOperation:op1];

? ? ? ? [queue addOperation:op2];

? ? ? ? [queue addOperation:op3];

}

使用NSOperation子類創(chuàng)建的操作,并使用addOperation:將操作加入到操作隊列后能夠開啟新線程,進(jìn)行并發(fā)執(zhí)行。

(2)- (void)addOperationWithBlock:(void (^)(void))block;

無需先創(chuàng)建操作,在block中直接添加操作,直接將包含操作的block加入到隊列中。

- (void)addOperationWithBlockToQueue {

NSOperationQueue *queue = [[NSOperation alloc] init];

? ? ? ? [queue addOperationWithBlock:^{

? ? ? ? ? ? ? ? for (int i = 0; i < 10; i++) {

? ? ? ? ? ? ? ? ? ? ? ? [NSThread sleepForTimeInterval:0.2];

? ? ? ? ? ? ? ? ? ? ? ? NSLog(@"1 --- %@", [NSThread currentThread]);

????????????????}

????????}];

? ? ? ? // ....

}

使用addOperationWithBlock:將操作加操作隊列后能夠開啟新線程,進(jìn)行并發(fā)執(zhí)行

-- NSOperationQueue控制串行,并發(fā)執(zhí)行

maxConcurrentOperationCount:最大并發(fā)操作數(shù),用來控制一個特定隊列中可以有多少個操作同時參與并發(fā)執(zhí)行。(不是指并發(fā)線程的數(shù)量,而是一個隊列中同時能并發(fā)執(zhí)行的最大操作數(shù)。而且一個操作也并非只能在一個線程中運行)。

maxConcurrentOperationCount:默認(rèn)情況為-1,表示不進(jìn)行限制,可進(jìn)行并發(fā)執(zhí)行。為1時,隊列為串行隊列,只能串行執(zhí)行。大于1是,隊列為并發(fā)隊列,操作并發(fā)執(zhí)行,當(dāng)然這個值不能超過系統(tǒng)限制,即使自己設(shè)置一個很大的值,系統(tǒng)也會自動調(diào)整為min(自己設(shè)定的值,系統(tǒng)設(shè)定的最大值)。

-- NSOperation操作依賴

NSOperation、NSOperationQueue最吸引人的地方是它能添加操作之間的依賴關(guān)系。通過操作依賴,我們可以很方便的控制操作之間的執(zhí)行順序。

// 添加依賴,使當(dāng)前操作依賴于操作op的完成

1、-(void)addDependency:(NSOperation *)op;?

// 移出依賴,取消當(dāng)前操作對操作op的依賴

2、- (void)removeDependency:(NSOperation *)op;

// 在當(dāng)前操作開始執(zhí)行之前,完成執(zhí)行的所有操作對象數(shù)組

3、@property (readonly, copy) NSArray<NSOperation *> *dependencies;

A執(zhí)行完操作,B才能執(zhí)行操作

- (void)addDependency {

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

? ? ? ? NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{

? ? ? ? ? ? ? ? // .....

????????}];

? ? ? ? NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{

? ? ? ? ? ? ? ? // .....

????????}];

? ? ? ? [op2 addDependency:op1];

? ? ? ? [queue addOperation:op1];

? ? ? ? [queue addOperation:op2];

}

-- NSOperation優(yōu)先級

queuePriority屬性適用于同一操作隊列中的操作,不適用與不同操作隊列中的操作。默認(rèn)所有新創(chuàng)建的操作對象優(yōu)先級都是 NSOperationQueuePriorityNormal;

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {

? ? ? ? NSOperationQueuePriorityVerLow = -8L,

? ? ? ? NSOperationQueuePriorityLow = -4L,

? ? ? ? NSOperationQueuePriorityNormal = 0,

? ? ? ? NSOperationQueuePriorityHigh = 4,

? ? ? ? NSOperationQueuePriorityVeryHigh = 8,

}

對于添加到隊列中的操作,首先進(jìn)入準(zhǔn)備就緒的狀態(tài)(就緒狀態(tài)取決于操作之間的依賴關(guān)系),然后進(jìn)入就緒狀態(tài)的操作開始執(zhí)行,順序(非結(jié)束執(zhí)行順序)由操作之間相對的優(yōu)先級決定(優(yōu)先級是操作對象自身的屬性)。

當(dāng)一個操作的所有依賴都已經(jīng)完成時,操作對象通常會進(jìn)入準(zhǔn)備就緒狀態(tài),等待執(zhí)行。

例如:

現(xiàn)在有4個優(yōu)先級都是 NSOperationQueuePriorityNormal(默認(rèn)級別)的操作:op1、op2、op3、op4.其中op3依賴op2,op2依賴op1?,F(xiàn)在講將者4個操作添加到隊列中并發(fā)執(zhí)行。

1、因為op1和op4都沒有需要依賴的操作,所以在op1、op4執(zhí)行之前,就處于就緒狀態(tài)的操作、

2、op3和op2都有依賴的操作,所以op3和op2都不是準(zhǔn)備就緒狀態(tài)下的操作。

queuePriority的作用:

1、queuePriority屬性決定了進(jìn)入準(zhǔn)備就緒狀態(tài)下的操作之間的開始執(zhí)行順序,并且,優(yōu)先級不能取代依賴關(guān)系。

2、如果一個隊列中既包含高優(yōu)先級操作,又包含低優(yōu)先級操作,并且兩個操作都已經(jīng)準(zhǔn)備就緒,那么列表先執(zhí)行高優(yōu)先級操作。比如上例,如果op1和op4是不同優(yōu)先級操作,那么就會先執(zhí)行優(yōu)先級高的操作。

3、如果一個隊列中既包含了準(zhǔn)備就緒狀態(tài)的操作,又包含了未準(zhǔn)備就緒的操作,未準(zhǔn)備就緒的操作優(yōu)先級比準(zhǔn)備就緒的操作優(yōu)先級高。那么,雖然準(zhǔn)備就緒的操作優(yōu)先級低,也會優(yōu)先執(zhí)行。優(yōu)先級不能取代依賴關(guān)系。如果要控制操作間的啟動順序,則必須使用依賴關(guān)系。

-- NSOperation、NSOperationQueue線程間的通信

在iOS開發(fā)過程中,我們一般在主線程進(jìn)行UI刷新,例如:點擊、滾動、拖拽等事件。我們常把一些耗時的操作放在其他線程,如網(wǎng)絡(luò)圖片加載、文件上傳等耗時操作。當(dāng)我們在其他線程完成了耗時操作時,需要回到主線程,那么就用到了線程之間的通信。

- (void)communication {

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

? ? ? ? [queue addOperationWithBlock:^{

? ? ? ? ? ? ? ? // 耗時操作

????????}];

? ? ? ? [[NSOperationQueue mainQueue] addOperationWithBlock:^{

? ? ? ? ? ? ? ? // 更新UI顯示

????????}];

}

-- NSOperation、NSOperationQueue線程同步和線程安全

1、線程安全:如果代碼所在的進(jìn)程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。

????1)如果每次運行結(jié)果和單線程運行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣,就是線程安全的。

????2)若每個線程中對全局變量、靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執(zhí)行寫操作(更改變量),一般都需要考慮線程同步,否則的話可能影響線程安全。

2、線程同步:

? ? 1)例如:線程A和線程B一塊合作,A執(zhí)行到一定程度時要依靠線程B的某個結(jié)果,于是停下來,示意B運行;B以言執(zhí)行,再講結(jié)果給A;A再繼續(xù)操作。

? ? 2)例如:兩個人在一起聊天,兩個人不能同時說話,避免聽不清(操作沖突)。等一個人說完(一個線程結(jié)束操作),另一個再說(另一個線程開始操作。)

## 模擬火車票售賣:總共有100張火車票,有兩個售賣窗口,一個在北京,一個再廣州。兩窗口同時售賣,直到賣完為止。

非線程安全:

/*

? ? 非線程安全:

? ? 初始化火車票數(shù)量,賣票窗口(非線程安全),并開始賣票

*/

- (void)initTicketStatusNotSave {

????????NSLog(@"currentThread --- %@", [NSThread currentThread]);

? ? ? ? self.ticketSurplusCount = 100;

? ? ? ? // 北京窗口

? ? ? ? NSOperationQueue *queue1?= [[NSOperationQueue alloc] init];

? ? ? ? queue1.maxConcurrentOperationCount = 1;

? ? ? ? // 廣州窗口

? ? ? ? NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];

? ? ? ? queue2.maxConcurrentOperationCount = 1;

? ? ? ? // 售賣操作 op

? ? ? ? NSBlockOperation *op1 ?= [NSBlockOperation blockOperationWithBlock:^{

? ? ? ? ? ? ? ? [self saleTicketNotSafe];

????????}];

? ? ? ? NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{

? ? ? ? ? ? ? ? [self saleTicketNotSafe];

????????}];

? ? ? ? // 添加操作,開始買票

? ? ? ? [queue1 addOperation:op1];

? ? ? ? [queue2 addOperation:op2];

}

// 售賣火車票(非線程安全)

- (void)saleTicketNotSafe{

? ? ? ? while(1) {

? ? ? ? ? ? ? ? if (self.ticketSurplusCount > 0 ) {

? ? ? ? ? ? ? ? ? ? ? ? self.ticketSurplusCount--;

? ? ? ? ? ? ? ? ? ? ? ? NSLog(@"剩余票數(shù):%zd,窗口:%@", self.ticketSurplusCount, [NSThread currentThread]);

????????????????}else {

? ? ? ? ? ? ? ? ? ? ? ? NSLog(@"火車票已售完!");

? ? ? ? ? ? ? ? ? ? ? ? break;

????????????????}

????????}

}

結(jié)果:非線程安全下,即不使用NSLock情況下,得到的票數(shù)錯亂無章的。

線程安全:給線程加鎖,在一個線程執(zhí)行高操作的時候,不允許其他線程進(jìn)行操作

iOS線程加鎖方式:

@synchronized、NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) set/get的等等

/*

線程安全:使用NSLocj加鎖

? ? 初始化火車票數(shù)量,賣票窗口(線程安全),并開始賣票

*/

-(void)initTicketStatusSave {

NSLog(@"currentThread -- %@", [NSThread currentThread]);

? ? ? ? self.ticketSurplusCount = 100; // 總票數(shù)

? ? ? ? self.luck = [[NSLock alloc] init]; // 初始化鎖

? ? ? ? // 初始化售賣窗口

? ? ? ? NSOperationQueue *queue1 = [[NSOperationQueue alloc] init];

? ? ? ? queue1.maxConcurrentOperationCount = 1;

? ? ? ? NSOperationQueue *queue2 = [[NSOperationQueue alloc] init];

? ? ? ? queue2.maxConcurrentOperationCount = 1;

? ? ? ? // 初始化售賣操作

? ? ? ? NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{

? ? ? ? ? ? ? ? [self saleTicketSafe];

????????}];

? ? ? ? NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{

? ? ? ? ? ? ? ? [self saleTicketSafe];

????????}];

? ? ? ? // 添加操作,開始賣票

? ? ? ? [queue1 addOperation:op1];

? ? ? ? [queue2 addOperation:op2];

}

// 售賣火車票(線程安全)

- (void)saleTicketSafe {

? ? ? ? while(1) {

????????????????[self.lock lock]; // 加鎖

? ? ? ? ? ? ? ?if (self.ticketSurplusCount > 0) {

? ? ? ? ? ? ? ? ? ? ? ? self.ticketSurplusCount--;

? ? ? ? ? ? ? ? ? ? ? ? NSLog(@"剩余票數(shù):%zd,窗口:%@", self.ticketSurplusCount, [NSThread currentThread]);

????????????????}

?????????????????[self.lock unlock]; ?// 解鎖

????????????????if (self.ticketSurplusCount <= 0) {

????????????????????????NSLog(@"所有火車票已售完!"); ? ?

????????????????????????bread;

????????????????}????

????????}

}

在考慮了線程安全,使用了NSLock加載、解鎖機制下,得到了票數(shù)是正確的,沒有出現(xiàn)混亂的情況。解決了多線程同步問題。

-- NSOperation常用屬性及方法

1、取消操作:-(void)cancel;

2、判斷操作狀態(tài)的方法

? ? ? ? 1)-(BOOL)isFinished; // 判斷操作是否已經(jīng)結(jié)束

? ? ? ? 2)-(BOOL)isCancelled; // 判斷操作是否已經(jīng)標(biāo)記為取消

? ? ? ? 3)-(BOOL)isExecuting; // 判斷操作是否正在運行

? ? ? ? 4)-(BOOL)isReady; // 判斷操作是否處于準(zhǔn)備就緒狀態(tài),這個值和操作的依賴關(guān)系相關(guān)

3、操作同步

? ? ? ? 1)-(void)waitUntilFinished;阻塞當(dāng)前線程,直到該操作結(jié)束??捎糜诰€程執(zhí)行順序的同步。

? ? ? ? 2)-(void)setCompletionBlock:(void (^)(void))block;completionBlock會在當(dāng)前操作執(zhí)行完畢時執(zhí)行completionBlock。

? ? ? ? 3)-(void)addDependency:(NSOperation *)op;添加依賴,使當(dāng)前操作依賴于操作op的完成。

? ? ? ? 4)-(void)removeDependency:(NSOperation *)op;移出依賴,取消當(dāng)前操作對操作ap的依賴。

? ? ? ? 5)@property (readonly, copy) NSArray<NSOperation *> *dependencies;在當(dāng)前操作開始執(zhí)行之前完成執(zhí)行的所有操作的數(shù)組。

-- NSOperationQueue的常用屬性和方法

1、取消、暫停、恢復(fù)操作

? ? ? ? 1)-(void)cancelAllOperation;可以取消隊列的所有操作。

? ? ? ? 2)-(BOOL)isSuspended;判斷隊列是否處于暫停狀態(tài)。YES:暫停,NO為恢復(fù)。

? ? ? ? 3)-(void)setSuspended:(BOOL)b;可設(shè)置操作的暫停和恢復(fù),YES代表暫停,NO代表恢復(fù)。

2、操作同步

-(void)waitUntilAllOperationsAreFinished;阻塞當(dāng)前線程,知道隊列中的操作全部執(zhí)行完畢。

3、添加、獲取操作

? ? ? ? 1)-(void)addOperationWithBlock:(void (^)(void))block;向隊列中添加一個NSBlockOperation類型的操作對象。

? ? ? ? 2)-(void)addOperations:(NSArray *)ops waitUntilFinished:(BOOL)wait;向隊列中添加數(shù)組,wait標(biāo)志是否阻塞當(dāng)前線程直到所有操作結(jié)束。

? ? ? ? 3)-(NSArray *)operations;當(dāng)前在隊列中的操作數(shù)組(某個操作執(zhí)行結(jié)束后會自動從數(shù)組清除)。

? ? ? ? 4)-(NSInteger)operationCount;當(dāng)前隊列中的操作數(shù)。

4、獲取隊列

? ? ? ? 1)+(id)currentQueue;獲取當(dāng)前隊列,如果當(dāng)前線程不是在NSOperationQueue上運行則返回bil。

? ? ? ? 2)+(id)mianQueue:獲取主隊列。

warning:

1、這里的暫停和取消(包括操作的取消和隊列的取消)并不代表可以將當(dāng)前的操作立即取消,而是當(dāng)當(dāng)前的操作執(zhí)行完畢忠厚不再執(zhí)行新的操作。

2、暫停和取消的區(qū)別就在于:暫停操作之后還可以恢復(fù)操作,繼續(xù)向下執(zhí)行;而取消操作之后,所有的操作就情況了,無法再接著執(zhí)行剩下的操作。

-- NSThread + runloop實現(xiàn)常駐線程

NSThread在實際開發(fā)中比較常用到的場景就是去實現(xiàn)常駐線程。

由于每次開辟子線程都會消耗cpu,在需要頻繁使用子線程的情況下,頻繁開辟子線程會消耗大量的cpu資源,而且創(chuàng)建線程都是任務(wù)執(zhí)行完之后就釋放了,不能再次利用。此時就需要創(chuàng)建一個常駐線程。

GCD實現(xiàn)以單例來保存NSThread

+ (NSThread *)shareThread {

static id shareThread;

? ? ? ? static dispatch_once_t onceToken;

? ? ? ? dispatch_once(&onceToken, ^{

? ? ? ? ? ? ? ? shareThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];

? ? ? ? ? ? ? ? [shareThread setName:@"shareThread"];

? ? ? ? ? ? ? ? [shareThread start];

????????});

? ? ? ? return shareThread;

}

+ (void)threadTest {

? ? ? ? @autoreleasepool {

? ? ? ? ? ? ? ? NSRunLoop *runloop = [NSRunLoop currentRunLoop];

? ? ? ? ? ? ? ? [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

? ? ? ? ? ? ? ? [runloop run];

????????}

}

[self performSelector:@selector(test) onThread:[[self class] shareThread] withObject:nil waitUntilDone:NO];

- (void)test {

? ? ? ? NSLog(@"test thread:%@", [NSThread currentThread]);

}

27、鎖

1、自旋鎖:

是一種用于保護(hù)多線程共享資源的鎖,與一般互斥鎖(mutex)不同之處在于當(dāng)自旋鎖嘗試獲取鎖時,以忙等待(busy waiting)的形式不斷地循環(huán)檢查鎖是否可用。當(dāng)上一個線程的任務(wù)沒有執(zhí)行完畢的時候(被鎖住),那么下一個線程會一直等待(不會睡眠),當(dāng)上一個線程的任務(wù)執(zhí)行完畢,下一個線程會立即執(zhí)行。在多CPU的環(huán)境中,對持有鎖較短的程序來說,使用自旋鎖代替一般的互斥鎖往往能夠提高程序性能。

2、互斥鎖:

讓上一個線程的任務(wù)沒有執(zhí)行完畢的時候(被鎖住),那么下一個線程會進(jìn)入睡眠狀態(tài)等待任務(wù)執(zhí)行完畢,當(dāng)上一個線程的任務(wù)執(zhí)行完畢,下一個線程會自動喚醒然后執(zhí)行任務(wù)。

總結(jié):

自旋鎖會忙等:忙等就是在訪問被鎖資源時,調(diào)用者線程不會休眠,而且不停的在那里循環(huán),直到被鎖資源釋放鎖。

互斥鎖會休眠:休眠就是在訪問被鎖資源時,調(diào)用者線程會休眠,此時cpu可以調(diào)度其他線程工作。直到被鎖資源釋放鎖。此時會喚醒休眠線程。

優(yōu)缺點:

自旋鎖:

????????優(yōu)點:因為自旋鎖不會引起調(diào)用者休眠,所以不會進(jìn)行線程調(diào)度,CPU時間片輪轉(zhuǎn)等耗時操作。所以如果能在很短時間內(nèi)獲得鎖,自旋鎖的效率遠(yuǎn)高于互斥鎖。

? ? ? ? 缺點:自旋鎖一直占著CPU,它在未獲得鎖的情況下,一直自旋,如果不能在很短的實踐內(nèi)獲得鎖,無疑會浪費CPU資源,是CPU效率降低。自旋鎖不能實現(xiàn)遞歸調(diào)用。 ? ? ? ?

自旋鎖:atomic、CSSPinLock、dispatch_semaphore_t

互斥鎖:pthread_mutex、@synchronized、NSLock、NSConditionLock、NSCondition、NSRecursiveLock。

自旋鎖實現(xiàn)思路:

bool lock = false; // 一開始沒上鎖,任何線程都可以申請解鎖

do {

? ? ? ? while (lock); // 如果lock為true就一直死循環(huán),相當(dāng)于申請鎖

? ? ? ? lock = true; ? ?// 上鎖,這樣別的線程就無法獲得鎖

? ? ? ? // 臨界區(qū)

? ? ? ? lock = false; ? ? // 相當(dāng)于釋放鎖,這樣別的線程可以進(jìn)入臨界區(qū)

????????// 不需要鎖保護(hù)的代碼

}

@synchronized

NSObject *objc = [[NSObject alloc] ?init];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

? ? ? ? @synchronized(obj) {

? ? ? ? ? ? ? ? NSLog(@"線程1 開始");

? ? ? ? ? ? ? ? sleep(2);

? ? ? ? ? ? ? ? NSLog(@"線程1 結(jié)束");

????????}

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

sleep(2);

? ? ? ? @synchronized(obj){

? ? ? ? ? ? ? ? NSLog(@"線程 2");

????????}

});

// 線程1 開始 ——》線程1 結(jié)束 ——》 線程2

@synchronized(obj)指令是用obj為該鎖的唯一標(biāo)識,只有當(dāng)標(biāo)識相同時,才能滿足互斥,如果線程2中的obj為其他值,則線程2就不會被阻塞。

@synchronized指令實現(xiàn)鎖的優(yōu)點:我們不需要在代碼中顯示的創(chuàng)建鎖對象,便可以實現(xiàn)鎖的機制,但作為一種防御措施,@synchronized塊會隱士的添加一個異常處理來保護(hù)代碼,該處理例程會在異常拋出的時候自動的釋放互斥鎖。如果不想讓隱士的異常處理例程帶來額外的開銷,可以考慮使用鎖對象。

dispatch_semaphore

dispatch_semaphore_t signal ?= dispatch_semaphore_create(1);

dispatch_time_t waitTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

dispatch_semaphore_wait(signal, waitTime);

? ? ? ? NSLog(@"線程 1 開始");

? ? ? ? sleep(2);

? ? ? ? NSLog(@"線程 1 結(jié)束");

? ? ? ? dispatch_semaphore_signal(signal);

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

sleep(1);

? ? ? ? dispatch_semaphore_wait(signal, waitTime);

? ? ? ? NSLog(@"線程 2");

? ? ? ? dispatch_semaphore_signal(signal);

});

// waitTime > 2:同步操作。 線程1 開始 ? ?——》 線程1 結(jié)束 ? ?——》線程2

//waitTime < 2:線程1 開始 ? ?——》線程2 ? ?——》線程1 結(jié)束

dispatch_semaphore是GCD用來同步的一種方式。

1、dispatch_semaphore_create的聲明:

dispatch_semaphore_t dispatch_semaphore_create(long value);傳入的參數(shù)為long,輸出是一個dispatch_semaphore_t類型且值為value的信號量。

參數(shù)value必須 大于或等于0,否認(rèn)在dispatch_semaphore_create會返回NULL。

2、dispatch_semaphore_signal聲明:

long dispatch_semaphore_signal(dispatch_semaphore_t dsema);這個函數(shù)會使傳入的信號量dsema的值加1。

3、dispatch_semaphore_wait聲明:

long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);設(shè)個函數(shù)會使傳入的信號量dsema的值減1。

函數(shù)作用:如果dsema信號量的值大于0,該函數(shù)所處的線程就繼續(xù)執(zhí)行下面的語句,并且將信號量的值減1;如果dsema的值為0;那么這個函數(shù)就阻塞當(dāng)前線程等待timeout(注意timeout的類型為dispatch_time_t,不能直接傳入整型或float型數(shù)),如果等待期dsema的值被dispatch_semaphore)signal函數(shù)加1了,且該函數(shù)(dispatch_semaphore_wait)所處線程獲得了信號量,那么就繼續(xù)向下執(zhí)行并將信號量減1.如果等待期沒有獲得信號量或者信號量的值一直未0,那么等到timeout時,其所處線程自動執(zhí)行其后語句。

dispatch_semaphore是信號量,但當(dāng)信號總量為1時,也可以當(dāng)作鎖來用。在沒有等待情況出現(xiàn)時,它的性能比 pthread_mutext還要高,但一旦有等待情況出現(xiàn),性能就會下降很多。相對于OSSpinLock來說,它的優(yōu)勢在于等待時不會消耗CPU資源。

NSLock

NSLock *lock = [[NSLock alloc] init];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

// [lock lock];

? ? ? ? [lock lockBeforeDate:[NSDate date]];

? ? ? ? NSLog(@"線程 1 開始");

? ? ? ? sleep(2);

? ? ? ? NSLog(@"線程1 結(jié)束");

? ? ? ? [lock unlock];

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

sleep(1);

? ? ? ? if ([lock tryLock]) { ?// 嘗試獲取鎖,如果獲取不到返回NO,不會阻塞該線程

? ? ? ? ? ? ? ? NSLog(@"鎖可以操作");

? ? ? ? ? ? ? ? [lock unlock];????

????????}else {????

? ? ? ? ? ? ? ? NSLog(@"鎖不可操作");

? ? ? ? }

? ? ? ? NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];

? ? ? ? if ([lock lockBeforeDate:date]) { // 嘗試在未來的3s內(nèi)獲取鎖,并阻塞該線程,如果3s內(nèi)獲取不到,恢復(fù)線程,返回NO,不會阻塞該線程

? ? ? ? ? ? ? ? NSLog(@"沒有超時,獲得鎖");

? ? ? ? ? ? ? ? [lock unlock];

????????}else {

? ? ? ? ? ? ? ? NSLog(@"超時,沒有獲得鎖");

????????}

});

// 線程1 開始 ——》 鎖不可操作 ——》線程1 結(jié)束 ——》沒有超時,獲得鎖

NSLock 是Cocoa提供給我們最基本的鎖對象,也是最經(jīng)常使用的

源碼:

@protocol NSLocking

- (void)lock;

- (void)unlock;

@end

@interface NSLock: NSObject<NSLocking> {

@private

? ? ? ? void *_priv;

}

- (BOOL)tryLock; // 嘗試加鎖,如果鎖不可用(已經(jīng)被鎖住),則不會阻塞線程,并返回NO

-(BOOL)lockBeforeDate:(NSDate *)limit; // 在指定Date之前嘗試加鎖,如果在指定時間之前都不能加鎖,則返回NO

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@end

NSRecursiveLock遞歸鎖:

NSRecursiveLock實際上定義的是一個遞歸鎖,這個鎖可以被同一個線程多次請求,而不會引起死鎖。主要是用在循環(huán)或遞歸操作中。

NSLock *lock = [[NSLock alloc] init];

// NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

static void (^RecursiveMethod)(int);

? ? ? ? RecursiveMethod = ^(int value) {

? ? ? ? ? ? ? ? [lock lock];

? ? ? ? ? ? ? ? if (value > 0) {

? ? ? ? ? ? ? ? ? ? ? ? NSLog(@"value = %d", value);

? ? ? ? ? ? ? ? ? ? ? ? sleep(1);

? ? ? ? ? ? ? ? ? ? ? ? RecursiveMethod(value-1);

????????????????}

? ? ? ? ? ? ? ? [lock unlock];

????????}

? ? ? ? RecursiveMethod(5);

});

這段代碼是一個典型的死鎖情況。在我們的線程中,RecursiveMethod是遞歸調(diào)用的。所以每次進(jìn)入這個block時,都會去加一次鎖,而從第二次開始,有鎖已經(jīng)被使用了且沒有解鎖,所以它需要等待鎖被解除,這樣就導(dǎo)致了死鎖,線程被阻塞了。

在這種情況下,我們就可以使用NSRecursiveLock。它允許同一個線程多次加鎖,而不會造成死鎖。遞歸鎖會跟蹤它被lock的次數(shù)。每次成功的lock都必須平衡調(diào)用unlock操作。只有所有達(dá)到這種平衡,鎖最后才能被釋放。

將NSLock替換為NSRecursiveLock,代碼才能正常工作

@interface NSRecursiveLock : NSObject <NSLocking> {

@private

? ? ? ? void *_priv;

}

- (BOOL)tryLock;

- (BOOL)lockBeforDate:(Date *)limit;

@property (nullable, copt) NSString *name NS_AVAILABEL(10_5,, 2_0);

@end

NSConditionLock條件鎖NSMutableArray *books = [NSMutableArray array];

NSInteger HAS_BOOK = 1;

NSInteger NO_BOOK = 0;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

? ? ? ? while (1) {

? ? ? ? ? ? ? ? [lock lockWhenCondition:NO_BOOK];

? ? ? ? ? ? ? ? [books addObject:[[Book alloc] initWithName:@"圍城"]];

? ? ? ? ? ? ? ? NSLog(@"1 total books:%zd", books.count);

? ? ? ? ? ? ? ? [lock unlockWithCondition:HAS_BOOK];

? ? ? ? ? ? ? ? sleep(1);

????????}

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

? ? ? ? while(1) {

? ? ? ? ? ? ? ? NSLog(@"2 wait for lend a book");

? ? ? ? ? ? ? ?[lock lockWhenCondition:HAS_BOOK];

? ? ? ? ? ? ? ? [books removeObjectAtIndex:0];

? ? ? ? ? ? ? ? NSLog(@"3 lend a book");

? ? ? ? ? ? ? ? [lock unlockWithCondition:NO_BOOK];

????????}

});

// 2 -> 1 -> 3 -> 2 -> 1 -> 3 -> 2 -> 1 -> 3

在線程1中的加鎖使用了lock,是不需要條件的,所以順利的就鎖住了,但在unlock時,使用了一個整型條件,它可以開啟其他線程中正在等待這把鑰匙的鎖。而線程2則只需要一把標(biāo)識為2的鑰匙,所以當(dāng)線程1循環(huán)到最后一次的時候,才最終打開可2中的阻塞。NSConditionLock跟其他鎖一樣,是需要lock與unlock對應(yīng)的,只是lock。lockWhenCondition與unlock、unlockWithCondition是可以隨意組合的。

當(dāng)使用多線程的時候,有時一把只會lock和unlock的鎖,不能完全滿足我們的需求。因為普通的鎖只關(guān)心鎖與不鎖,而不在乎用什么鑰匙才能開鎖,而我們在處理資源共享的時候,多數(shù)請求是只滿足一定的條件下才能打開這把鎖。

NSCondition

NSCondition *condition = [[NSCondition alloc] init];

NSMutableArray *books = [NSMutableArray array];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

? ? ? ? while(1) {

? ? ? ? ? ? ? ? [condition lock];

? ? ? ? ? ? ? ? if [books count] == 0) {

? ? ? ? ? ? ? ? ? ? ? ? NSLog(@"1 ?have no book ");

? ? ? ? ? ? ? ? ? ? ? ? [condition wait];

????????????????}

? ? ? ? ? ? ? ? [books removeObjectAtIndex:0];

? ? ? ? ? ? ? ? NSLog(@"2 ?lend a book ");

? ? ? ? ? ? ? ? [condition unlock];

}

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

? ? ? ? while(1) {

? ? ? ? ? ? ? ? [condition lock];

? ? ? ? ? ? ? ? [books addObject:[[Books alloc] initWithName:@"四季"] ];

? ? ? ? ? ? ? ? NSLog(@"3 ?borrow a book, total books :%zd ", books.count);

? ? ? ? ? ? ? ? [condition signal];

? ? ? ? ? ? ? ? [condition unlock];

? ? ? ? ? ? ? ?sleep(1);

}

});

// 1 -> 3 -> 2 -> 1 -> 3 -> 2 -> 1 -> 3 -> 2

一種最基本的條件鎖,受控控制線程wait和signal

1、[condition lock]:一般用于多線程同時訪問、修改同一個數(shù)據(jù)源時,保證在同一時間內(nèi)數(shù)據(jù)只被訪問、修改一次,其他線程的命令需要lock外等待,直到unlock,才可訪問。

2、[condition unlock]:與lock同理。

3、[condition wait]:讓當(dāng)前線程處于等待狀態(tài)。

4、[condition signal]:CPU發(fā)信號告訴線程不用等待,可以繼續(xù)執(zhí)行

pthread_mutex

__block pthread_mutex_t pLock;

pthread_mutex_init(&pLock, NULL);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

? ? ? ? pthread_mutex_lock(&pLock);

? ? ? ? NSLog(@"線程 1 開始");

? ? ? ? sleep(2);

? ? ? ? NSLog(@"線程 1 結(jié)束");

? ? ? ? pthread_mutex_unlock(&pLock);

}),

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

? ? ? ? sleep(1);

? ? ? ? pthread_mutex_lock(&pLock);

? ? ? ? NSLog(@"線程 2");

? ? ? ? pthread_mutex_unlock(&pLock);

});

// 線程1 開始 ——》 線程 1 結(jié)束 ? ?——》線程 2

c語言下多線程加鎖方式。

1、pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);初始化鎖變量mutex,attr為鎖屬性,NULL值為默認(rèn)屬性。

2、pthread_mutex_look(pthread_mutex_t *mutex):加鎖

3、pthread_mutex_tylock(pthread_mutex_t *mutex);加鎖,但與2不同的是:當(dāng)鎖已經(jīng)在使用的使用,返回EBUSY,而不是掛機等待。

4、pthread_mutex_unlock(pthread_mutex_t *mutex);釋放鎖

5、pthread_mutex_destroy(pthread_mutex_t ** mutex);使用完后釋放

pthread_mutex(recursive)

__block pthread_mutex_t pLock;

// pthread_mutex_init(&pLock, NULL); 初始化會出現(xiàn)死鎖

pthread_mutexattr_t attr;

pthread_mutexattr_init(&attr);

pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

pthread_mutex_init(&pLock, &attr);

pthread_mutexattr_destroy(&attr);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

? ? ? ? static void (^RecursiveMethod)(int);

? ? ? ? RecursiveMethod = ^(int value) {

? ? ? ? ? ? ? ? pthread_mutex_lock(&pLock);

? ? ? ? ? ? ? ? if (value > 0 ) {

? ? ? ? ? ? ? ? ? ? ? ? NSLog(@"value = %zd", value);

? ? ? ? ? ? ? ? ? ? ? ? sleep(1) ? ?;

? ? ? ? ? ? ? ? ? ? ? ? RecursiveMethod(value - 1);

????????????????}

? ? ? ? ? ? ? ? pthread_mutex_unlock(&pLock);

????????};

? ? ? ? RecursiveMethod(5);

});

這是pthread_mutex為了防止在遞歸的情況下出現(xiàn)死鎖而出現(xiàn)的遞歸鎖。作用和NSRecursiveLock遞歸鎖類似。

OSSpinLock

__block OSSpinLock spLock = OS_SPINLOCK_INIT;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

? ? ? ? OSSpinLockLock(&spLock);

? ? ? ? NSLog(@"線程 1 開始");

? ? ? ? sleep(1);

? ? ? ? NSLog(@"線程 1 結(jié)束");

? ? ? ? OSSpinLockUnlock(&spLock);

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

? ? ? ? OSSpinLockLock(&spLock);

? ? ? ? sleep(1);

? ? ? ? NSLog("線程 2");

? ? ? ? QSSpinLockUnlock(&spLock);

});

OSSpinLock:自旋鎖,性能最高的鎖。原理很簡單,就是一直do while 忙等。它的缺點是當(dāng)?shù)却龝r會消耗大量的CPU資源,所以它不適合較長時間的事務(wù)。而且OSSpinLock目前已經(jīng)不安全,慎用。

鎖之間性能對比:

OSSpinLock和dispatch_semaphore的效率遠(yuǎn)遠(yuǎn)高于其他。

@synchronized和NSCondition效率較差

OSSpinLock現(xiàn)在已經(jīng)不安全。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容