一,GCD简介
GCD是Apple开发的一个多线程的较新的解决方案。它主要用于优化应用程序以支持多核处理器以及其他对称处理系统。它是一个在线程池模式的基础上执行的并发任务。
为什么要使用GCD?
- GCD!可用于多核的并行运算
- GCD会自动利用更多的CPU内核(比如双核,四核)
- GCD会自动管理线程的生命周期(创建线程,调度任务,销毁线程)
- 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
二,GCD任务和队列
- 任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。
- 队列:这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的 线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
- 同步执行(
sync
):
同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
只能在当前线程中执行任务,不具备开启新线程的能力。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1 -- %@", [NSThread currentThread]);
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"2 -- %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"3 -- %@",[NSThread currentThread]);
});
NSLog(@"4 -- %@", [NSThread currentThread]);
// Do any additional setup after loading the view.
}
输出
2024-05-29 20:53:55.168850+0800 GCD详解[69722:2675459] 1 -- <_NSMainThread: 0x600000b70100>{number = 1, name = main}
2024-05-29 20:53:55.168911+0800 GCD详解[69722:2675459] 2 -- <_NSMainThread: 0x600000b70100>{number = 1, name = main}
2024-05-29 20:53:55.168958+0800 GCD详解[69722:2675459] 3 -- <_NSMainThread: 0x600000b70100>{number = 1, name = main}
2024-05-29 20:53:55.169010+0800 GCD详解[69722:2675459] 4 -- <_NSMainThread: 0x600000b70100>{number = 1, name = m
- 异步执行(
async
):
异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
可以在新的线程中执行任务,具备开启新线程的能力。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1 -- %@", [NSThread currentThread]);
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"2 -- %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"3 -- %@",[NSThread currentThread]);
});
NSLog(@"4 -- %@", [NSThread currentThread]);
// Do any additional setup after loading the view.
}
输出
2024-05-29 21:02:04.468604+0800 GCD详解[69868:2682284] 1 -- <_NSMainThread: 0x60000376c540>{number = 1, name = main}
2024-05-29 21:02:04.468673+0800 GCD详解[69868:2682284] 4 -- <_NSMainThread: 0x60000376c540>{number = 1, name = main}
2024-05-29 21:02:04.468678+0800 GCD详解[69868:2682427] 2 -- <NSThread: 0x600003765580>{number = 6, name = (null)}
2024-05-29 21:02:04.468735+0800 GCD详解[69868:2682427] 3 -- <NSThread: 0x600003765580>{number = 6, name = (null)}
- 串行队列(
Serial Dispatch Queue
):
每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
- 并发队列(
Concurrent Dispatch Queue
):
可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
⚠️注意:并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1 -- %@", [NSThread currentThread]);
// 并行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
NSLog(@"2 -- %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"3 -- %@",[NSThread currentThread]);
});
NSLog(@"4 -- %@", [NSThread currentThread]);
// Do any additional setup after loading the view.
}
输出
024-05-29 21:03:45.087904+0800 GCD详解[69913:2684176] 1 -- <_NSMainThread: 0x600001d0c400>{number = 1, name = main}
2024-05-29 21:03:45.087962+0800 GCD详解[69913:2684176] 2 -- <_NSMainThread: 0x600001d0c400>{number = 1, name = main}
2024-05-29 21:03:45.087992+0800 GCD详解[69913:2684176] 3 -- <_NSMainThread: 0x600001d0c400>{number = 1, name = main}
2024-05-29 21:03:45.088034+0800 GCD详解[69913:2684176] 4 -- <_NSMainThread: 0x600001d0c400>{number = 1, name = main}
三, GCD 的使用步骤
GCD 的使用步骤只有两步:
1,创建一个队列(串行队列或并发队列);
2,将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)。
3.1 队列的创建方法 / 获取方法
- 可以使用 dispatch_queue_create 方法来创建队列。该方法需要传入两个参数:
第一个参数表示队列的唯一标识符,用于 DEBUG,可为空。队列的名称推荐使用应用程序 ID 这种逆序全程域名。
第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL
表示串行队列,DISPATCH_QUEUE_CONCURRENT
表示并发队列。
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
- 对于串行队列,GCD 默认提供了:主队列(Main Dispatch Queue)。
1.所有放在主队列中的任务,都会放到主线程中执行。
2.可使用 dispatch_get_main_queue() 方法获得主队列。
⚠️注意:主队列其实并不特殊。 主队列的实质上就是一个普通的串行队列,只是因为默认情况下,平常写的代码是放在主队列中的,然后主队列中的代码,又都会放到主线程中去执行,所以才造成了主队列特殊的现象。
dispatch_queue_t queue = dispatch_get_main_queue();
- 对于并发队列,GCD 默认提供了 全局并发队列(Global Dispatch Queue)。
可以使用 dispatch_get_global_queue 方法来获取全局并发队列。需要传入两个参数。第一个参数表示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT
。第二个参数暂时没用,用 0 即可。
// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
3.2 任务的创建方法
GCD 提供了同步执行任务的创建方法 dispatch_sync 和异步执行任务创建方法 dispatch_async。
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 这里放异步执行任务代码
});
虽然使用 GCD 只需两步,但是既然我们有两种队列(串行队列 / 并发队列),两种任务执行方式(同步执行 / 异步执行),那么我们就有了四种不同的组合方式。这四种不同的组合方式是:
- 同步执行 + 并发队列
- 异步执行 + 并发队列
- 同步执行 + 串行队列
- 异步执行 + 串行队列
实际上,刚才还说了两种默认队列:全局并发队列、主队列。全局并发队列可以作为普通并发队列来使用。但是当前代码默认放在主队列中,所以主队列很有必要专门来研究一下,这样就有六种不同的组合方式了。
- 同步执行 + 主队列
- 异步执行 + 主队列
3.3任务和队列不同组合方式的区别
3.3.1 同步执行 + 并发队列
在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
NSLog(@"1 -- %@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2 -- %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"3 -- %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"4 -- %@",[NSThread currentThread]);
});
NSLog(@"5 -- %@", [NSThread currentThread]);
输出结果
2024-05-29 21:30:27.212376+0800 GCD详解[70435:2704510] 1 -- <_NSMainThread: 0x6000022584c0>{number = 1, name = main}
2024-05-29 21:30:27.212433+0800 GCD详解[70435:2704510] 2 -- <_NSMainThread: 0x6000022584c0>{number = 1, name = main}
2024-05-29 21:30:27.212479+0800 GCD详解[70435:2704510] 3 -- <_NSMainThread: 0x6000022584c0>{number = 1, name = main}
2024-05-29 21:30:27.212530+0800 GCD详解[70435:2704510] 4 -- <_NSMainThread: 0x6000022584c0>{number = 1, name = main}
2024-05-29 21:30:27.212569+0800 GCD详解[70435:2704510] 5 -- <_NSMainThread: 0x6000022584c0>{number = 1, name = main}
从 同步执行 + 并发队列 中可看到:
- 所有任务都是在当前线程(主线程)中执行的,没有开启新的线程(同步执行不具备开启新线程的能力)。
- 所有任务都在打印的任务1和任务5之间执行的(同步任务 需要等待队列的任务执行结束)。
- 任务按顺序执行的。按顺序执行的原因:虽然 并发队列 可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务 不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务 需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。
3.3.2 异步执行 + 并发队列
- 可以开启多个线程,任务交替(同时)执行。
NSLog(@"1 -- %@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2 -- %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"3 -- %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"4 -- %@",[NSThread currentThread]);
});
NSLog(@"5 -- %@", [NSThread currentThread]);
输出结果
024-05-29 21:36:49.626519+0800 GCD详解[70567:2709956] 1 -- <_NSMainThread: 0x600003d9c040>{number = 1, name = main}
2024-05-29 21:36:49.626591+0800 GCD详解[70567:2709956] 5 -- <_NSMainThread: 0x600003d9c040>{number = 1, name = main}
2024-05-29 21:36:51.631683+0800 GCD详解[70567:2710054] 2 -- <NSThread: 0x600003dd4c00>{number = 4, name = (null)}
2024-05-29 21:36:51.631683+0800 GCD详解[70567:2710050] 4 -- <NSThread: 0x600003dd4e00>{number = 5, name = (null)}
2024-05-29 21:36:51.631683+0800 GCD详解[70567:2710053] 3 -- <NSThread: 0x600003d9af40>{number = 3, name = (null)}
在 异步执行 + 并发队列 中可以看出:
- 除了当前线程(主线程),系统又开启了 3 个线程,并且任务是交替/同时执行的。(异步执行 具备开启新线程的能力。且 并发队列 可开启多个线程,同时执行多个任务)。
- 所有任务是在打印的任务1 和任务5之后才执行的。说明当前线程没有等待,而是直接开启了新线程,在新线程中执行任务(异步执行 不做等待,可以继续执行任务)。
3.3.3 同步执行 + 串行队列
- 不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
NSLog(@"1 -- %@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2 -- %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"3 -- %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"4 -- %@",[NSThread currentThread]);
});
NSLog(@"5 -- %@", [NSThread currentThread]);
输出结果
2024-05-29 21:40:14.290797+0800 GCD详解[70741:2714482] 1 -- <_NSMainThread: 0x60000362c400>{number = 1, name = main}
2024-05-29 21:40:16.291817+0800 GCD详解[70741:2714482] 2 -- <_NSMainThread: 0x60000362c400>{number = 1, name = main}
2024-05-29 21:40:18.292859+0800 GCD详解[70741:2714482] 3 -- <_NSMainThread: 0x60000362c400>{number = 1, name = main}
2024-05-29 21:40:20.293898+0800 GCD详解[70741:2714482] 4 -- <_NSMainThread: 0x60000362c400>{number = 1, name = main}
2024-05-29 21:40:20.293968+0800 GCD详解[70741:2714482] 5 -- <_NSMainThread: 0x60000362c400>{number = 1, name = main}
在 同步执行 + 串行队列 可以看到:
- 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(同步执行 不具备开启新线程的能力)。
- 所有任务都在打印的任务1和任务5之间执行(同步任务 需要等待队列的任务执行结束)。
- 任务是按顺序执行的(串行队列 每次只有一个任务被执行,任务一个接一个按顺序执行)。
3.3.4异步执行 + 串行队列
- 会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务
NSLog(@"1 -- %@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2 -- %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"3 -- %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"4 -- %@",[NSThread currentThread]);
});
NSLog(@"5 -- %@", [NSThread currentThread]);
输出结果:
2024-05-29 21:43:18.017856+0800 GCD详解[70835:2717791] 1 -- <_NSMainThread: 0x6000037b8000>{number = 1, name = main}
2024-05-29 21:43:18.017922+0800 GCD详解[70835:2717791] 5 -- <_NSMainThread: 0x6000037b8000>{number = 1, name = main}
2024-05-29 21:43:20.023459+0800 GCD详解[70835:2717899] 2 -- <NSThread: 0x6000037f0540>{number = 5, name = (null)}
2024-05-29 21:43:22.028921+0800 GCD详解[70835:2717899] 3 -- <NSThread: 0x6000037f0540>{number = 5, name = (null)}
2024-05-29 21:43:24.033872+0800 GCD详解[70835:2717899] 4 -- <NSThread: 0x6000037f0540>{number = 5, name = (null)}
在 异步执行 + 串行队列 可以看到:
- 开启了一条新线程(异步执行 具备开启新线程的能力,串行队列 只开启一个线程)。
- 所有任务是在打印的任务1和任务5之后才开始执行的(异步执行 不会做任何等待,可以继续执行任务)。
- 任务是按顺序执行的(串行队列 每次只有一个任务被执行,任务一个接一个按顺序执行)。
3.3.5同步执行 + 主队列
- 同步执行 + 主队列 在不同线程中调用结果也是不一样,在主线程中调用会发生死锁问题,而在其他线程中调用则不会。
- 主队列:
默认情况,平常所写的代码是直接放在主队列中的
所有放在主队列中的任务,都会在主线程中执行
可使用dispatch_get_main_queue() 获得主队列
3.3.5.1 在主线程中调用 同步执行 + 主队列
- 互相等待卡住不可行
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1 -- %@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2 -- %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"3 -- %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"4 -- %@",[NSThread currentThread]);
});
NSLog(@"5 -- %@", [NSThread currentThread]);
}
这样会直接崩溃
这是因为我们在主线程中执行任务5方法,相当于把任务5放到了主线程的队列中。而 同步执行 会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把 任务2追加到主队列中,任务2就在等待主线程处理完5任务。而任务5需要等待任务1 所在队列的所有任务执行完毕,才能接着执行。这样相互等待,就造成了死锁。
3.3.5.2在其他线程中调用『同步执行 + 主队列』
- 不会开启新线程,执行完一个任务,再执行下一个任务
- (void)viewDidLoad {
[super viewDidLoad];
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
}
- (void)syncMain {
NSLog(@"1 -- %@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2 -- %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"3 -- %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"4 -- %@",[NSThread currentThread]);
});
NSLog(@"5 -- %@", [NSThread currentThread]);
}
输出结果:
2024-05-29 21:59:50.168185+0800 GCD详解[71147:2730839] 1 -- <NSThread: 0x6000027bc700>{number = 7, name = (null)}
2024-05-29 21:59:52.197328+0800 GCD详解[71147:2730626] 2 -- <_NSMainThread: 0x6000027d0000>{number = 1, name = main}
2024-05-29 21:59:54.203257+0800 GCD详解[71147:2730626] 3 -- <_NSMainThread: 0x6000027d0000>{number = 1, name = main}
2024-05-29 21:59:56.210364+0800 GCD详解[71147:2730626] 4 -- <_NSMainThread: 0x6000027d0000>{number = 1, name = main}
2024-05-29 21:59:56.210959+0800 GCD详解[71147:2730839] 5 -- <NSThread: 0x6000027bc700>{number = 7, name = (null)}
在其他线程中使用 同步执行 + 主队列 可看到:
- 所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行)。
- 所有任务都在打印的任务1和任务5之间执行(同步任务 需要等待队列的任务执行结束)。
- 任务是按顺序执行的(主队列是 串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
这里任务2不用等待其他线程的任务5执行完,不会造成死锁。
3.3.6 异步执行 + 主队列
- 只在主线程执行任务,执行完一个任务,在执行下一个任务
NSLog(@"1 -- %@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2 -- %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"3 -- %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"4 -- %@",[NSThread currentThread]);
});
NSLog(@"5 -- %@", [NSThread currentThread]);
输出结果:
2024-05-30 19:47:21.114444+0800 GCD详解[96320:3468801] 1 -- <_NSMainThread: 0x6000011780c0>{number = 1, name = main}
2024-05-30 19:47:21.114507+0800 GCD详解[96320:3468801] 5 -- <_NSMainThread: 0x6000011780c0>{number = 1, name = main}
2024-05-30 19:47:23.140562+0800 GCD详解[96320:3468801] 2 -- <_NSMainThread: 0x6000011780c0>{number = 1, name = main}
2024-05-30 19:47:25.142281+0800 GCD详解[96320:3468801] 3 -- <_NSMainThread: 0x6000011780c0>{number = 1, name = main}
2024-05-30 19:47:27.143875+0800 GCD详解[96320:3468801] 4 -- <_NSMainThread: 0x6000011780c0>{number = 1, name = main}
在 异步执行 + 主队列 可以看到:
- 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然 异步执行 具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
- 所有任务是在打印的任务1和任务5之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
- 任务是按顺序执行的(因为主队列是 串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
四,GCD线程间的通信
在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
//获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//获取主队列
dispatch_queue_t mainqueue = dispatch_get_main_queue();
NSLog(@"begin---%@", [NSThread currentThread]);
dispatch_async(queue, ^{
//异步追加任务1
[NSThread sleepForTimeInterval:2];
NSLog(@"1---%@", [NSThread currentThread]);
//回到主线程
dispatch_async(mainqueue, ^{
//追加到主线程中2执行任务
[NSThread sleepForTimeInterval:2];
NSLog(@"2---%@", [NSThread currentThread]);
});
NSLog(@"3---%@", [NSThread currentThread]);
});
NSLog(@"end---%@", [NSThread currentThread]);
输出结果
2024-05-30 20:15:39.553857+0800 GCD详解[96959:3489760] begin---<_NSMainThread: 0x600000a0c100>{number = 1, name = main}
2024-05-30 20:15:39.553924+0800 GCD详解[96959:3489760] end---<_NSMainThread: 0x600000a0c100>{number = 1, name = main}
2024-05-30 20:15:41.559224+0800 GCD详解[96959:3490016] 1---<NSThread: 0x600000a4dd40>{number = 8, name = (null)}
2024-05-30 20:15:41.559701+0800 GCD详解[96959:3490016] 3---<NSThread: 0x600000a4dd40>{number = 8, name = (null)}
2024-05-30 20:15:43.560499+0800 GCD详解[96959:3489760] 2---<_NSMainThread: 0x600000a0c100>{number = 1, name = main}
- 可以看到在其他线程中先执行任务,执行完了之后回到主线程执行主线程的相应操作。
六,GCD信号量: dispatch_semaphore
Dispatch Semaphore
(调度信号量)是 GCD(Grand Central Dispatch)提供的一种同步机制,用于控制并发访问资源或者在不同线程之间进行同步。信号量可以用来限制并发执行的任务数量,或者让一个线程等待某个事件的发生。
使用 Dispatch Semaphore
- 创建信号量
dispatch_semaphore_create 用于创建一个信号量,初始计数可以是任意非负整数。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); // 创建初始值为1的信号量
- 等待信号量
dispatch_semaphore_wait 用于等待信号量,如果信号量计数大于0,则减1并立即返回;否则阻塞当前线程,直到信号量计数大于0或者超时。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 等待信号量,永远阻塞直到信号量计数大于0
- 发送信号
dispatch_semaphore_signal 用于发送信号,即增加信号量的计数。如果有等待的线程,则唤醒一个等待的线程。
dispatch_semaphore_signal(semaphore); // 发送信号,增加信号量计数
控制并发任务数量
假设你想限制最多只有2个任务同时执行,可以使用信号量来实现:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2); // 最多允许2个任务同时执行
for (int i = 0; i < 5; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 等待信号量
NSLog(@"Task %d started", i);
sleep(2); // 模拟任务耗时2秒
NSLog(@"Task %d completed", i);
dispatch_semaphore_signal(semaphore); // 发送信号
});
}
实现线程同步,将异步执行任务转换为同步执行任务
NSLog(@"currentThread -- %@", [NSThread currentThread]);
NSLog(@"semaphere --- begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphere = dispatch_semaphore_create(0);
__block int number = 0;
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"1 --- %@", [NSThread currentThread]);
number = 100;
dispatch_semaphore_signal(semaphere);
});
dispatch_semaphore_wait(semaphere, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore --- end number = %d", number);
输出结果:
2024-05-30 21:27:19.082058+0800 GCD详解[98426:3541645] currentThread -- <_NSMainThread: 0x6000030f4000>{number = 1, name = main}
2024-05-30 21:27:19.082108+0800 GCD详解[98426:3541645] semaphere --- begin
2024-05-30 21:27:21.087427+0800 GCD详解[98426:3541817] 1 --- <NSThread: 0x6000030b2380>{number = 4, name = (null)}
2024-05-30 21:27:21.087923+0800 GCD详解[98426:3541645] semaphore --- end number = 100
semaphore—end 是在执行完 number = 100; 之后才打印的。而且输出结果 number 为 100。
执行顺如下:
- semaphore 初始创建时计数为 0。
- 异步执行 将 任务 1 追加到队列之后,不做等待,接着执行 dispatch_semaphore_wait 方法,semaphore 减 1,此时 semaphore == -1,当前线程进入等待状态。
- 然后,异步任务 1 开始执行。任务 1 执行到 dispatch_semaphore_signal 之后,总信号量加 1,此时 semaphore == 0,正在被阻塞的线程(主线程)恢复继续执行,最后打印 semaphore—end,number = 100。
异步执行任务转换为同步执行任务,可以对异步的执行结果进行进一步操作。
Dispatch Semaphore
在实际开发中主要用于:
- 保持线程同步,将异步执行任务转换为同步执行任务
- 保证线程安全,为线程加锁
七,Dispatch Semaphore 线程安全和线程同步(为线程加锁)
非线程安全(不使用 semaphore)
先来看看不考虑线程安全的代码:
/**
* 非线程安全:不使用 semaphore
* 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
*/
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
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 saleTicketNotSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketNotSafe];
});
/**
* 售卖火车票(非线程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
if (self.ticketSurplusCount > 0) { // 如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { // 如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
break;
}
}
}
线程安全(使用 semaphore 加锁)
/**
* 线程安全:使用 semaphore 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
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) {
// 相当于加锁
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
if (self.ticketSurplusCount > 0) { // 如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { // 如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
break;
}
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
}
}
运行结果
2024-05-30 21:54:54.019858+0800 GCD详解[99036:3563863] 剩余票数:10 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:54.222849+0800 GCD详解[99036:3563864] 剩余票数:9 窗口:<NSThread: 0x600003681bc0>{number = 3, name = (null)}
2024-05-30 21:54:54.428311+0800 GCD详解[99036:3563863] 剩余票数:8 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:54.632216+0800 GCD详解[99036:3563864] 剩余票数:7 窗口:<NSThread: 0x600003681bc0>{number = 3, name = (null)}
2024-05-30 21:54:54.833208+0800 GCD详解[99036:3563863] 剩余票数:6 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:55.037057+0800 GCD详解[99036:3563864] 剩余票数:5 窗口:<NSThread: 0x600003681bc0>{number = 3, name = (null)}
2024-05-30 21:54:55.237681+0800 GCD详解[99036:3563863] 剩余票数:4 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:55.439402+0800 GCD详解[99036:3563864] 剩余票数:3 窗口:<NSThread: 0x600003681bc0>{number = 3, name = (null)}
2024-05-30 21:54:55.643820+0800 GCD详解[99036:3563863] 剩余票数:2 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:55.849417+0800 GCD详解[99036:3563864] 剩余票数:1 窗口:<NSThread: 0x600003681bc0>{number = 3, name = (null)}
2024-05-30 21:54:56.055054+0800 GCD详解[99036:3563863] 剩余票数:0 窗口:<NSThread: 0x6000036c0dc0>{number = 5, name = (null)}
2024-05-30 21:54:56.260555+0800 GCD详解[99036:3563864] 所有火车票均已售完
2024-05-30 21:54:56.261010+0800 GCD详解[99036:3563863] 所有火车票均已售完
semaphoreLock = dispatch_semaphore_create(1)
;这里的信号量设置为1,就是为了保证只有一个线程同时运行售卖火车票的这段代码。保证了线程安全。