iOS--底层学习--GCD的简单认识
- 前言
- 什么是GCD
- GCD的优点
- GCD中的任务和队列
- 任务
- 队列
- GCD的使用
- 队列的创建和获取
- 任务的创建
- 队列嵌套
- 任务和队列中的一些要点
- GCD线程间的通信
- 从后台线程切换到主线程
- 通过队列传递数据
- 使用Dispatch Group进行线程间协调
- GCD的方法
- dispatch_barrier_async(栅栏方法)
- dispatch_after(延时方法)
- dispatch_once(一次性或只执行一次)
- dispatch_apply (快速迭代方法)
- dispatch_group(GCD 队列组)
- 创建group
- 将任务添加到组内
- 等待组内所有任务完成
- dispatch_group_wait
- dispatch_group_notify
- dispatch_semaphore(GCD:信号量)
- Dispatch Semaphore 线程安全和线程同步(为线程加🔒)
- 线程安全
前言
前段时间看了一阵子源码,但遇到了大量的线程操作,导致看起来迷迷糊糊的,而且看源码的方式也有点不对,所以现在打算先学习部分iOS的底层知识后,再回去重新过一遍源码 ;
这里先来了解一下之前蓝书和小白书上的GCD ;这里参考了这位大神的博客iOS多线程:『GCD』详尽总结
什么是GCD
iOS中的GCD是什么,简单介绍一下:
GCD,全称为Grand Central Dispatch,是苹果公司推出的一种用于多核编程的技术,首次出现在Mac OS X 10.6 Snow Leopard和iOS 4中。GCD旨在让开发者能够更容易地利用现代多核处理器的并行计算能力,提高应用程序的性能和响应性,而无需直接管理线程的创建、销毁和同步等底层细节。
GCD的核心概念围绕着“任务”和“队列”:
-
任务(Task):代表你想要执行的代码块。在GCD中,任务通常以block(闭包)的形式定义。
-
队列(Queue):用于存放待执行任务的先进先出(FIFO)列表。GCD提供了以下几种类型的队列:
- 主队列(Main Queue):这是一个特殊的串行队列,所有放在主队列中的任务都会在主线程上按顺序执行。适合更新UI等操作。
- 全局队列(Global Queues):GCD提供了几个优先级不同的并发队列,用于执行后台任务。这些队列可以同时处理多个任务,适用于耗时操作,如网络请求、大量计算等。
- 自定义队列(Custom Queues):开发者可以创建自己的串行或并发队列,根据需要安排任务执行的环境。
GCD通过以下主要API来管理任务的执行:
dispatch_async
:异步执行任务,不会阻塞当前线程,立即返回。dispatch_sync
:同步执行任务,会阻塞当前线程直到任务完成。dispatch_after
:延时一定时间后执行任务。dispatch_group
:用于管理一组任务,可以监听这一组任务全部完成的事件。dispatch_barrier_async
:在并发队列中使用,确保某任务在其前后没有其他任务并发执行,常用于保护共享资源的访问。
GCD通过其高效的线程管理和任务调度机制,极大地简化了并发编程,帮助开发者编写高性能的iOS和macOS应用程序。
GCD的优点
-
简化并发编程:GCD通过队列和任务的概念抽象化了复杂的线程管理,使得开发者可以专注于定义任务本身而不是线程的创建和管理,降低了多线程编程的门槛。
-
自动线程管理:GCD自动根据系统的负载和可用资源管理线程的创建和销毁,开发者无需直接干预,这减少了因手动管理线程而导致的错误和资源浪费。
-
高效的资源利用:GCD能够充分利用多核处理器,通过并发队列并发执行任务,显著提高应用程序的执行效率和响应速度。
-
灵活的任务调度:提供了不同优先级的全局并发队列以及主队列,同时允许创建自定义队列(串行或并发),使得开发者可以根据任务需求灵活安排执行策略。
-
易用的API:GCD的API设计简洁直观,使用block(闭包)来定义任务,使得代码更加紧凑和易于理解。
-
集成内存管理:GCD与ARC(Automatic Reference Counting)结合紧密,自动管理block中引用的对象生命周期,减少内存泄露的风险。
-
高级功能:支持障碍块(barrier blocks)、队列组(dispatch groups)和延迟执行(dispatch_after),这些高级特性让处理复杂并发逻辑变得更加简单。
-
减少死锁风险:通过合理的队列和执行模式选择,GCD能有效降低死锁的发生概率,尤其是使用异步执行时。
综上所述,GCD凭借其高效、灵活且易于使用的特性,成为了iOS开发中处理并发任务的首选工具,大大提升了应用的性能和用户体验。
GCD中的任务和队列
GCD的核心概念围绕着“任务”和“队列”展开;
我们先了解这里的任务和队列都是什么东西 ;
任务
在iOS中,GCD(Grand Central Dispatch)中的任务是您希望异步或同步执行的代码块,通常以Block(闭包)的形式定义。Block是一段可以捕获上下文变量的匿名函数,非常适合用来封装工作单元。GCD通过调度这些任务到不同的队列中执行,来管理线程和任务的并发执行。以下是GCD中关于任务的两个基本概念:
异步任务 (dispatch_async)
- 功能:使用dispatch_async函数提交的任务会在当前线程之外的某个时间点开始执行,不会阻塞当前线程的执行。这意味着调用dispatch_async后,函数会立即返回,而任务将在未来的某个时刻并发或异步执行。
- 应用场景:适用于执行耗时操作,如网络请求、I/O操作或大量计算,以避免阻塞主线程,保持应用的响应性。
同步任务 (dispatch_sync)
- 功能:通过dispatch_sync提交的任务会立即阻塞当前线程,直到该任务完成。这意味着只有当提交的任务执行完毕后,当前线程才会继续执行下一行代码。
- 应用场景:同步任务较少在主线程中使用,因为会导致界面卡顿。但在某些情况下,同步执行可以用于确保一系列操作按顺序执行,或在访问需要同步控制的资源时(如数据库或共享资源)。
****异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关
队列
在iOS中,GCD(Grand Central Dispatch)通过队列来组织和执行任务,队列遵循先进先出(FIFO)原则,即队列中的任务按照添加的顺序依次执行。
GCD主要提供了四种类型的队列来满足不同的执行需求:
1. 主队列(Main Queue)
- 类型:串行队列。
- 特点:所有放在主队列中的任务都会在应用程序的主线程上执行,因此它是唯一可以更新UI的队列。
- 使用场景:适合执行UI更新、用户交互反馈等操作。
2. 全局队列(Global Queue)
- 类型:并发队列。
- 特点:系统提供的几个优先级不同的队列,可以同时执行多个任务,适用于后台处理大量数据、网络请求、计算密集型任务等。
- 优先级:分为高、默认、低、后台四个优先级,使用
dispatch_get_global_queue(priority, 0)
获取,其中priority
参数决定队列的执行优先级。
3. 自定义串行队列(Custom Serial Queue)
- 类型:串行队列。
- 特点:由开发者创建,任务按顺序执行,不会并发。适合需要控制任务执行顺序的场景,比如数据库操作,避免数据竞争。
- 创建方式:使用
dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL)
创建,其中label
是队列的标识符,用于调试。
4. 自定义并发队列(Custom Concurrent Queue)
- 类型:并发队列。
- 特点:同样由开发者创建,但任务可以并发执行,提高了效率。适用于可以并行处理的任务,但仍需控制队列中任务执行的并发级别。
- 创建方式:使用
dispatch_queue_create(label, DISPATCH_QUEUE_CONCURRENT)
创建,DISPATCH_QUEUE_CONCURRENT
标志表明这是一个并发队列。
不过简单的来说,队列其实只有两种:串行队列和并行队列 ;这两者间的大致区别如图:
串行队列(Serial Dispatch Queue):
- 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Dispatch Queue):
- 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
队列的使用策略
- 任务调度:通过
dispatch_async
或dispatch_sync
函数将任务添加到队列中,前者异步执行不阻塞当前线程,后者同步执行会阻塞当前线程直到任务完成。 - 队列优先级:除了全局队列的固定优先级外,自定义队列默认采用与主队列相同的优先级,但不直接影响任务执行顺序,因为串行队列本身决定了任务是按序执行的。(队列的优先级会在后面讲解GCD的使用时再重点了解的)
通过合理利用不同类型的队列和任务调度方式,GCD使得iOS应用能够高效地管理并发和多线程任务,提升性能和用户体验。
对于串行队列和并行队列还有一个要点:并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。 这个不需要去单独记忆 ;
GCD的使用
简单来说可以分为两步:
- 创建一个队列
- 将任务追加到任务的等待队列中执行任务
队列的创建和获取
在iOS中,使用GCD(Grand Central Dispatch)时,可以通过以下方法来创建或获取不同类型的队列:
1. 获取主队列(Main Queue)
主队列是GCD预先创建好的一个串行队列,专门用于在主线程上执行任务。获取主队列的方式如下:
dispatch_queue_t mainQueue = dispatch_get_main_queue();
2. 获取全局并发队列(Global Concurrent Queue)
GCD提供了多个优先级的全局并发队列,适用于大多数后台处理任务。获取全局并发队列的方法及优先级参数如下:
// 优先级参数可以是:DISPATCH_QUEUE_PRIORITY_HIGH,DISPATCH_QUEUE_PRIORITY_DEFAULT,
// DISPATCH_QUEUE_PRIORITY_LOW,或 DISPATCH_QUEUE_PRIORITY_BACKGROUND
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
创建自定义串行队列:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.mySerialQueue", DISPATCH_QUEUE_SERIAL);
创建自定义并发队列:
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
在上述代码中,com.example.mySerialQueue
和 com.example.myConcurrentQueue
是队列的标签,用于标识队列,方便在调试时辨认。DISPATCH_QUEUE_SERIAL
和 DISPATCH_QUEUE_CONCURRENT
分别指定了队列的类型。
要点
- 当创建自定义队列时,应当确保提供一个唯一的标签,以防与其他队列冲突。
- 在不再需要自定义队列时,虽然GCD会自动管理队列的生命周期,但为了内存管理的最佳实践,建议在适当的时候显式地释放队列(尽管在大多数情况下这不是必需的,特别是在应用的整个生命周期内都会用到的队列)。
- 主队列其实并不特殊。 主队列的实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,又都会放到主线程中去执行,所以才造成了主队列特殊的现象。
任务的创建
// 同步执行任务创建方法
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
// 这里放异步执行任务代码
});
这里我了解到在不考虑嵌套的情况下,可以有一下几种组合(这里由于主队列具有特殊性,所以也单独拎出来了) ;
- 同步执行 + 并发队列
- 异步执行 + 并发队列
- 同步执行 + 串行队列
- 异步执行 + 串行队列
- 同步执行 + 主队列
- 异步执行 + 主队列
接下来讲解这几种组合之间的区别;但在这之前先讲一个从大神博客里找到的例子来理清楚队列和任务之间的关系:
假设现在有 5 个人要穿过一道门禁,这道门禁总共有 10 个入口,管理员可以决定同一时间打开几个入口,可以决定同一时间让一个人单独通过还是多个人一起通过。不过默认情况下,管理员只开启一个入口,且一个通道一次只能通过一个人。
- 这个故事里,人好比是 任务,管理员好比是 系统,入口则代表 线程。
- 5 个人表示有 5 个任务,10 个入口代表 10 条线程。
- 串行队列 好比是 5 个人排成一支长队。
- 并发队列 好比是 5 个人排成多支队伍,比如 2 队,或者 3 队。
- 同步任务 好比是管理员只开启了一个入口(当前线程)。
- 异步任务 好比是管理员同时开启了多个入口(当前线程 + 新开的线程)。
- 『异步执行 + 并发队列』 可以理解为:现在管理员开启了多个入口(比如 3 个入口),5 个人排成了多支队伍(比如 3 支队伍),这样这 5 个人就可以 3 个人同时一起穿过门禁了。
- 『同步执行 + 并发队列』 可以理解为:现在管理员只开启了 1 个入口,5 个人排成了多支队伍。虽然这 5 个人排成了多支队伍,但是只开了 1 个入口啊,这 5 个人虽然都想快点过去,但是 1 个入口一次只能过 1 个人,所以大家就只好一个接一个走过去了,表现的结果就是:顺次通过入口。
- 换成 GCD 里的语言就是说:
- 『异步执行 + 并发队列』就是:系统开启了多个线程(主线程+其他子线程),任务可以多个同时运行。
- 『同步执行 + 并发队列』就是:系统只默认开启了一个主线程,没有开启子线程,虽然任务处于并发队列中,但也只能一个接一个执行了。
通过上面的例子,下面这张图也就好理解很多了:
这里给出一个代码实例:
dispatch_queue_t testQueue = dispatch_queue_create("testqueue", DISPATCH_QUEUE_SERIAL) ;
dispatch_async(testQueue, ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task01") ;
NSLog(@"%@",[NSThread currentThread]) ;
}) ;
dispatch_async(testQueue, ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task02") ;
NSLog(@"%@",[NSThread currentThread]) ;
}) ;
dispatch_async(testQueue, ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task03") ;
NSLog(@"%@",[NSThread currentThread]) ;
}) ;
通过这个例子可以简单的体会到同步执行 + 串行队列中任务的等待过程;同理上面所有的组合都可以通过类似的代码实现,这里就不一一列出了 ;
然后就是注意下上面同步执行 + 主队列的死锁现象时怎么产生的:
同步执行(sync)要求当前线程必须等待指定任务完成后再继续执行后续代码。这意味着,当调用dispatch_sync时,调用线程会被阻塞,直到传入的block执行完毕。
由于任务是添加到主队列的,它必须等待主线程空闲才能开始执行。然而,主线程正在等待这个任务完成,形成了互相等待的状态。
因此,主线程既不能继续向下执行(因为它在等待同步任务完成),也无法开始执行队列中的任务(因为主线程被占用,无法分配新的任务),从而导致了死锁。
简单来说,就是主线程在等待自己完成一个任务,而这个任务又必须等主线程空闲才能开始,形成一个无法解开的“死锁”状态。为了避免这种情况,应该避免在主线程使用同步方式向主队列添加任务,或者使用异步执行(async)来避免阻塞当前线程。
但要注意:同步执行 + 主队列 在不同线程中调用结果也是不一样,在主线程中调用会发生死锁问题,而在其他线程中调用则不会。
主线程中调用:
其他线程中调用:
这是因为我们在其他线程中向主线程添加任务时,调用线程会被阻塞,直到传入的block执行完毕。这是主线程空闲,可以执行任务,所以没有造成死锁 ;
队列嵌套
除了上边提到的『主线程』中调用『主队列』+『同步执行』会导致死锁问题。实际在使用『串行队列』的时候,也可能出现阻塞『串行队列』所在线程的情况发生,从而造成死锁问题。这种情况多见于同一个串行队列的嵌套使用。
如:
同理:第一个dispatch_sync阻塞[NSThread detachNewThreadSelector:@selector(addTask) toTarget:self withObject:nil] 创建的线程,将任务追加到主队列,该任务中的dispatch_sync此时阻塞了主线程,又追加了新的任务到主线程,互相等待造成死锁 ;
队列嵌套时的区别:
任务和队列中的一些要点
- 任务才拥有创建新线程的能力,而队列只有开启线程的能力,并不能创建线程
GCD线程间的通信
在 iOS 开发过程中,我们一般在主线程里边进行 UI 刷新,例如:点击、滚动、拖拽等事件。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。由于GCD是基于队列的,线程间的通信主要是通过将任务安排到特定队列中执行来实现的。以下是如何使用GCD进行线程间通信的基本步骤和原理:
从后台线程切换到主线程
在多线程开发中,经常需要在后台线程执行耗时操作(如网络请求、大数据处理等),完成后需要更新UI,而所有的UI更新都必须在主线程(也称为主线程)上进行。这时,就需要从后台线程切换到主线程。GCD中,可以使用主队列来实现这种切换:
// 在后台线程执行完任务后
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
// 执行耗时操作...
// 切换到主线程更新UI
dispatch_async(dispatch_get_main_queue()) {
// 更新UI的操作,如改变按钮文字、更新Label内容等
}
}
通过队列传递数据
虽然GCD本身不直接提供像NSOperation那样携带数据的队列操作,但可以通过Block捕获上下文变量或使用全局变量等方式,在不同线程间传递数据。例如,可以在Block中访问外部变量或将数据作为Block的参数传递:
var resultFromBackgroundThread: String?
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)) {
let data = fetchData() // 假设这是在后台线程获取的数据
resultFromBackgroundThread = data
dispatch_async(dispatch_get_main_queue()) {
updateUI(withData: resultFromBackgroundThread) // 在主线程更新UI
}
}
使用Dispatch Group进行线程间协调
当需要在多个后台任务全部完成后才在主线程执行某个操作时,可以使用Dispatch Group。这允许你监控一组任务的完成情况,然后在所有任务完成时收到通知,由于没有学习Group,这里就跳过了 ;
GCD的方法
dispatch_barrier_async(栅栏方法)
dispatch_barrier_async是GCD中用于在队列中插入一个障碍任务的函数。这个函数的主要用途是在并发队列中确保一组任务执行完毕后,再开始执行另一个任务,或者在某个任务前后保持独占访问(比如对共享资源的修改)。简而言之,它起到了一个同步点或者说是“屏障”的作用。
如图:
下面是一个方法的使用实例:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[NSThread detachNewThreadSelector:@selector(addTask) toTarget:self withObject:nil] ;
}
- (void)addTask {
// dispatch_queue_t testQueue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0) ;
dispatch_queue_t testQueue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(testQueue, ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task01") ;
NSLog(@"%@",[NSThread currentThread]) ;
}) ;
dispatch_async(testQueue, ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task02") ;
NSLog(@"%@",[NSThread currentThread]) ;
}) ;
dispatch_barrier_async(testQueue, ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task03") ;
NSLog(@"%@",[NSThread currentThread]) ;
}) ;
dispatch_async(testQueue, ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task04") ;
NSLog(@"%@",[NSThread currentThread]) ;
}) ;
}
对于这个方法还有几个要说的点:
- dispatch_barrier_async主要用于自定义的并发队列(dispatch_queue_create创建并指定为DISPATCH_QUEUE_CONCURRENT的队列)。在这样的队列中,屏障任务会在所有在此之前提交的异步任务结束之后开始执行,并且在它完成之前,之后提交的异步任务不会开始。这样可以保护一段代码免受并发访问的影响,适用于读多写少的场景,比如数据库的读写操作。
- 非阻塞:尽管dispatch_barrier_async能起到同步的效果,但它本身是异步执行的,不会阻塞当前线程。这意味着调用它的线程可以继续执行其他任务,而不会等待屏障任务完成。
- 还有一点就是dispatch_barrier_async方法只对自定义的并行队列或自定义串行队列生效,如果在全局并行队列中调用这个方法,是不会出现阻塞同步效果的 ;
dispatch_after(延时方法)
dispatch_after是GCD(Grand Central Dispatch)中用于在指定时间之后执行代码的一个函数。它可以让开发者安排一个Block(包含代码块)在未来的某个时间点异步执行,而不是立即执行。这对于实现定时任务、延迟执行某些操作非常有用。
代码实例:
- (void)after {
dispatch_queue_t queue = dispatch_queue_create("testqueue", DISPATCH_QUEUE_CONCURRENT) ;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), queue, ^{
NSLog(@"Hello") ;
});
}
- dispatch_time函数用于计算执行时间点,其中DISPATCH_TIME_NOW表示当前时间,(int64_t)(delayInSeconds * NSEC_PER_SEC)则是要延迟的秒数转换成纳秒。
- dispatch_after接受三个参数:执行时间点、在哪个队列上执行Block、以及要执行的Block。
- 现在这个方法在输入时默认追加到主队列中,但该罚方法允许追加方法在其他队列中 ;
dispatch_once(一次性或只执行一次)
dispatch_once是GCD(Grand Central Dispatch)提供的一个API,用于确保某段代码在整个应用程序生命周期内只被执行一次。这对于需要初始化单例、执行只运行一次的设置或注册操作等场景非常有用。dispatch_once通过全局的原子操作保证了即使在多线程环境中也能安全地实现“单次执行”。
即使在多线程的环境下,dispatch_once 也可以保证线程安全。线程安全后面讲 ;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 这里的代码块将只执行一次
});
- dispatch_once接受两个参数:一个是指向dispatch_once_t类型变量的指针(通常声明为static),另一个是包含待执行代码的Block
dispatch_apply (快速迭代方法)
dispatch_apply是GCD(Grand Central Dispatch)中的一个函数,用于同步地执行某个Block多次。它可以在指定的队列上并行或串行地重复执行Block,直到指定的迭代次数完成。这对于需要遍历数组、执行固定次数的操作等场景非常有用,特别是在处理大量数据时能有效利用多核CPU提升性能。
//在这里插入代码片iterations:指定Block执行的次数。
//queue:指定执行Block的队列,可以是串行队列或并发队列。队列类型决定了Block的执行方式(串行或并行)。
//Block参数index:在每次迭代中,Block会被传入一个递增的索引值。
- (void)apply {
dispatch_apply(5, dispatch_get_main_queue(), ^(size_t iteration) {
NSLog(@"nice") ;
}) ;
}
- 同步执行:dispatch_apply是同步执行的,意味着调用者会阻塞,直到所有的迭代完成。因此,如果在主线程调用并且迭代次数很大,可能会导致UI冻结。
- 并发与串行:如果queue是一个并发队列,dispatch_apply会尽可能地并行执行迭代,非常适合CPU密集型任务。如果是串行队列,则迭代会按顺序执行。
dispatch_group(GCD 队列组)
dispatch_group是Grand Central Dispatch(GCD)提供的一种机制,用于同步任务或工作块。它允许你将一系列任务组织在一起,并跟踪这些任务的完成状态。当你将多个任务添加到一个dispatch group时,你可以等待这个组里的所有任务都完成后再执行下一步操作,这在需要基于多个异步操作的结果来执行后续逻辑的场景中非常有用。
简单来说,dispatch_group可以用来监视队列中任务的执行情况 ;
创建group
dispatch_group_t group = dispatch_group_create() ;
将任务添加到组内
dispatch_group_async,用于将异步执行的代码块(block)添加到指定的 dispatch group 中。这个函数会立即返回,不会阻塞当前线程,同时保证group内的任务按照它们被添加的顺序开始执行,但不保证完成的顺序。
dispatch_group_async(group, dispatch_get_main_queue(), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task01") ;
NSLog(@"%@",[NSThread currentThread]) ;
}) ;
dispatch_group_enter、dispatch_group_leave:
- dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1
- dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1。
- 当 group 中未执行完毕任务数为0的时候,才会使 dispatch_group_wait 解除阻塞,以及执行追加到
dispatch_group_notify 中的任务。
- (void)group {
dispatch_group_t group = dispatch_group_create() ;
// dispatch_group_async(group, dispatch_get_main_queue(), ^{
// [NSThread sleepForTimeInterval:2] ;
// NSLog(@"task01") ;
// NSLog(@"%@",[NSThread currentThread]) ;
// }) ;
dispatch_group_enter(group) ;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task01") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
dispatch_group_enter(group) ;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task02") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
dispatch_group_wait(group, DISPATCH_TIME_FOREVER) ;
dispatch_group_enter(group) ;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task03") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
}
这里的dispatch_group_enter(group) ;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@“task01”) ;
NSLog(@“%@”,[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
和// dispatch_group_async(group, dispatch_get_main_queue(), ^{
// [NSThread sleepForTimeInterval:2] ;
// NSLog(@“task01”) ;
// NSLog(@“%@”,[NSThread currentThread]) ;
// }) ;
几乎是等效的 ;
等待组内所有任务完成
使用dispatch_group_wait(group, DISPATCH_TIME_FOREVER)阻塞当前线程,直到group中的所有任务完成。更灵活的做法是使用dispatch_group_notify(group, queue, ^{ /* completion task */ }),当group中所有任务结束时,会在指定的queue上执行一个通知 block,而无需阻塞当前线程。
dispatch_group_wait
dispatch_group_wait是Apple操作系统(如iOS和macOS)中的Grand Central Dispatch (GCD)框架使用的一个函数,用于管理并发操作。它通过阻塞当前线程,直到指定的调度组中的所有任务都完成,从而允许你同步任务的执行。
这个函数会阻塞当前线程,直到调度组中的所有任务都执行完毕。这意味着调用这个函数后,线程会一直等待,直到组内所有的任务都完成,之后才会继续执行下一条语句。
- (void)group {
dispatch_group_t group = dispatch_group_create() ;
// dispatch_group_async(group, dispatch_get_main_queue(), ^{
// [NSThread sleepForTimeInterval:2] ;
// NSLog(@"task01") ;
// NSLog(@"%@",[NSThread currentThread]) ;
// }) ;
dispatch_group_enter(group) ;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task01") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
dispatch_group_enter(group) ;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task02") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
dispatch_group_wait(group, DISPATCH_TIME_FOREVER) ;
dispatch_group_enter(group) ;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task03") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
}
当你需要确保一组后台操作全部完成后再更新UI或进行下一步操作时,dispatch_group_wait就会非常有用。但需要注意的是,因为它会阻塞线程,所以在主线程上使用时要谨慎,以避免影响用户体验。在某些情况下,可能更倾向于使用dispatch_group_notify来避免阻塞,尤其是在处理UI更新时。
dispatch_group_notify
dispatch_group_notify同样是GCD中用于管理并发操作的一个函数,但它与dispatch_group_wait的工作方式有所不同。dispatch_group_notify不会阻塞当前线程,而是提供了一个更加优雅的方式来响应调度组中所有任务完成的事件。
当所有属于指定调度组的任务完成后,dispatch_group_notify会在一个指定的队列上异步执行一个闭包(block)或者函数。这样可以在不阻塞主线程的情况下,执行一些后续操作,比如更新用户界面或者发出通知。
- (void)group {
dispatch_group_t group = dispatch_group_create() ;
// dispatch_group_async(group, dispatch_get_main_queue(), ^{
// [NSThread sleepForTimeInterval:2] ;
// NSLog(@"task01") ;
// NSLog(@"%@",[NSThread currentThread]) ;
// }) ;
dispatch_group_enter(group) ;
dispatch_group_notify(group, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task03") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
dispatch_group_enter(group) ;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task01") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
dispatch_group_enter(group) ;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task02") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
// dispatch_group_wait(group, DISPATCH_TIME_FOREVER) ;
//
// dispatch_group_enter(group) ;
// dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
// [NSThread sleepForTimeInterval:2] ;
// NSLog(@"task03") ;
// NSLog(@"%@",[NSThread currentThread]) ;
// dispatch_group_leave(group) ;
// }) ;
dispatch_group_enter(group) ;
dispatch_group_notify(group, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task03") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
dispatch_group_enter(group) ;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task04") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
}
这里猜一下输出为啥;上面的dispatch_group_notify使用是有问题的 ;我们不应该在它的block中使用dispatch_group_leave(group) ;应为它和dispatch_group_async都是实际上调用了dispatch_group_enter、dispatch_group_leave ;如果再调用这两个方法会发什么:
这时我们发现在
dispatch_group_enter(group) ;
dispatch_group_notify(group, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2] ;
NSLog(@"task03") ;
NSLog(@"%@",[NSThread currentThread]) ;
dispatch_group_leave(group) ;
}) ;
中group先增加了两个任务数,但block中的代码需要等待group中的任务数为零 ;
所以这个任务就会无限等待,但由于该方法特性不会阻塞线程 ;
dispatch_semaphore(GCD:信号量)
dispatch_semaphore
是Grand Central Dispatch (GCD) 提供的一种同步原语,用于控制访问特定资源的线程数量或者在某些条件满足时(比如完成特定任务数量)唤醒线程。Semaphore(信号量)的基本原理是一个整型计数器,用于跟踪资源的可用数量。它有两个主要操作:signal
(信号)和 wait
(等待)。
- dispatch_semaphore_create: 创建一个semaphore,传入的值通常表示当前可用资源的数量。
- dispatch_semaphore_signal: 发送一个信号,意味着释放一个资源,使计数器加1。如果有线程正在
dispatch_semaphore_wait
上等待,那么这将唤醒其中一个线程继续执行。 - dispatch_semaphore_wait: 使当前线程等待,直到semaphore的计数器大于0,然后将其减1并继续执行。如果计数器为0,则线程会被阻塞,直到其他线程调用
dispatch_semaphore_signal
使其变为非零。
Dispatch Semaphore 线程安全和线程同步(为线程加🔒)
线程安全
线程安全指的是在多线程环境下,代码能够正确地运行并产生预期结果,而不会因为线程间的相互干扰(如数据竞争和竞态条件)导致错误或不可预料的行为。为了确保线程安全,开发时需要采取一些策略和机制来保护共享资源免受并发访问的影响。
下面是一个线程不安全的情况:
/**
* 非线程安全:不使用 semaphore
* 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
*/
- (void)initTicketStatusNotSave {
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;
}
}
}
这就造成了数据竞争了 ;所以我们使用信号量来为其加锁 ;
- (void)initTicketStatusNotSave {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
self.ticketSurplusCount = 50;
self.semaphore = dispatch_semaphore_create(1) ;
// 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) {
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER) ;
if (self.ticketSurplusCount > 0) { // 如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", (long)self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { // 如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
dispatch_semaphore_signal(self.semaphore) ;
break;
}
dispatch_semaphore_signal(self.semaphore) ;
}
}