「OC」GCD的简单运用

「OC」GCD的简单实用

前言

前一篇博客初次接触了线程这些内容,了解了NSThread的相关内容,今天我们继续学习线程之中GCD的学习

介绍

Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。

GCD是一个替代诸如NSThread等技术的很高效和强大的技术。GCD完全可以处理诸如数据锁定和资源泄漏等复杂的异步编程问题。GCD的工作原理是让一个程序,根据可用的处理资源,安排他们在任何可用的处理器核心上平行排队执行特定的任务。这个任务可以是一个功能或者一个程序段。

——百度百科

  • GCD 可用于多核的并行运算
  • GCD 会自动利用更多的 CPU 内核 (比如双核、四核)
  • GCD 会自动管理线程的生命周期(创建线程、请度任务、销段线程)

任务和队列

在了解GCD之前我们需要任务和队列的两个概念

任务

简单来说就是我们放进GCD的block之中的代码操作

同步执行(sync)
  • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
  • 只能在当前线程中执行任务,不具备开启新线程的能力
dispatch_sync(queue, ^{
// 这里放同步执行任务代码
});
异步执行(async)
  • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
  • 可以在新的线程中执行任务,具备开启新线程的能力。
dispatch_async(queue, ^{
// 这里放同步执行任务代码
});

队列

队列作为一种数据结构,具有先进先出的特点,这里的队列是用于对任务进行存储的,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务

串行队列(Serial Dispatch Queue)
  • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
并发队列(Concurrent Dispatch Queue)
  • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

创建队列的方式

可以使用dispatch_queue_create来创建队列,需要传入两个参数,第一个参数表示队列的唯一标识符,也就是队列的名字,用于 DEBUG,可为空,Dispatch Queue 的名称推荐使用应用程序 ID 这种逆序全程域名;第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并发队列。

// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bb.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bb.testQueue", DISPATCH_QUEUE_CONCURRENT);

OC之中还提供一个方法能够让我们获得一个主串行队列,即这个队列的内容都会在主线程之中进行操作,我们可以用一下方法获得主线程

// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();

在创建主串行队列之外,OC还另外提供了一个全局并发队列,我们可以使用一下方法

// 获取默认的全局并发队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

创建全局并发队列的时候,第一个参数是关于这个队列的优先级,第二个参数通常用于指定该队列的附加特性,在一般的使用之中我们都使用默认值0即可。

 // 获取用户交互优先级的全局并发队列
dispatch_queue_t userInteractiveQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

任务和队列的结合

接下来我们将任务和队列进行组合使用,来探究GCD的具体用法

同步串行队列

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
	// 追加任务1
	for (int i = 0; i < 2; ++i) {
		NSLog(@"1--%d---%@",i ,[NSThread currentThread]);
	}
});

dispatch_sync(queue, ^{
	// 追加任务2
	for (int i = 0; i < 2; ++i) {
		NSLog(@"2--%d---%@", i, [NSThread currentThread]);
	}
});

在这里插入图片描述

由于是在主线程之中进行同步操作,所以代码内容是按序执行的,不会开启新的任务

同步并行队列

dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            NSLog(@"1--%d---%@",i ,[NSThread currentThread]);
        }
    });

    dispatch_sync(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            NSLog(@"2--%d---%@", i, [NSThread currentThread]);
        }
    });

这个在主线程之中使用并行操作,我们可以看到即使我们使用的并发队列,我们的任务依然是按序进行,在主线程之上执行,并不会开启新的线程

在这里插入图片描述

异步串行队列

dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            NSLog(@"1--%d---%@",i ,[NSThread currentThread]);
        }
    });

    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            NSLog(@"2--%d---%@", i, [NSThread currentThread]);
        }
    });

由于我们使用了异步执行,所以这个两个任务不会在主线程进行,会新建一个线程,在这个新建成的线程进行串行操作。

image-20241007115428602

异步并行队列

NSLog(@"主线程:%@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
ispatch_async(queue, ^{
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
            NSLog(@"1--%d---%@",i ,[NSThread currentThread]);
        }
    });

    dispatch_async(queue, ^{
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
            NSLog(@"2--%d---%@", i, [NSThread currentThread]);
        }
    });

可以看到我们的程序就会实现很明显的并发效果

image-20241007120754538

死锁

情况一:

关于GCD同步和异步的使用,有时候会产生死锁。首先我们要明白,同步时,我们必须完成会阻塞当前函数的返回,而异步函数会立即返回执行GCD内容下面的代码】

以下面的代码为例子:

NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
	NSLog(@"执行任务2");
});

NSLog(@"执行任务3");

在我们使用同步队列中把任务2添加到了主队列之中,而由于串行队列需要按序完成任务,我们理清一下:

  1. 主线程:在主线程之中我们的处理流程是这样的——任务1,sync,任务3
  2. 主队列:而在主队列之中我们需要先完成viewDidLoad的方法,再去完成队列之中的后一个任务

在这里插入图片描述
从上图我们就可以看到,想要完成主线程的任务需要按序完成sync之中的内容,而在主队列的要求之中我们需要将viewDidLoad的方法内容进行完成才会接着处理sync之中的内容,于是程序在互相等待之中得不到结果于是就形成了死锁

那我们如何进行修改呢,我们可以将同步的sync修改为异步完成的async,这样子我们的程序就不会要求队列之中的内容要直接完成,可以先执行后面的内容。

NSLog(@"执行任务1");

	dispatch_queue_t queue = dispatch_get_main_queue();
	dispatch_async(queue, ^{
		NSLog(@"执行任务2");
	});

	NSLog(@"执行任务3");

	// dispatch_async不要求立马在当前线程同步执行任务

情况二:

接下来我们将内容变得更加复杂一点:

NSLog(@"执行任务1");

dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
	NSLog(@"执行任务2");

	dispatch_sync(queue, ^{ // 1
		NSLog(@"执行任务3");
	});

	NSLog(@"执行任务4");
});

NSLog(@"执行任务5");

以上的内容我们就可以看出,这个程序仍然造成了死锁,执行完2后(2当前线程为串行),打印3任务通过同步任务插入到串行队列,放在打印4的后面(在执行2串行任务里,4的打印在3的前面),但是同步任务有需要先执行3在执行4,就造成相互等待,造成死锁。

我们可以直接将串行队列直接改为并行队列,这样子任务3和任务4就不会产生死锁,大致的输出如下:

  1. 执行任务1
  2. 执行任务5
  3. 执行任务2
  4. 执行任务3(可能在任务 4 之前或之后)
  5. 执行任务4

GCD之间的通信

栅栏函数

栅栏函数可以控制任务执行的顺序,栅栏函数之前的执行完毕之后,执行栅栏函数,然后在执行栅栏函数之后的

 dispatch_queue_t queue = dispatch_queue_create("com.xxccqueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download1--%@",i,[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download2--%@",i,[NSThread currentThread]);
        }
    });
    //栅栏函数
    dispatch_barrier_async(queue, ^{
        NSLog(@"我是一个栅栏函数");
      	[NSThread sleepForTimeInterval:2]; 
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download3--%@",i,[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download4--%@",i,[NSThread currentThread]);
        }
    });

我们可以看到在并发的情况下,在栅栏方法之中我们暂时把异步的操作转化为同步的操作,所以会像栅栏一样将前后的线程任务给分开,我们可以使得线程转化变得更加明显,在栅栏函数处进行线程睡眠来看一下打印出来的时间。
image-20241008215924873

GCD队列组

在我们同时进行异步耗时的操作的时候,当我们有多个耗时操作,我们可以使用GCD之中的队列组来进行操作,我先给出示例在进行讲解

dispatch_group_t group = dispatch_group_create();

// 假设我们有三个异步任务
for (int i = 0; i < 3; i++) {
    dispatch_group_enter(group);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 模拟一个耗时操作
        [NSThread sleepForTimeInterval:1.0];
        NSLog(@"任务 %d ", i);
        
        // 完成任务后离开组
        dispatch_group_leave(group);
    });
}

// 在所有任务完成后执行某个操作
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"所有任务都完成");
});

  • dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1
  • dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1
  • 当 group 中未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞,以及执行追加到dispatch_group_notify中的任务。
  • dispatch_group_notify在所有任务完成的时候执行其中的任务

GCD延时执行方法

在我们实际运用之中可能会需要使用到,在指定时间之后开始执行某一个任务,这个功能我们可以使用GCD之中的dispatch_after来进行实现。

以下是一个延时两秒的GCD示例:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //执行的任务
    });

要注意的是:这个dispatch_after并不是先添加到主队列之中然后在指定时间后进行执行,而是在指定时间之后再添加至主队列之中,所以严格来说这个时间不是绝对准确的。

一次性函数

在使用单例的时候,为了预防多线程操作单例的重复创建,我们就会使用一次性函数,这个函数在整个程序运行的时候只会执行一次,在这个函数里面默认是线程安全的

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
	// 只执行1次的代码(这里面默认是线程安全的)
});

GCD 信号量:dispatch_semaphore

这个内容有点类似引用计数,我们可以用这个信号量的功能来限制同时执行的任务数量

创建信号量:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); // 初始值为1

等待信号量: 使用 dispatch_semaphore_wait() 来请求信号量。如果信号量的计数大于0,则计数减1并继续执行;如果为0,则当前线程会被阻塞,直到信号量可用。

释放信号量: 使用 dispatch_semaphore_signal() 来释放信号量,将计数加1。

信号量的初始值通常是1,表示一个资源可以被访问;可以设置更高的值来允许多个线程同时访问。

在使用 dispatch_semaphore_wait() 时,如果信号量的计数为0,线程会被阻塞,直到信号量可用或超时。

必须确保每次调用 dispatch_semaphore_wait() 后都有相应的 dispatch_semaphore_signal(),否则可能导致死锁。

参考文章

GCD介绍
iOS多线程-概念

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值