iOS 多线程 -- GCD 入门篇 | 来自缤纷多彩的灰

iOS 多线程 -- GCD 入门篇 @ WHlcj | 2024-07-08T00:41:51+08:00 | 7 分钟阅读

本篇只介绍 GCD 的一些基础知识,包括创建队列的方式和一些参数介绍,以及相关的应用场景。关于更多 api 的调用方式,后面会开一篇 GCD 实践篇专门介绍~

GCD 全称是Grand Central Dispatch是 Apple 开发的一个多核编程问题的解决方案,与 NSOperation、NSThread、pthread 是 iOS 中多线程的几种处理方式。

C++ 多线程基本应用

之前在 C++ 中怎么使用多线程呢?最简单的就是这么用:

  1. 提供一个任务 c 函数
  2. 创建一个新线程pthread_create,并向这个线程提交一个任务函数
  3. 线程结束的时候调用pthread_exit来回收资源(main函数要调用pthread_exit等待其他线程执行完毕)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//1.提供一个任务c函数
void *GCDTest(void *threadid)
{
    printf("Hello World!\n");
    //3.线程结束调用pthread_exit
    pthread_exit(NULL);
}

int main(int argc, char *argv[])
{   //2.创建一个新线程线程
    pthread_t thread;
    int rc = pthread_create(&thread, NULL, PrintHello, (void *) t);
    //4.main函数要调用pthread_exit等待其他线程执行完毕)
    pthread_exit(NULL);
}

大多数场景下,多线程都使用在异步并发场景。但也有少数场景有些特殊要求:例如要求阻塞,当之前创建的线程执行完退出之前,我们什么都不能做

  1. 在上述的pthread_create的基础上,将其设置成可被 join 的属性,join 方法会阻塞调用它的线程,直到被 join 的线程完成执行。这确保了线程的同步。
  2. 使用pthread_join函数同步等待线程执行完毕
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int main(int argc, char *argv[])
{
    pthread_t thread;
    //1.将其设置成可被join的属性
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
    int rc = pthread_create(&thread, &attr, PrintHello, (void *) t);
    //记得手动释放
    pthread_attr_destroy(&attr);
    //2.使用pthread_join函数同步等待线程执行完毕
    void *status;
    rc = pthread_join(thread, &status);
    ...
    ...
    pthread_exit(NULL);
}

还有些情况下,我们还希望创建的几个多线程任务是能够按照先后顺序执行的。可以尝试信号量、或者接连好几个 join 等方法 … GCD 封装之后的多线程使用起来就会变得比较容易一些:

  1. 创建一个队列
  2. 向队列提交一个任务

GCD 让开发者仅关注自己的任务想要被如何执行(串行并发?同步异步?),从而隐藏了创建线程的过程,产生了队列的概念。我们仅需要用的 GCD 函数向 GCD 队列中提交我们的任务即可。

GCD 多线程基本概念

大部分基础概念建议可以参考这篇博客多线程原理,我这里只补充一部分。

  1. 队列:
  • 队列是负责任务调度的。我们向队列中提交了任务之后,queue 会调度此任务并分配线程来执行。
  • 分类:串行队列:只能按照 FIFO 原则进行调度,并发队列则没有此限制
  1. 同步函数:
  • 会保证得到任务的执行结果之后再返回,否则就会阻塞下面代码执行
  • 实际上不会开辟新的线程,就是在当前线程执行任务 block
  1. 异步函数:
  • 不会等待任务执行结果,提交任务后立即返回
  • queue 会开辟新线程来执行
  1. 任务的执行顺序:
  • 不是被提交的任务就会立即执行,会等待被调度。
  • 不是被调度了就会立即执行,而是要看其被分配的线程的运行情况。
  • 不是先被执行了就会先得到结果,要看任务本身耗时

基础队列的创建和使用

Objective-C 示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
- (void)GCDTest {
    // 创建串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("com.example.myserial", DISPATCH_QUEUE_SERIAL);  // 第二个参数也可以填 NULL
    // 创建并发队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.myconcurrent", DISPATCH_QUEUE_CONCURRENT);
    // 获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    // 获取 default 级别的全局队列
    dispatch_queue_t globalBackground = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0); // 第一个参数也可以填旧版的 DISPATCH_QUEUE_PRIORITY_DEFAULT
    // 同步提交任务
    dispatch_sync(serialQueue, ^{
        NSLog(@"提交同步任务");
    });
    // 异步提交任务
    dispatch_async(concurrent, ^{
       NSlog(@"提交异步任务");
    });
}

Swift 示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fun GCDTest {
    // 创建串行队列
    let serialQueue = DispatchQueue(label: "com.example.myserial")
    // 创建并发队列
    let concurrentQueue = DispatchQueue(label: "com.example.myconcurrent", attributes: .comcurrent)
    // 获取主队列
    let mainQueue = DispatchQueue.main
    // 获取 default 级别的全局队列
    let globalDefault = DispatchQueue.global()
    serialQueue.sync {
        print("提交同步任务")
    }
    concurrentQueue.async {
        print("提交异步任务")
    }
}

GCD 中的全局并发队列实际上是一组不同 QoS 优先级的并发队列,按优先级由高到低依次执行。

QoS

在计算机科学中,QoS(Quality of Service)是指服务质量的一种度量,用于描述网络和计算资源在一定 时间内能够提供的性能水平。在操作系统和并行编程中,QoS 也用于指定任务的优先级和资源调度策略。不同的 QoS 级别可以影响任务的调度、执行优先级和资源分配。

QoS 的应用领域

  • 网络服务:在网络通信中,QoS 用于确保不同类型的数据流(如视频、音频、文件传输等)在网络上传输时具有不同的优先级和带宽保障,从而提供不同的服务质量。
  • 操作系统和并行编程:在操作系统和并行编程中,QoS 用于指定任务的优先级,影响任务在多核处理器上的调度和执行。

在 iOS 和 macOS 中,QoS 被用于调度线程和任务,以便系统可以根据任务的重要性和紧急程度来分配 CPU 资源。通过合理使用 QoS,可以优化系统资源的使用,提升用户体验,并有效管理电源。Apple 的 GCD(Grand Central Dispatch)和 NSOperationQueue 框架都支持 QoS。

QoS 等级

在 iOS 和 macOS 中,QoS 级别分为以下几个等级,从最高优先级到最低优先级依次是:

  • User Interactive: 用于需要立即执行的任务,例如更新 UI 和响应用户交互。这类任务应尽可能快地执行,以确保流畅的用户体验。
  • User Initiated: 用于用户主动请求的任务,需要快速完成但不是立即需要的,例如打开文档或加载数据。
  • Utility: 用于可能需要一些时间完成的任务,例如下载文件或处理数据。通常是用户可以等待完成的任务。
  • Background: 用于对用户不可见的任务,例如同步数据、预取内容。这类任务优先级最低,在系统资源充足时执行。
  • Default: 系统默认的 QoS 级别,通常介于 User Initiated 和 Utility 之间。

使用 QoS

在 GCD 中,可以在创建队列或提交任务时指定 QoS 级别:

Objective-C 示例

1
2
3
4
5
6
7
8
// 创建具有 User Initiated 级别的 QoS 的并发队列
dispatch_queue_attr_t myAttr = dispatch_queue_attr_make_with_qos_class(QOS_CLASS_USER_INITIATED, QOS_CLASS_DEFAULT, 0);
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.myconcurrent", myAttr);

// 向 Background 级别的 QoS 的全局队列提交任务
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
    NSLog(@"Task is executing");
});

在引入 QoS 类之前,Objective-C 中使用调度队列优先级来确定任务的执行顺序:

  • DISPATCH_QUEUE_PRIORITY_HIGH: 高优先级。
  • DISPATCH_QUEUE_PRIORITY_DEFAULT: 默认优先级。
  • DISPATCH_QUEUE_PRIORITY_LOW: 低优先级。
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND: 后台优先级。

在现代 macOS 和 iOS 系统中,Apple 建议使用 QoS 类而不是调度队列优先级,因为 QoS 类提供了更细粒度的控制,并且更符合现代应用的需求。

Swift 示例

1
2
3
4
5
6
7
// 创建具有 User Initiated 级别的 QoS 的并发队列
let queue = DispatchQueue(label: "com.example.myqueue", qos: .userInitiated)

// 向 Background 级别的 QoS 的全局队列提交任务
DispatchQueue.global(qos: .background).async {
    print("Task is executing")
}

DispatchWorkItem

DispatchWorkItem 是 Grand Central Dispatch (GCD) 提供的一个类,用于封装可以在队列中执行的工作单元。DispatchWorkItem 提供了更强大的功能和灵活性,可以用来控制和管理并发任务。它支持任务的取消、优先级和 QoS(Quality of Service),以及任务完成后的通知回调。效果上类似于 Objective-C 中的dispatch_block_t

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 创建一个 DispatchWorkItem
let workItem = DispatchWorkItem {
    print("Work item is executing")
    Thread.sleep(forTimeInterval: 2) // 模拟耗时任务
    print("Work item finished")
}

// 提交任务到全局并发队列
DispatchQueue.global().async(execute: workItem)

// 取消任务
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
    workItem.cancel()
    print("Work item cancel requested")
}

// 设置完成后的通知回调
workItem.notify(queue: DispatchQueue.main) {
    print("Work item has completed or was cancelled")
}

Objective-C 没有 DispatchWorkItem,但可以使用 dispatch_block_t 和相关的 GCD 函数实现类似的功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 创建一个 dispatch_block_t
dispatch_block_t block = dispatch_block_create(0, ^{
    NSLog(@"Block is executing");
    [NSThread sleepForTimeInterval:2]; // 模拟耗时任务
    NSLog(@"Block finished");
});

// 提交任务到全局并发队列
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);

// 延迟1秒后取消任务
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_block_cancel(block);
    NSLog(@"Block cancel requested");
});

// 设置完成后的通知回调
dispatch_block_notify(block, dispatch_get_main_queue(), ^{
    NSLog(@"Block has completed or was cancelled");
});

notify

DispatchWorkItem 和 dispatch_block_t 中的 notify 是 GCD 提供的一个强大功能,能够在指定任务执行完成或被取消之后执行一个通知回调。

使用场景

  • UI 更新:任务完成后在主线程中更新 UI。
  • 任务依赖:确保一个任务在另一个任务完成后执行。
  • 资源清理:任务完成后执行资源清理操作。

注意事项

  • 队列选择:确保回调代码在适当的队列中执行,避免在非主线程中更新 UI。
  • 任务取消:即使任务被取消,notify 回调也会被执行。因此,回调代码中应检查任务是否被取消。

终~