iOS 多線程之 GCD

前言:
以下內(nèi)容是對GCD的一些總結(jié),若有寫的不合適的地方,歡迎批評指正。若有遺漏的地方歡迎下方留言,我會做及時更新。

一:關(guān)于GCD的一些概念

1.GCD概念

Grand Central Dispatch(GCD) 是 Apple 開發(fā)的一個多核編程的較新的解決方法。它主要用于優(yōu)化應用程序以支持多核處理器以及其他對稱多處理系統(tǒng)。它是一個在線程池模式的基礎(chǔ)上執(zhí)行的并發(fā)任務。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。

2.GCD 任務和隊列

1>任務:

1)概念
就是執(zhí)行操作的意思,換句話說就是你在線程中執(zhí)行的那段代碼在 GCD 中是放在 block 中的*。
2)執(zhí)行任務有兩種方式:
同步執(zhí)行(sync)和異步執(zhí)行(async)
同步執(zhí)行(sync)
同步添加任務到指定的隊列中,在添加的任 務執(zhí)行結(jié)束之前,會一直等待,直到隊列里面的任務完成之后再繼續(xù)執(zhí)行只能在當前線程中執(zhí)行任務,不具備開啟新線程的能力
異步執(zhí)行(async)
異步添加任務到指定的隊列中,它不會做任何等待,可以繼續(xù)執(zhí)行任務。
可以在新的線程中執(zhí)行任務,具備開啟新線程的能力。

2>隊列

1)概念
隊列(Dispatch Queue):這里的隊列指執(zhí)行任務的等待隊列,即用來存放任務的隊列。隊列是一種特殊的線性表,采用 FIFO(先進先出)的原則,即新任務總是被插入到隊列的末尾,而讀取任務的時候總是從隊列的頭部開始讀取。每讀取一個任務,則從隊列中釋放一個任務
2)GCD 中兩種隊列
串行隊列和并發(fā)隊列。兩者都符合 FIFO(先進先出)的原則。兩者的主要區(qū)別是:執(zhí)行順序不同,以及開啟線程數(shù)不同。
串行隊列(Serial Dispatch Queue)
每次只有一個任務被執(zhí)行。讓任務一個接著一個地執(zhí)行。(只開啟一個線程,一個任務執(zhí)行完畢后,再執(zhí)行下一個任務)
并發(fā)隊列(Concurrent Dispatch Queue)
可以讓多個任務并發(fā)(同時)執(zhí)行。(可以開啟多個線程,并且同時執(zhí)行任務)
注意:并發(fā)隊列 的并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效

3. GCD 的使用

3.1 隊列的創(chuàng)建方法、獲取方法

3.1.1自己創(chuàng)建的隊列

可以使用dispatch_queue_create來創(chuàng)建隊列,需要傳入兩個參數(shù),
第一個參數(shù)表示隊列的唯一標識符,用于 DEBUG,可為空,Dispatch Queue 的名稱推薦使用應用程序 ID 這種逆序全程域名;
第二個參數(shù)用來識別是串行隊列還是并發(fā)隊列。
DISPATCH_QUEUE_SERIAL 表示串行隊列,DISPATCH_QUEUE_CONCURRENT 表示并發(fā)隊列
。

    /*
      第一個參數(shù)表示隊列的唯一標識符,用于 DEBUG,可為空,Dispatch Queue 的名稱推薦使用應用程序 ID 這種逆序全程域名;
      第二個參數(shù)用來識別是串行隊列還是并發(fā)隊列。
      **********DISPATCH_QUEUE_SERIAL 表示串行隊列,DISPATCH_QUEUE_CONCURRENT 表示并發(fā)隊列。
    */
    //創(chuàng)建串行隊
    dispatch_queue_t queueSerial = dispatch_queue_create("gcd.test.queue", DISPATCH_QUEUE_SERIAL);
    //創(chuàng)建并發(fā)隊列
    dispatch_queue_t queueConcurret = dispatch_queue_create("gcd.test.queue", DISPATCH_QUEUE_CONCURRENT);    

3.1.2主隊列

主隊列的獲取方法:

// 主隊列的獲取方法
dispatch_queue_t queue = dispatch_get_main_queue();

說明:
主隊列由系統(tǒng)自動創(chuàng)建,并與應用程序的主線程相關(guān)聯(lián)。主隊列上的任務一定是在主線程上執(zhí)行(不過主線程并不是只執(zhí)行主隊列的任務,為了避免線程切換對性能的消耗,主線程還有可能會執(zhí)行其他隊列的任務)。
主隊列是一個串行隊列,所以即便是在異步函數(shù)中也不會去開啟新的線程。它只有在主線程空閑的時候才能調(diào)度里面的任務。
開發(fā)中一般是子線程執(zhí)行完耗時操作后,我們獲取到主隊列并將刷新UI的任務添加到主隊列中去執(zhí)行。

案例分析:

-(void)mainQueueTest{
    NSLog(@"全局隊列同步");
    for (int i = 0 ; i < 5; i++) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            //模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d------%@",i,[NSThread currentThread]);
        });
    }
}
上面程序運行會堵塞線程;
因為主隊列是串行隊列,如果通過同步函數(shù)向主隊列中添加任務,那么并不會開啟子線程來執(zhí)行這個任務,而是在主線程中執(zhí)行。
我們將方法mainQueueTest1稱作task1,將同步函數(shù)添加的任務稱作task2。task1和task2都在主隊列中,主隊列先安排task1到主線程執(zhí)行,等task1執(zhí)行了完了才能安排task2到主線程執(zhí)行,而task1又必須等task2執(zhí)行完了才能繼續(xù)往下執(zhí)行,而task2又必須等task1執(zhí)行完了才能被主隊列安排執(zhí)行,這樣就造成了相互等待而卡死(死鎖)。
-(void)mainQueueTest{
    NSLog(@"主隊列同步");
    for (int i = 0 ; i < 5; i++) {
        dispatch_async(dispatch_get_main_queue(), ^{
            //模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d------%@",i,[NSThread currentThread]);
        });
    }
}
運行結(jié)果:
2021-03-22 23:36:04.077073+0800 ios_thread[4498:62538] 全局隊列同步
2021-03-22 23:36:06.129103+0800 ios_thread[4498:62538] 0------<NSThread: 0x600001428040>{number = 1, name = main}
2021-03-22 23:36:08.130407+0800 ios_thread[4498:62538] 1------<NSThread: 0x600001428040>{number = 1, name = main}
2021-03-22 23:36:10.132023+0800 ios_thread[4498:62538] 2------<NSThread: 0x600001428040>{number = 1, name = main}
2021-03-22 23:36:12.133623+0800 ios_thread[4498:62538] 3------<NSThread: 0x600001428040>{number = 1, name = main}
2021-03-22 23:36:14.135197+0800 ios_thread[4498:62538] 4------<NSThread: 0x600001428040>{number = 1, name = main}
說明:
在主隊列中通過異步函數(shù)添加的任務都是在主線程執(zhí)行
(因為主隊列中的任務都在主線程執(zhí)行,所以并不會開啟新的線程),
并且是串行執(zhí)行。

3.1.3全局隊列

獲取全局隊列的函數(shù)是dispatch_get_global_queue(long identifier, unsigned long flags)。第一個參數(shù)表示隊列的優(yōu)先級(優(yōu)先級等級如下表所示);第二個參數(shù)是保留參數(shù),傳0即可。全局隊列是一個并發(fā)隊列。
注意:這里的優(yōu)先級和NSOperation中的優(yōu)先級不同,這里的優(yōu)先級是指隊列的優(yōu)先級,NSOperation中的優(yōu)先級是指任務的優(yōu)先級。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
-(void)globalQueueTest{
    NSLog(@"全局隊列同步");
    for (int i = 0 ; i < 5; i++) {
        dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            //模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            NSLog(@"%d------%@",i,[NSThread currentThread]);
        });
    }
}
打印結(jié)果:
2021-03-22 23:54:07.952830+0800 ios_thread[4676:73422] 全局隊列同步
2021-03-22 23:54:09.954219+0800 ios_thread[4676:73422] 0------<NSThread: 0x600001278640>{number = 1, name = main}
2021-03-22 23:54:11.955710+0800 ios_thread[4676:73422] 1------<NSThread: 0x600001278640>{number = 1, name = main}
2021-03-22 23:54:13.957181+0800 ios_thread[4676:73422] 2------<NSThread: 0x600001278640>{number = 1, name = main}
2021-03-22 23:54:15.958770+0800 ios_thread[4676:73422] 3------<NSThread: 0x600001278640>{number = 1, name = main}
2021-03-22 23:54:17.960338+0800 ios_thread[4676:73422] 4------<NSThread: 0x600001278640>{number = 1, name = main}

3.1.4隊列總結(jié)

image.png

3.2 任務的創(chuàng)建方法

GCD 同步執(zhí)行任務:創(chuàng)建方法dispatch_sync
GCD異步執(zhí)行任務:創(chuàng)建方法dispatch_async

// 同步執(zhí)行任務創(chuàng)建方法
dispatch_sync(queue, ^{
    // 這里放同步執(zhí)行任務代碼
});
// 異步執(zhí)行任務創(chuàng)建方法
dispatch_async(queue, ^{
    // 這里放異步執(zhí)行任務代碼
});

我們根據(jù)兩種任務(同步和異步)和3種執(zhí)行方式(串行隊列、并發(fā)隊列、主隊列)組合成6中不同的方式

1.同步執(zhí)行 + 并發(fā)隊列
2.異步執(zhí)行 + 并發(fā)隊列
3.同步執(zhí)行 + 串行隊列
4.異步執(zhí)行 + 串行隊列
5.同步執(zhí)行 + 主隊列
6.異步執(zhí)行 + 主隊列

接下分析一下這6中不同的組合方式

3.2.1同步執(zhí)行+并發(fā)隊列

#pragma mark 同步執(zhí)行+并發(fā)隊列
/**
 * 同步執(zhí)行 + 并發(fā)隊列
 * 特點:在當前線程中執(zhí)行任務,不會開啟新線程,執(zhí)行完一個任務,再執(zhí)行下一個任務。
 */
- (void)syncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"begin");
    
    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_sync(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });

    dispatch_sync(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });

    dispatch_sync(queue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    
    NSLog(@"end");
}
打印結(jié)果:
2021-03-23 22:45:04.439960+0800 ios_thread[1136:32399] currentThread---<NSThread: 0x6000009c46c0>{number = 1, name = main}
2021-03-23 22:45:04.440112+0800 ios_thread[1136:32399] begin
2021-03-23 22:45:06.441521+0800 ios_thread[1136:32399] 1---<NSThread: 0x6000009c46c0>{number = 1, name = main}
2021-03-23 22:45:08.442404+0800 ios_thread[1136:32399] 1---<NSThread: 0x6000009c46c0>{number = 1, name = main}
2021-03-23 22:45:10.443892+0800 ios_thread[1136:32399] 2---<NSThread: 0x6000009c46c0>{number = 1, name = main}
2021-03-23 22:45:12.445396+0800 ios_thread[1136:32399] 2---<NSThread: 0x6000009c46c0>{number = 1, name = main}
2021-03-23 22:45:14.446111+0800 ios_thread[1136:32399] 3---<NSThread: 0x6000009c46c0>{number = 1, name = main}
2021-03-23 22:45:16.448193+0800 ios_thread[1136:32399] 3---<NSThread: 0x6000009c46c0>{number = 1, name = main}
2021-03-23 22:45:16.448513+0800 ios_thread[1136:32399] end
總結(jié):不啰嗦了打印結(jié)果一目了然

3.2.2 異步執(zhí)行 + 并發(fā)隊列

#pragma mark 同步執(zhí)行+并發(fā)隊列
/**
 * 同步執(zhí)行 + 并發(fā)隊列
 * 特點:在當前線程中執(zhí)行任務,不會開啟新線程,執(zhí)行完一個任務,再執(zhí)行下一個任務。
 */
- (void)syncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"begin");
    
    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_sync(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });

    dispatch_sync(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });

    dispatch_sync(queue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });
    
    NSLog(@"end");
}
#pragma mark 異步執(zhí)行并發(fā)隊列
/**
 * 異步執(zhí)行 + 并發(fā)隊列
 * 特點:可以開啟多個線程,任務交替(同時)執(zhí)行。
 */
- (void)asyncConcurrent {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"begin");

    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        // 追加任務1
        for (int i = 0; i < 3; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });

    dispatch_async(queue, ^{
        // 追加任務2
        for (int i = 0; i < 1; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });

    dispatch_async(queue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });

    NSLog(@"asyncConcurrent---end");
}
打印結(jié)果:
2021-03-23 22:53:23.149962+0800 ios_thread[1210:37698] currentThread---<NSThread: 0x6000019cca80>{number = 1, name = main}
2021-03-23 22:53:23.150112+0800 ios_thread[1210:37698] begin
2021-03-23 22:53:23.150273+0800 ios_thread[1210:37698] asyncConcurrent---end
2021-03-23 22:53:25.151177+0800 ios_thread[1210:37781] 1---<NSThread: 0x6000019e2040>{number = 5, name = (null)}
2021-03-23 22:53:25.151179+0800 ios_thread[1210:37783] 3---<NSThread: 0x6000019e1f00>{number = 4, name = (null)}
2021-03-23 22:53:25.151269+0800 ios_thread[1210:37779] 2---<NSThread: 0x6000019c9900>{number = 6, name = (null)}
2021-03-23 22:53:27.156291+0800 ios_thread[1210:37781] 1---<NSThread: 0x6000019e2040>{number = 5, name = (null)}
2021-03-23 22:53:27.156291+0800 ios_thread[1210:37783] 3---<NSThread: 0x6000019e1f00>{number = 4, name = (null)}
2021-03-23 22:53:29.161743+0800 ios_thread[1210:37781] 1---<NSThread: 0x6000019e2040>{number = 5, name = (null)}

從打印的日志可以清晰的看到,除了當前主線程,系統(tǒng)有開啟了3個線程,并且任務是交替同時執(zhí)行的。說明異步執(zhí)行具備開啟線程的能力,并發(fā)隊列可開啟多個線程同時執(zhí)行多個任務。

3.2.3 同步執(zhí)行 + 串行隊列

#pragma mark 同步執(zhí)行 + 串行隊列
/**
 * 同步執(zhí)行 + 串行隊列
 * 特點:不會開啟新線程,在當前線程執(zhí)行任務。任務是串行的,執(zhí)行完一個任務,再執(zhí)行下一個任務。
 */
- (void)syncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"syncSerial---begin");

    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });

    NSLog(@"syncSerial---end");
}
打印日志:
2021-03-23 23:06:24.647668+0800 ios_thread[1257:44419] currentThread---<NSThread: 0x6000034e4980>{number = 1, name = main}
2021-03-23 23:06:24.647819+0800 ios_thread[1257:44419] syncSerial---begin
2021-03-23 23:06:26.649245+0800 ios_thread[1257:44419] 1---<NSThread: 0x6000034e4980>{number = 1, name = main}
2021-03-23 23:06:28.650779+0800 ios_thread[1257:44419] 1---<NSThread: 0x6000034e4980>{number = 1, name = main}
2021-03-23 23:06:30.652343+0800 ios_thread[1257:44419] 2---<NSThread: 0x6000034e4980>{number = 1, name = main}
2021-03-23 23:06:32.653168+0800 ios_thread[1257:44419] 2---<NSThread: 0x6000034e4980>{number = 1, name = main}
2021-03-23 23:06:34.653735+0800 ios_thread[1257:44419] 3---<NSThread: 0x6000034e4980>{number = 1, name = main}
2021-03-23 23:06:36.654308+0800 ios_thread[1257:44419] 3---<NSThread: 0x6000034e4980>{number = 1, name = main}
2021-03-23 23:06:36.654652+0800 ios_thread[1257:44419] syncSerial---end

總結(jié):
1.所有任務都是在當前線程(主線程)中執(zhí)行的,并沒有開啟新的線程(同步執(zhí)行不具備開啟新線程的能力)
2.任務是按順序執(zhí)行的(串行隊列每次只有一個任務被執(zhí)行,任務一個接一個按順序執(zhí)行)

3.2.4 異步執(zhí)行 + 串行隊列

#pragma mark 異步執(zhí)行 + 串行隊列
/**
 * 異步執(zhí)行 + 串行隊列
 * 特點:會開啟新線程,但是因為任務是串行的,執(zhí)行完一個任務,再執(zhí)行下一個任務。
 */
- (void)asyncSerial {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"asyncSerial---begin");

    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_async(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"3---%@",[NSThread currentThread]);
        }
    });

    NSLog(@"asyncSerial---end");
}
打印日志:
2021-03-23 23:16:44.027616+0800 ios_thread[1299:50254] currentThread---<NSThread: 0x600001fb08c0>{number = 1, name = main}
2021-03-23 23:16:44.027780+0800 ios_thread[1299:50254] asyncSerial---begin
2021-03-23 23:16:44.027958+0800 ios_thread[1299:50254] asyncSerial---end
2021-03-23 23:16:46.033318+0800 ios_thread[1299:50352] 1---<NSThread: 0x600001fa44c0>{number = 6, name = (null)}
2021-03-23 23:16:48.037867+0800 ios_thread[1299:50352] 1---<NSThread: 0x600001fa44c0>{number = 6, name = (null)}
2021-03-23 23:16:50.038528+0800 ios_thread[1299:50352] 2---<NSThread: 0x600001fa44c0>{number = 6, name = (null)}
2021-03-23 23:16:52.044250+0800 ios_thread[1299:50352] 2---<NSThread: 0x600001fa44c0>{number = 6, name = (null)}
2021-03-23 23:16:54.049903+0800 ios_thread[1299:50352] 3---<NSThread: 0x600001fa44c0>{number = 6, name = (null)}
2021-03-23 23:16:56.054594+0800 ios_thread[1299:50352] 3---<NSThread: 0x600001fa44c0>{number = 6, name = (null)}

總結(jié):
1.異步執(zhí)行具備開啟新線程的能力,串行隊列只開啟一個線程
(主線程:<NSThread: 0x600001fb08c0>,
新線程:<NSThread: 0x600001fa44c0>)

2.任務是按順序執(zhí)行的

3.2.5 同步執(zhí)行 + 主隊列

同步執(zhí)行 + 主隊列在不同線程中調(diào)用結(jié)果也是不一樣,在主線程中調(diào)用會出現(xiàn)死鎖,而在其他線程中則不會。

#pragma mark 同步執(zhí)行 + 主隊列
/**
 * 同步執(zhí)行 + 主隊列
 * 特點(主線程調(diào)用):互等卡主不執(zhí)行。
 * 特點(其他線程調(diào)用):不會開啟新線程,執(zhí)行完一個任務,再執(zhí)行下一個任務。
 */
- (void)syncMain {

    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"syncMain---begin");

    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_sync(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });

    dispatch_sync(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });

    NSLog(@"syncMain---end");
}
打印日志:
2021-03-23 23:26:43.957379+0800 ios_thread[1352:56626] currentThread---<NSThread: 0x600002430440>{number = 1, name = main}
2021-03-23 23:26:43.957592+0800 ios_thread[1352:56626] syncMain---begin
(lldb) 

總結(jié):
1.直接崩掉,具體原因在上文的主隊列案例中已經(jīng)做了詳細解釋

3.2.6 在其他線程中調(diào)用同步執(zhí)行 + 主隊列

// 使用 NSThread 的 detachNewThreadSelector 方法會創(chuàng)建線程,并自動啟動線程執(zhí)行
 selector 任務
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
- (void)syncMain {

    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"syncMain---begin");

    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_sync(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });

    dispatch_sync(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });

    NSLog(@"syncMain---end");
}
輸出結(jié)果:
2021-03-23 23:46:33.008678+0800 ios_thread[1411:66125] currentThread---<NSThread: 0x6000010331c0>{number = 8, name = (null)}
2021-03-23 23:46:33.008840+0800 ios_thread[1411:66125] syncMain---begin
2021-03-23 23:46:35.036965+0800 ios_thread[1411:65970] 1---<NSThread: 0x600001060940>{number = 1, name = main}
2021-03-23 23:46:37.037517+0800 ios_thread[1411:65970] 1---<NSThread: 0x600001060940>{number = 1, name = main}
2021-03-23 23:46:39.039026+0800 ios_thread[1411:65970] 2---<NSThread: 0x600001060940>{number = 1, name = main}
2021-03-23 23:46:41.040455+0800 ios_thread[1411:65970] 2---<NSThread: 0x600001060940>{number = 1, name = main}
2021-03-23 23:46:41.040750+0800 ios_thread[1411:66125] syncMain---end

總結(jié):
1.所有任務都是在主線程(非當前線程)中執(zhí)行的,沒有開啟新的線程(所有放在主隊列中的任務,都會放到主線程中執(zhí)行)
2.任務是按順序執(zhí)行的(主隊列是串行隊列,每次只有一個任務被執(zhí)行,任務一個接一個按順序執(zhí)行)
分析:完美解決3.2.5中的問題
因為syncMain 任務放到了其他線程里,而任務1、任務2、都在追加到主隊列中,這三個任務都會在主線程中執(zhí)行。syncMain 任務在其他線程中執(zhí)行到追加任務1到主隊列中,因為主隊列現(xiàn)在沒有正在執(zhí)行的任務,所以,會直接執(zhí)行主隊列的任務1,等任務1執(zhí)行完畢,再接著執(zhí)行任務2。這樣就解決了卡頓死鎖的問題了

3.2.7 異步執(zhí)行 + 主隊列

#pragma mark 異步執(zhí)行 + 主隊列
/**
 * 異步執(zhí)行 + 主隊列
 * 特點:只在主線程中執(zhí)行任務,執(zhí)行完一個任務,再執(zhí)行下一個任務
 */
- (void)asyncMain {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"asyncMain---begin");

    dispatch_queue_t queue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"1---%@",[NSThread currentThread]);
        }
    });

    dispatch_async(queue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"2---%@",[NSThread currentThread]);
        }
    });

    NSLog(@"asyncMain---end");
}
輸入結(jié)果:
2021-03-23 23:57:44.857805+0800 ios_thread[1450:72181] currentThread---<NSThread: 0x600001cd4080>{number = 1, name = main}
2021-03-23 23:57:44.857960+0800 ios_thread[1450:72181] asyncMain---begin
2021-03-23 23:57:44.858116+0800 ios_thread[1450:72181] asyncMain---end
2021-03-23 23:57:46.887331+0800 ios_thread[1450:72181] 1---<NSThread: 0x600001cd4080>{number = 1, name = main}
2021-03-23 23:57:48.887700+0800 ios_thread[1450:72181] 1---<NSThread: 0x600001cd4080>{number = 1, name = main}
2021-03-23 23:57:50.889198+0800 ios_thread[1450:72181] 2---<NSThread: 0x600001cd4080>{number = 1, name = main}
2021-03-23 23:57:52.890802+0800 ios_thread[1450:72181] 2---<NSThread: 0x600001cd4080>{number = 1, name = main}

總結(jié):
1.所有任務都是在當前線程(主線程)中執(zhí)行的,并沒有開啟新的線程(雖然異步執(zhí)行具備開啟線程的能力,但因為是主隊列,所以所有任務都在主線程中)
*2.任務是按順序執(zhí)行的(因為主隊列是串行隊列,每次只有一個任務被執(zhí)行,任務一個接一個按順序執(zhí)行)。

4.GCD 線程間的通信

4.1 iOS開發(fā)中的高頻操作

在 iOS 開發(fā)過程中。我們通常把一些耗時的操作放在其他線程,比如說圖片下載、文件上傳等耗時操作。而當我們有時候在其他線程完成了耗時操作時,需要回到主線程,在主線程里邊進行 UI 刷新,那么就用到了線程之間的通訊
案例

- (void)communication {
    // 獲取全局并發(fā)隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    // 獲取主隊列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    dispatch_async(queue, ^{
        // 異步追加任務
        for (int i = 0; i < 3; ++i) {
            // 模擬耗時操作 例如網(wǎng)絡請求
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"1---%@",[NSThread currentThread]);
        }

        // 回到主線程
        dispatch_async(mainQueue, ^{
            // 追加在主線程中執(zhí)行的任務 例如刷新UI
            // 模擬耗時操作
            [NSThread sleepForTimeInterval:2];
            // 打印當前線程
            NSLog(@"2---%@",[NSThread currentThread]);
        });
    });
}
輸出結(jié)果:
2021-03-24 00:08:54.439904+0800 ios_thread[1499:79037] 1---<NSThread: 0x6000020e7fc0>{number = 3, name = (null)}
2021-03-24 00:08:56.445422+0800 ios_thread[1499:79037] 1---<NSThread: 0x6000020e7fc0>{number = 3, name = (null)}
2021-03-24 00:08:58.446310+0800 ios_thread[1499:79037] 1---<NSThread: 0x6000020e7fc0>{number = 3, name = (null)}
2021-03-24 00:09:00.448014+0800 ios_thread[1499:78899] 2---<NSThread: 0x6000020e8240>{number = 1, name = main}

總結(jié):
輸入結(jié)果顯示:其他線程中先執(zhí)行任務,執(zhí)行完了之后回到主線程執(zhí)行主線程的相應操作

4.2 GCD 柵欄方法:dispatch_barrier_async

4.2.1.概念解釋:

dispatch_barrier)柵欄函數(shù)是GCD提供的用于阻塞分割任務的一組函數(shù)。其主要作用就是在隊列中設(shè)置柵欄,來人為干預隊列中任務的執(zhí)行順序。也可以理解為用來設(shè)置任務之間的依賴關(guān)系。柵欄函數(shù)分同步柵欄函數(shù)和異步柵欄函數(shù)

4.2.2柵欄函數(shù)分類

同步柵欄函數(shù)和異步柵欄函數(shù)

同步柵欄函數(shù)和異步柵欄函數(shù)的異同點
** 相同點:** 它們都將多個任務分割成了3個部分,第1部分是柵欄函數(shù)之前的任務,是最先執(zhí)行的;第2部分是柵欄函數(shù)添加的任務,這個任務要等柵欄函數(shù)之前的任務都執(zhí)行完了才會執(zhí)行;第3個部分是柵欄函數(shù)之后的任務,這個部分要等柵欄函數(shù)里面的任務執(zhí)行完了才會執(zhí)行
不同點: 同步柵欄函數(shù)不會開啟新線程,其添加的任務在當前線程執(zhí)行,會阻塞當前線程;異步柵欄函數(shù)會開啟新線程來執(zhí)行其添加的任務,不會阻塞當前線程

4.2.3需求案例分析

假設(shè)有這樣一個需求:
一個大文件被分成part1和part2兩部分存在服務器上,現(xiàn)在要將part1和part2都下載下來后然后合并并寫入磁盤。這里其實有4個task,下載part1是task1,下載part2是task2,合并part1和part2是task3,將合并后的文件寫入磁盤是task4。這4個任務執(zhí)行順序是task1和task2并發(fā)異步執(zhí)行,這兩個任務都執(zhí)行完了后再執(zhí)行task3,task3執(zhí)行完了再執(zhí)行task4
我們通過兩種方案來實現(xiàn)這個需求:

4.2.3.1同步柵欄實現(xiàn)方式:

- (void)barrierSync {
    NSLog(@"當前線程1");
    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"開始下載part1 -> %@",[NSThread currentThread]);
        //這里模擬下載操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"完成part1下載 -> %@",[NSThread currentThread]);
    });
    NSLog(@"當前線程2");
    dispatch_async(queue, ^{
        NSLog(@"開始下載part2 -> %@",[NSThread currentThread]);
        //這里模擬下載操作
        [NSThread sleepForTimeInterval:4];
        NSLog(@"完成part2下載 -> %@",[NSThread currentThread]);
    });
    NSLog(@"當前線程3");
    dispatch_barrier_sync(queue, ^{
        NSLog(@"合并part1和part2 -> %@",[NSThread currentThread]);
        //模擬合并操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"合并part1和part2合并完成 -> %@",[NSThread currentThread]);
    });
    NSLog(@"當前線程4");
輸出結(jié)果:
2021-03-24 16:51:18.997002+0800 ios_thread[36727:332568] 當前線程1
2021-03-24 16:51:18.998320+0800 ios_thread[36727:332568] 當前線程2
2021-03-24 16:51:18.998501+0800 ios_thread[36727:332941] 開始下載part1 -> <NSThread: 0x600000a51400>{number = 5, name = (null)}
2021-03-24 16:51:18.999153+0800 ios_thread[36727:332568] 當前線程3
2021-03-24 16:51:18.999273+0800 ios_thread[36727:333275] 開始下載part2 -> <NSThread: 0x600000a4f300>{number = 6, name = (null)}
2021-03-24 16:51:21.002392+0800 ios_thread[36727:332941] 完成part1下載 -> <NSThread: 0x600000a51400>{number = 5, name = (null)}
2021-03-24 16:51:23.002451+0800 ios_thread[36727:333275] 完成part2下載 -> <NSThread: 0x600000a4f300>{number = 6, name = (null)}
2021-03-24 16:51:23.002885+0800 ios_thread[36727:332568] 合并part1和part2 -> <NSThread: 0x600000a10480>{number = 1, name = main}
2021-03-24 16:51:25.003632+0800 ios_thread[36727:332568] 合并part1和part2合并完成 -> <NSThread: 0x600000a10480>{number = 1, name = main}
2021-03-24 16:51:25.003940+0800 ios_thread[36727:332568] 當前線程4

總結(jié):
通過運行過程(看模擬器或真機的運行狀態(tài))和運行結(jié)果可以看出,需求的功能是實現(xiàn)了,但是有個問題,同步柵欄函數(shù)在分割任務的同時也阻塞了當前線程,這里當前線程是主線程,這就意味著在task1、task2這2個任務都完成之前,UI界面是出于卡死狀態(tài)的,這種用戶體驗顯然是非常糟糕的。所以在開發(fā)中很理所應當?shù)木桶阉黳ass掉了

4.2.3.2異步柵欄實現(xiàn)

- (void)barrierAsync {
    NSLog(@"當前線程1");
    dispatch_queue_t queue = dispatch_queue_create("net.thread.testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"開始下載part1 -> %@",[NSThread currentThread]);
        //這里模擬下載操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"完成part1下載 -> %@",[NSThread currentThread]);
    });
    NSLog(@"當前線程2");
    dispatch_async(queue, ^{
        NSLog(@"開始下載part2 -> %@",[NSThread currentThread]);
        //這里模擬下載操作
        [NSThread sleepForTimeInterval:4];
        NSLog(@"完成part2下載 -> %@",[NSThread currentThread]);
    });
    NSLog(@"當前線程3");
    dispatch_barrier_async(queue, ^{
        NSLog(@"合并part1和part2 -> %@",[NSThread currentThread]);
        //模擬合并操作
        [NSThread sleepForTimeInterval:2];
        NSLog(@"合并part1和part2合并完成 -> %@",[NSThread currentThread]);
    });
    NSLog(@"當前線程4");
}
輸出結(jié)果:
2021-03-24 16:58:59.454335+0800 ios_thread[36787:337646] 當前線程1
2021-03-24 16:58:59.454524+0800 ios_thread[36787:337646] 當前線程2
2021-03-24 16:58:59.454576+0800 ios_thread[36787:337720] 開始下載part1 -> <NSThread: 0x6000008c0040>{number = 5, name = (null)}
2021-03-24 16:58:59.454679+0800 ios_thread[36787:337646] 當前線程3
2021-03-24 16:58:59.454742+0800 ios_thread[36787:337719] 開始下載part2 -> <NSThread: 0x6000008b2800>{number = 3, name = (null)}
2021-03-24 16:58:59.454816+0800 ios_thread[36787:337646] 當前線程4
2021-03-24 16:59:01.455557+0800 ios_thread[36787:337720] 完成part1下載 -> <NSThread: 0x6000008c0040>{number = 5, name = (null)}
2021-03-24 16:59:03.455153+0800 ios_thread[36787:337719] 完成part2下載 -> <NSThread: 0x6000008b2800>{number = 3, name = (null)}
2021-03-24 16:59:03.455575+0800 ios_thread[36787:337719] 合并part1和part2 -> <NSThread: 0x6000008b2800>{number = 3, name = (null)}
2021-03-24 16:59:05.460777+0800 ios_thread[36787:337719] 合并part1和part2合并完成 -> <NSThread: 0x6000008b2800>{number = 3, name = (null)}

總結(jié)說明:
從上面運行結(jié)果可以看出,異步柵欄函數(shù)不會阻塞當前線程,也就是說UI界面并不會被卡死
特別注意:
1.全局隊列對柵欄函數(shù)是不生效的,必須是自己創(chuàng)建的并發(fā)隊列。
2.所有任務(包括柵欄函數(shù)添加的任務)都必須在同一個派發(fā)隊列中,否則柵欄函數(shù)不生效。使用第三方網(wǎng)絡框架(比如AFNetworking)進行網(wǎng)絡請求時使用柵欄函數(shù)無效的正是因為這個原因?qū)е隆?/em>

4.3 GCD 延時執(zhí)行方法:dispatch_after

案例需求: 在指定時間(例如3秒)之后執(zhí)行某個任務。
可以用 GCD 的dispatch_after函數(shù)來實現(xiàn)。

- (void)dispatch_after {
    // 打印當前線程
    NSLog(@"currentThread---%@",[NSThread currentThread]);
    NSLog(@"asyncMain---begin");

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 2.0秒后異步追加任務代碼到主隊列,并開始執(zhí)行
        // 打印當前線程
        NSLog(@"after---%@",[NSThread currentThread]);
    });
}
輸出結(jié)果:2021-03-24 17:20:32.263404+0800 ios_thread[36954:350438] currentThread---<NSThread: 0x60000034c200>{number = 1, name = main}
2021-03-24 17:20:32.263559+0800 ios_thread[36954:350438] asyncMain---begin
2021-03-24 17:20:34.327446+0800 ios_thread[36954:350438] after---<NSThread: 0x60000034c200>{number = 1, name = main}

總結(jié):
需要注意的是:dispatch_after函數(shù)并不是在指定時間之后才開始執(zhí)行處理,而是在指定時間之后將任務追加到主隊列中。嚴格來說,這個時間并不是絕對準確的,但想要大致延遲執(zhí)行任務,dispatch_after函數(shù)是很有效的。

4.4GCD 一次性代碼(只執(zhí)行一次):dispatch_once

案例分析:
我們在創(chuàng)建單例、或者有整個程序運行過程中只執(zhí)行一次的代碼時,我們就用到了 GCD 的 dispatch_once 函數(shù)。使用dispatch_once 函數(shù)能保證某段代碼在程序運行過程中只被執(zhí)行1次,并且即使在多線程的環(huán)境下,dispatch_once也可以保證線程安全。

- (void)once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只執(zhí)行1次的代碼(這里面默認是線程安全的)
    });
}

4.5 GCD 快速迭代方法:dispatch_apply

dispatch_apply類似for循環(huán),會在指定的隊列中多次執(zhí)行任務。
dispatch_apply按照指定的次數(shù)將指定的任務追加到指定的隊列中,并等待全部隊列執(zhí)行結(jié)束。
我們可以利用異步隊列同時遍歷。比如說遍歷 0~5 這6個數(shù)字,for 循環(huán)的做法是每次取出一個元素,逐個遍歷。dispatch_apply可以同時遍歷多個數(shù)字。

- (void)apply {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    NSLog(@"apply---begin");
    dispatch_apply(5, queue, ^(size_t index) {
        NSLog(@"%zd---%@",index, [NSThread currentThread]);
    });
    NSLog(@"apply---end");
}
輸入結(jié)果:
2021-03-24 17:32:42.924078+0800 ios_thread[37098:358812] apply---begin
2021-03-24 17:32:42.924317+0800 ios_thread[37098:358812] 0---<NSThread: 0x600003fcc000>{number = 1, name = main}
2021-03-24 17:32:42.924320+0800 ios_thread[37098:358915] 2---<NSThread: 0x600003ffdf40>{number = 3, name = (null)}
2021-03-24 17:32:42.924326+0800 ios_thread[37098:358913] 1---<NSThread: 0x600003ffb580>{number = 5, name = (null)}
2021-03-24 17:32:42.924369+0800 ios_thread[37098:358916] 3---<NSThread: 0x600003ffe400>{number = 4, name = (null)}
2021-03-24 17:32:42.924475+0800 ios_thread[37098:358812] 4---<NSThread: 0x600003fcc000>{number = 1, name = main}
2021-03-24 17:32:42.924563+0800 ios_thread[37098:358812] apply---end

總結(jié):
從dispatch_apply相關(guān)代碼執(zhí)行結(jié)果中可以看出: 0~5 打印順序不定,最后打印了 apply---end。
因為是在并發(fā)隊列中異步隊執(zhí)行任務,所以各個任務的執(zhí)行時間長短不定,最后結(jié)束順序也不定。但是apply---end一定在最后執(zhí)行。這是因為dispatch_apply函數(shù)會等待全部任務執(zhí)行完畢。

4.6 GCD 的隊列組:dispatch_group

現(xiàn)需求如下:
某個界面需要請求banner信息和產(chǎn)品列表信息,等這兩個接口的數(shù)據(jù)都返回后再回到主線程刷新UI。
我就直接上代碼了

-(void)grop{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("com.thread.task", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"當前線程1");
    dispatch_group_async(group, queue, ^{
        NSLog(@"開始請求-輪播圖-數(shù)據(jù) -> %@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:2];
        NSLog(@"獲取到-輪播圖-數(shù)據(jù) -> %@",[NSThread currentThread]);
    });
    NSLog(@"當前線程2");
    dispatch_group_async(group, queue, ^{
        NSLog(@"開始請求-列表-數(shù)據(jù) -> %@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:3];
        NSLog(@"獲取到-列表-數(shù)據(jù) -> %@",[NSThread currentThread]);
    });
    NSLog(@"當前線程3");
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"回到主線程刷新列表");
        [NSThread sleepForTimeInterval:1];
    });
    NSLog(@"當前線程4");
}
輸出結(jié)果:
2021-03-24 22:58:40.009873+0800 ios_thread[1117:29526] 當前線程1
2021-03-24 22:58:40.010103+0800 ios_thread[1117:29526] 當前線程2
2021-03-24 22:58:40.010218+0800 ios_thread[1117:29607] 開始請求-輪播圖-數(shù)據(jù) -> <NSThread: 0x60000361bbc0>{number = 4, name = (null)}
2021-03-24 22:58:40.010248+0800 ios_thread[1117:29526] 當前線程3
2021-03-24 22:58:40.010303+0800 ios_thread[1117:29611] 開始請求-列表-數(shù)據(jù) -> <NSThread: 0x600003667040>{number = 6, name = (null)}
2021-03-24 22:58:40.010351+0800 ios_thread[1117:29526] 當前線程4
2021-03-24 22:58:42.015402+0800 ios_thread[1117:29607] 獲取到-輪播圖-數(shù)據(jù) -> <NSThread: 0x60000361bbc0>{number = 4, name = (null)}
2021-03-24 22:58:43.013025+0800 ios_thread[1117:29611] 獲取到-列表-數(shù)據(jù) -> <NSThread: 0x600003667040>{number = 6, name = (null)}
2021-03-24 22:58:43.013323+0800 ios_thread[1117:29526] 回到主線程刷新列表

說明:dispatch_group_notify監(jiān)聽任務組并不會阻塞當前線程。

備注:(以下不做介紹,了解就行)
1.dispatch_group_wait(暫停當前線程(阻塞當前線程),等待指定的 group 中的任務執(zhí)行完成后,才會往下繼續(xù)執(zhí)行)
2.dispatch_group_enter* 標志著一個任務追加到 group,執(zhí)行一次,相當于 group 中未執(zhí)行完畢任務數(shù)+1*
3.dispatch_group_leave* 標志著一個任務離開了 group,執(zhí)行一次,相當于 group 中未執(zhí)行完畢任務數(shù)-1*

4.7 GCD 信號量:dispatch_semaphore

GCD 中的信號量是指 Dispatch Semaphore,是持有計數(shù)的信號。類似于過高速路收費站的欄桿。可以通過時,打開欄桿,不可以通過時,關(guān)閉欄桿。在 Dispatch Semaphore中,使用計數(shù)來完成這個功能,計數(shù)為0時等待,不可通過。計數(shù)為1或大于1時,計數(shù)減1且不等待,可通過。Dispatch Semaphore 提供了三個函數(shù)。

1.dispatch_semaphore_create:創(chuàng)建一個Semaphore并初始化信號的總量
2.dispatch_semaphore_signal:發(fā)送一個信號,讓信號總量加1
3.* dispatch_semaphore_wait*可以使總信號量減1,當信號總量為0時就會一直等待(阻塞所在線程),否則就可以正常執(zhí)行

注意:信號量的使用前提是:想清楚你需要處理哪個線程等待(阻塞),又要哪個線程繼續(xù)執(zhí)行,然后使用信號量

Dispatch Semaphore 在實際開發(fā)中主要用于:
1.保持線程同步,將異步執(zhí)行任務轉(zhuǎn)換為同步執(zhí)行任務
2.保證線程安全,為線程加鎖

4.7.1 Dispatch Semaphore 線程同步

需求分析:
我們在開發(fā)中,會遇到這樣的需求:異步執(zhí)行耗時任務,并使用異步執(zhí)行的結(jié)果進行一些額外的操作。換句話說,相當于,將將異步執(zhí)行任務轉(zhuǎn)換為同步執(zhí)行任務。比如說:AFNetworking 中 AFURLSessionManager.m 里面的 tasksForKeyPath: 方法。通過引入信號量的方式,等待異步執(zhí)行任務結(jié)果,獲取到 tasks,然后再返回該 tasks。

#pragma mark 線程同步
/**
 * Dispatch Semaphore 實現(xiàn)線程同步,將異步執(zhí)行任務轉(zhuǎn)換為同步執(zhí)行任務。
 */
- (void)semaphoreSync {

    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"semaphore---begin");

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任務1
        [NSThread sleepForTimeInterval:2];              // 模擬耗時操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印當前線程

        number = 100;

        dispatch_semaphore_signal(semaphore);
    });

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %d",number);
}
輸出結(jié)果:
2021-03-24 23:28:57.053051+0800 ios_thread[1277:45210] currentThread---<NSThread: 0x600002cf4180>{number = 1, name = main}
2021-03-24 23:28:57.053201+0800 ios_thread[1277:45210] semaphore---begin
2021-03-24 23:28:59.057845+0800 ios_thread[1277:45365] 1---<NSThread: 0x600002cb6240>{number = 4, name = (null)}
2021-03-24 23:28:59.058181+0800 ios_thread[1277:45210] semaphore---end,number = 100

代碼總結(jié):
從 Dispatch Semaphore 實現(xiàn)線程同步的代碼可以看到:
semaphore---end 是在執(zhí)行完 number = 100; 之后才打印的。而且輸出結(jié)果 number 為 100。這是因為異步執(zhí)行不會做任何等待,可以繼續(xù)執(zhí)行任務。異步執(zhí)行將任務1追加到隊列之后,不做等待,接著執(zhí)行dispatch_semaphore_wait方法。此時 semaphore == 0,當前線程進入等待狀態(tài)。然后,異步任務1開始執(zhí)行。任務1執(zhí)行到dispatch_semaphore_signal之后,總信號量,此時 semaphore == 1,dispatch_semaphore_wait方法使總信號量減1,正在被阻塞的線程(主線程)恢復繼續(xù)執(zhí)行。最后打印semaphore---end,number = 100。這樣就實現(xiàn)了線程同步,將異步執(zhí)行任務轉(zhuǎn)換為同步執(zhí)行任務。

4.7.2Dispatch Semaphore 線程安全和線程同步(為線程加鎖)

線程安全
1.如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結(jié)果和單線程運行的結(jié)果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
2.若每個線程中對全局變量、靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執(zhí)行寫操作(更改變量),一般都需要考慮線程同步,否則的話就可能影響線程安全
線程同步
可理解為線程 A 和 線程 B 一起配合,A 執(zhí)行到一定程度時要依靠線程 B 的某個結(jié)果,于是停下來,示意 B 運行;B 依言執(zhí)行,再將結(jié)果給 A;A 再繼續(xù)操作---------------舉個簡單例子就是:兩個人在一起聊天。兩個人不能同時說話,避免聽不清(操作沖突)。等一個人說完(一個線程結(jié)束操作),另一個再說(另一個線程再開始操作)

案例場景:
總共有50張火車票,有兩個售賣火車票的窗口,一個是北京火車票售賣窗口,另一個是上?;疖嚻笔圪u窗口。兩個窗口同時售賣火車票,賣完為止。
下面我們通過非線程安全和線程安全兩段代碼加以分析比較
1.非線程安全(不使用 semaphore)

/**
 * 非線程安全:不使用 semaphore
 * 初始化火車票數(shù)量、賣票窗口(非線程安全)、并開始賣票
 */
- (void)initTicketStatusNotSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"semaphore---begin");

    self.ticketSurplusCount = 30;

    // queue1 代表北京火車票售賣窗口
    dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上?;疖嚻笔圪u窗口
    dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);

    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketNotSafe];
    });

    dispatch_async(queue2, ^{
        [weakSelf saleTicketNotSafe];
    });
}

/**
 * 售賣火車票(非線程安全)
 */
- (void)saleTicketNotSafe {
    while (1) {

        if (self.ticketSurplusCount > 0) {  //如果還有票,繼續(xù)售賣
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票數(shù):%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { //如果已賣完,關(guān)閉售票窗口
            NSLog(@"所有火車票均已售完");
            break;
        }

    }
}
輸出結(jié)果(部分):
2021-03-24 23:55:23.408177+0800 ios_thread[1439:59058] currentThread---<NSThread: 0x600003dd0880>{number = 1, name = main}
2021-03-24 23:55:23.408325+0800 ios_thread[1439:59058] semaphore---begin
2021-03-24 23:55:23.408621+0800 ios_thread[1439:59130] 剩余票數(shù):28 窗口:<NSThread: 0x600003d92840>{number = 4, name = (null)}
2021-03-24 23:55:23.408622+0800 ios_thread[1439:59133] 剩余票數(shù):29 窗口:<NSThread: 0x600003def240>{number = 6, name = (null)}
2021-03-24 23:55:23.613155+0800 ios_thread[1439:59133] 剩余票數(shù):27 窗口:<NSThread: 0x600003def240>{number = 6, name = (null)}
2021-03-24 23:55:23.613358+0800 ios_thread[1439:59130] 剩余票數(shù):26 窗口:<NSThread: 0x600003d92840>{number = 4, name = (null)}
2021-03-24 23:55:23.818865+0800 ios_thread[1439:59133] 剩余票數(shù):25 窗口:<NSThread: 0x600003def240>{number = 6, name = (null)}
2021-03-24 23:55:23.819457+0800 ios_thread[1439:59130] 剩余票數(shù):24 窗口:<NSThread: 0x600003d92840>{number = 4, name = (null)}
2021-03-24 23:55:24.023485+0800 ios_thread[1439:59133] 剩余票數(shù):23 窗口:<NSThread: 0x600003def240>{number = 6, name = (null)}
2021-03-24 23:55:24.026350+0800 ios_thread[1439:59130] 剩余票數(shù):22 窗口:<NSThread: 0x600003d92840>{number = 4, name = (null)}
.............
021-03-24 23:55:26.260378+0800 ios_thread[1439:59133] 剩余票數(shù):3 窗口:<NSThread: 0x600003def240>{number = 6, name = (null)}
2021-03-24 23:55:26.260394+0800 ios_thread[1439:59130] 剩余票數(shù):2 窗口:<NSThread: 0x600003d92840>{number = 4, name = (null)}
2021-03-24 23:55:26.465636+0800 ios_thread[1439:59130] 剩余票數(shù):1 窗口:<NSThread: 0x600003d92840>{number = 4, name = (null)}
2021-03-24 23:55:26.465636+0800 ios_thread[1439:59133] 剩余票數(shù):1 窗口:<NSThread: 0x600003def240>{number = 6, name = (null)}
2021-03-24 23:55:26.667177+0800 ios_thread[1439:59130] 所有火車票均已售完
2021-03-24 23:55:26.667221+0800 ios_thread[1439:59133] 剩余票數(shù):0 窗口:<NSThread: 0x600003def240>{number = 6, name = (null)}
2021-03-24 23:55:26.870117+0800 ios_thread[1439:59133] 所有火車票均已售完

總結(jié):
可以看到在不考慮線程安全,不使用 semaphore 的情況下,得到票數(shù)是錯亂的,這樣顯然不符合我們的需求,所以我們需要考慮線程安全問題。

2. 線程安全(使用 semaphore 加鎖)

#pragma mark 線程安全:使用 semaphore
/**
 * 線程安全:使用 semaphore 加鎖
 * 初始化火車票數(shù)量、賣票窗口(線程安全)、并開始賣票
 */
- (void)initTicketStatusSave {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"semaphore---begin");

    self.semaphoreLock = dispatch_semaphore_create(1);

    self.ticketSurplusCount = 50;

    // queue1 代表北京火車票售賣窗口
    dispatch_queue_t queue1 = dispatch_queue_create("net.thread.testQueue1", DISPATCH_QUEUE_SERIAL);
    // queue2 代表上?;疖嚻笔圪u窗口
    dispatch_queue_t queue2 = dispatch_queue_create("net.thread.testQueue2", DISPATCH_QUEUE_SERIAL);

    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketSafe];
    });

    dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

/**
 * 售賣火車票(線程安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 相當于加鎖
        dispatch_semaphore_wait(self.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(@"所有火車票均已售完");

            // 相當于解鎖
            dispatch_semaphore_signal(self.semaphoreLock);
            break;
        }

        // 相當于解鎖
        dispatch_semaphore_signal(self.semaphoreLock);
    }
}
輸出結(jié)果:
2021-03-25 00:01:39.161931+0800 ios_thread[1487:63043] currentThread---<NSThread: 0x6000007d8200>{number = 1, name = main}
2021-03-25 00:01:39.162083+0800 ios_thread[1487:63043] semaphore---begin
2021-03-25 00:01:39.162360+0800 ios_thread[1487:63102] 剩余票數(shù):49 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:39.367677+0800 ios_thread[1487:63100] 剩余票數(shù):48 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:39.570486+0800 ios_thread[1487:63102] 剩余票數(shù):47 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:39.774759+0800 ios_thread[1487:63100] 剩余票數(shù):46 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:39.978508+0800 ios_thread[1487:63102] 剩余票數(shù):45 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:40.181938+0800 ios_thread[1487:63100] 剩余票數(shù):44 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:40.384955+0800 ios_thread[1487:63102] 剩余票數(shù):43 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:40.590373+0800 ios_thread[1487:63100] 剩余票數(shù):42 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:40.795111+0800 ios_thread[1487:63102] 剩余票數(shù):41 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:40.997988+0800 ios_thread[1487:63100] 剩余票數(shù):40 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:41.201602+0800 ios_thread[1487:63102] 剩余票數(shù):39 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:41.403678+0800 ios_thread[1487:63100] 剩余票數(shù):38 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:41.606723+0800 ios_thread[1487:63102] 剩余票數(shù):37 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:41.811657+0800 ios_thread[1487:63100] 剩余票數(shù):36 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:42.012513+0800 ios_thread[1487:63102] 剩余票數(shù):35 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:42.213897+0800 ios_thread[1487:63100] 剩余票數(shù):34 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:42.417450+0800 ios_thread[1487:63102] 剩余票數(shù):33 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:42.617871+0800 ios_thread[1487:63100] 剩余票數(shù):32 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:42.823405+0800 ios_thread[1487:63102] 剩余票數(shù):31 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:43.028284+0800 ios_thread[1487:63100] 剩余票數(shù):30 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:43.233293+0800 ios_thread[1487:63102] 剩余票數(shù):29 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:43.438096+0800 ios_thread[1487:63100] 剩余票數(shù):28 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:43.639584+0800 ios_thread[1487:63102] 剩余票數(shù):27 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:43.845104+0800 ios_thread[1487:63100] 剩余票數(shù):26 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:44.047000+0800 ios_thread[1487:63102] 剩余票數(shù):25 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:44.252575+0800 ios_thread[1487:63100] 剩余票數(shù):24 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:44.455481+0800 ios_thread[1487:63102] 剩余票數(shù):23 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:44.661001+0800 ios_thread[1487:63100] 剩余票數(shù):22 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:44.865401+0800 ios_thread[1487:63102] 剩余票數(shù):21 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:45.067629+0800 ios_thread[1487:63100] 剩余票數(shù):20 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:45.273211+0800 ios_thread[1487:63102] 剩余票數(shù):19 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:45.478097+0800 ios_thread[1487:63100] 剩余票數(shù):18 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:45.682950+0800 ios_thread[1487:63102] 剩余票數(shù):17 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:45.885351+0800 ios_thread[1487:63100] 剩余票數(shù):16 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:46.088975+0800 ios_thread[1487:63102] 剩余票數(shù):15 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:46.294408+0800 ios_thread[1487:63100] 剩余票數(shù):14 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:46.497893+0800 ios_thread[1487:63102] 剩余票數(shù):13 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:46.701095+0800 ios_thread[1487:63100] 剩余票數(shù):12 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:46.901881+0800 ios_thread[1487:63102] 剩余票數(shù):11 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:47.106582+0800 ios_thread[1487:63100] 剩余票數(shù):10 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:47.311287+0800 ios_thread[1487:63102] 剩余票數(shù):9 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:47.513746+0800 ios_thread[1487:63100] 剩余票數(shù):8 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:47.714846+0800 ios_thread[1487:63102] 剩余票數(shù):7 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:47.918548+0800 ios_thread[1487:63100] 剩余票數(shù):6 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:48.121033+0800 ios_thread[1487:63102] 剩余票數(shù):5 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:48.322008+0800 ios_thread[1487:63100] 剩余票數(shù):4 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:48.527011+0800 ios_thread[1487:63102] 剩余票數(shù):3 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:48.728485+0800 ios_thread[1487:63100] 剩余票數(shù):2 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:48.933886+0800 ios_thread[1487:63102] 剩余票數(shù):1 窗口:<NSThread: 0x600000798240>{number = 3, name = (null)}
2021-03-25 00:01:49.139563+0800 ios_thread[1487:63100] 剩余票數(shù):0 窗口:<NSThread: 0x6000007913c0>{number = 5, name = (null)}
2021-03-25 00:01:49.340256+0800 ios_thread[1487:63102] 所有火車票均已售完
2021-03-25 00:01:49.340603+0800 ios_thread[1487:63100] 所有火車票均已售完

總結(jié)分析
可以看出,在考慮了線程安全的情況下,使用 dispatch_semaphore 機制之后,得到的票數(shù)是正確的,沒有出現(xiàn)混亂的情況。我們也就解決了多個線程同步的問題。

4.8定時器的使用

話不多說了直接看代碼吧

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.codeButton];
    [self startCountDownAction];
   
}

- (void)startCountDownAction{
    NSLog(@"倒計時功能");
    //開始倒計時
    __block NSInteger time = 59; //倒計時時間
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    dispatch_source_set_timer(_timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0); //每秒執(zhí)行
    
    dispatch_source_set_event_handler(_timer, ^{
        
        if(time <= 0){ //倒計時結(jié)束,關(guān)閉
            
            dispatch_source_cancel(_timer);
            dispatch_async(dispatch_get_main_queue(), ^{
                
                //設(shè)置按鈕的樣式
                [self.codeButton setTitle:@"重新發(fā)送" forState:UIControlStateNormal];
                [self.codeButton setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal];
                self.codeButton.userInteractionEnabled = YES;
            });
            
        }else{
            
            int seconds = time % 60;
            dispatch_async(dispatch_get_main_queue(), ^{
                
                //設(shè)置按鈕顯示讀秒效果
                [self.codeButton setTitle:[NSString stringWithFormat:@"%.2ds后重新發(fā)送", seconds] forState:UIControlStateNormal];
                [self.codeButton setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
                self.codeButton.userInteractionEnabled = NO;
            });
            time--;
        }
    });
    dispatch_resume(_timer);
    
}

-(UIButton *)codeButton{
    if(!_codeButton){
        _codeButton = [UIButton buttonWithbuttonType:UIButtonTypeCustom andText:@"獲取驗證碼" andTextAlignment:UIControlContentHorizontalAlignmentCenter andTextColor:[UIColor whiteColor] andTextFont:13 andBackgroungColor:[UIColor grayColor] cornerRadiusSize:2 borderWidth:1 borderColor:[UIColor blackColor] abscissa:SCREEN_WIDTH/2-70 ordinate:SCREEN_HEIGHT/2-25 width:140 height:50];
        [_codeButton addTarget:self action:@selector(clickCountDownAction:) forControlEvents:UIControlEventTouchUpOutside];
    }
    return _codeButton;
}
-(void)clickCountDownAction:(UIButton *)sender{
    [self startCountDownAction];
}

最后總結(jié)
這是關(guān)于GCD內(nèi)容的一些總結(jié),希望對需要了解GCD的小伙伴一些幫助,一些內(nèi)容還沒添加,后續(xù)會慢慢更新此簡書內(nèi)容

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

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

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