Dispatch Queues

774 阅读10分钟

Concurrency Programming Guide

About Dispatch Queues

  • 能够使得应用同步或是异步执行任务
  • 通过将相应的代码放在函数或块对象内并将其添加到调度队列来执行任务
  • 所有的调度队列都是先进先出的数据结构, 任务开始执行顺序是按照添加到队列的顺序

派发队列的类型

  • Serial

    • 串行队列, 也称私有派发队列, 按照任务添加顺序依次执行任务, 执行的任务在独立的线程中执行, 执行的线程由队列管理, 串行队列一般是对特定资源的同步访问
    • 可创建任意数量的串行队列, 队列每次只能执行一个任务, 但是各个队列间是并发执行的
  • Concurrent

    • 也称为全局派发队列的一种队列, 并发的执行一个或多个任务, 任务也是按照添加顺序开始执行, 每个任务运行在独立的线程中, 线程由队列管理, 任务线程分配执行由系统的条件决定
    • 在iOS5以后, 可以指定类型DISPATCH_QUEUE_CONCURRENT创建并发队列, 此外应用提供了四种预制的全局并发队列可以使用
  • Main dispatch queue

    • 全局的串行主队列, 在主线程中, 串行地执行任务
    • 主队列协同Runloop一起工作, 除了执行任务还处理run loop接收到的事件消息的任务
    • 常作为应用的关键同步点
    • 不用主动创建

派发队列的关键点

  • 派发队列执行任务的时候相对于其他队列是并行执行的, 串行执行的任务仅限于一个单一的派发队列
  • 系统决定了某一时段执行任务的总数, 100个任务在100个队列中不代表100个任务都会并发执行
  • 系统会依据队列的优先级选择开始执行那个任务
  • 添加到队列的任务必须是就绪状态(都准备好了,就差CPU资源了)才能够被执行, 注意Cocoa operation对象的使用
  • Private dispatch queues是引用计数的对象, 注意 dispatch sources可以增加queue的引用计数, 确保所有的dispatch sources被取消, 要适当的调用release

Queue-Related Technologies

  • Dispatch groups
    • 用来监测/观察一组block对象任务的执行完毕, 可以同步或是异步监测, 这个取决于需求
  • Dispatch semaphores
    • 派发信号量和传统信号量作用相似(临界区控制同步/控制并发数啥的), 但是更加简便高效, 当信号量不可用时, 调用线程会被阻塞, 此时Dispatch semaphores会去调用内核, 与内核发生交互, 信号量可用时, 与内核的交互不会发生, 可以使用信号量管理有限的资源访问
  • Dispatch sources
    • Dispatch sources能够产生系统特定事件响应的通知, 使用Dispatch sources监测各种事件时候, 例如进程通知, 信号, 描述符事件等等,Dispatch sources异步的提交任务到特定的派发队列来执行

Implementing Tasks Using Blocks

  • 使用Blocks能够容易的定义任务单元
  • Blocks看起来像指针, 实际上由底层的数据结构来表示, 由编译器创建和管理
  • 编译器打包Block代码(以及所有相关数据),并将其封装为可存在于堆中并在应用程序中传递
  • 最主要的优点是Block可以捕获外部变量, 捕获的变量在block内是只读的, __block定义的变量在block内操作会影响到变量的值

block的简单定义

int x = 123;
int y = 456;
 
// Block declaration and assignment
void (^aBlock)(int) = ^(int z) {
    printf("%d %d %d\n", x, y, z);
};
 
// Execute the block
aBlock(789);   // prints: 123 456 789

设计Block的关键点

  • 使用派发队列执行异步任务的时候, block可以安全的使用外部方法或者函数的标量变量, 由调用上下文产生和销毁的大型结构体以及指针变量, block不应当去捕获, 在block执行的时候, 变量指向的内存空间可能已经释放, block中自行创建的内存空间是安全的, 可正常使用
  • 当block添加当派发队列中的时候, 队列对block进行copy操作, 在block任务执行完毕的时候, 执行了release操作, 所以将block添加到队列的时候不用自己进行copy操作
  • 队列执行小任务比原始线程执行更高效, 但创建block和执行block也是有开销的, 所以要比较执行小任务时, 直接执行还是添加到队列中执行那个性能更好
  • 不要缓存来自底层线程的数据, 以及让这些数据在block中访问, 如果同一个队列的任务需要共享数据, 可以使用队列的context指针来进行操作
  • 如果block中产生了大量的OC对象, 考虑使用@autorelease block来管理这些对象的内存, 尽管GCD有自己的自动释放池, 但并不能保证释放池何时释放

Creating and Managing Dispatch Queues

Getting the Global Concurrent Dispatch Queues

  • 系统提供了四种全局的并发队列, 它们仅仅是优先级不同
  • 因为是全局队列, 不需要手动创建
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  • 优先级除了DISPATCH_QUEUE_PRIORITY_DEFAULT, 还有DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_PRIORITY_LOW, DISPATCH_QUEUE_PRIORITY_BACKGROUND
  • dispatch_get_global_queue第二个参数是保留参数, 将来使用的, 传0就可以了
  • 队列虽然是引用计数的对象, 然而因为是全局的队列, 所以不需要进行retainrelease操作

Creating Serial Dispatch Queues

  • 按照特定顺序执行任务使用串行队列, 一次只执行一个任务, 可以替代锁去访问共享资源和可变的数据结构, 异步提交任务到串行队列, 队列就不会死锁
  • 可以创建任意的串行队列, 单也要避免创建太多, 如果要执行并发任务, 还是推荐使用全局并发队列
  • 系统为应用主线程的任务也创建了一个串行队列, 就是主队列
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);

Getting Common Queues at Runtime

  • dispatch_get_current_queue用于调试确定当前的队列, 在提交到队列的block中调用
  • dispatch_get_main_queue获取主队列
  • dispatch_get_global_queue获取任意全局共享的并发队列

Memory Management for Dispatch Queues

  • 派发队列和其他派发对象都是引用计数类型的数据, 如果创建一个串行队列, 它初始的引用计数是1, 按照需要使用dispatch_retain,dispatch_release增加和减少引用计数
  • 要遵循引用计数的内存管理原则
  • 如果实现了垃圾回收管理的应用, CGD对象还是要使用retainrelease, CGD并不支持垃圾回收

Storing Custom Context Information with a Queue

  • 所有的派发对象(包括派发队列), 都可以设置一个关联的上下文数据对象, 使用dispatch_set_contextdispatch_get_context设置和获取关联内容, 系统不会使用上下文内容, 所以需要自行进行创建和释放
  • 上下文存储一个指向Objective-C的对象或者其他数据结构的指针, 用于数据的访问, 可以使用队列的finalizer函数来释放指针指向的数据资源
void myFinalizerFunction(void *context)
{
    MyDataContext* theData = (MyDataContext*)context;
 
    // Clean up the contents of the structure
    myCleanUpDataContextFunction(theData);
 
    // Now release the structure itself.
    free(theData);
}
 
dispatch_queue_t createMyQueue()
{
    MyDataContext*  data = (MyDataContext*) malloc(sizeof(MyDataContext));
    myInitializeDataContextFunction(data);
 
    // Create the queue and set the context data.
    dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL);
    dispatch_set_context(serialQueue, data);
    dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);
 
    return serialQueue;
}

Providing a Clean Up Function For a Queue

  • 在自定义队列销毁的时候可以做一些数据清理工作, 可以使用dispatch_set_finalizer_f函数来指定特定的销毁任务执行, 示例见上面代码👆

Adding Tasks to a Queue

Adding a Single Task to a Queue

  • 异步方式添加任务
    • dispatch_async方法
    • dispatch_async_f方法
  • 同步方式添加任务
    • dispatch_sync方法
    • dispatch_sync_f方法

注意 在串行队列的任务中, 不要再在相同的队列里执行同步任务, 这样会造成死锁

dispatch_queue_t myCustomQueue;
myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL);
 
dispatch_async(myCustomQueue, ^{
    printf("Do some work here.\n");
});
 
printf("The first block may or may not have run.\n");
 
dispatch_sync(myCustomQueue, ^{
    printf("Do some more work here.\n");
});
printf("Both blocks have completed.\n");

Performing a Completion Block When a Task Is Done

  • 使用完成block来执行任务结束后的操作

示例


void average_async(int *data, size_t len,
   dispatch_queue_t queue, void (^block)(int))
{
   // Retain the queue provided by the user to make
   // sure it does not disappear before the completion
   // block can be called.
   dispatch_retain(queue);
 
   // Do the work on the default concurrent queue and then
   // call the user-provided block with the results.
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      int avg = average(data, len);
      dispatch_async(queue, ^{ block(avg);});
 
      // Release the user-provided queue when done
      dispatch_release(queue);
   });
}

Performing Loop Iterations Concurrently

  • 并发执行循环迭代使用dispatch_applydispatch_apply_f函数
  • for循环一样, dispatch_apply/ dispatch_apply_f并发循环也会阻塞当前线程, 直到循环结束才会返回
  • 传入的参数注意不要是当前执行的串行队列/主队列,容易造成死锁
  • 循环迭代需要评估任务量, 太多会降低响应, 太少会影响整体性能
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 
dispatch_apply(count, queue, ^(size_t i) {
   printf("%u\n",i);
});

Performing Tasks on the Main Thread

  • dispatch_get_main_queue函数获取主队列
  • 在主队列中添加任务block, 主线程中执行串行任务

Suspending and Resuming Queues

  • dispatch_suspend挂起队列阻塞任务执行, dispatch_resume恢复任务执行
  • dispatch_suspend会使得队列的引用计数+1, dispatch_resume会使队列引用计数-1
  • 挂起和继续队列是异步操作, 挂起队列不会对正在执行的block生效

Using Dispatch Semaphores to Regulate the Use of Finite Resources

  • 用于控制访问有限资源
  • 获取调度信号所需的时间比获取传统系统信号所需的时间更少
  • 资源不足时会调用内核, 阻塞当前线程以等待信号量有资源

使用信号量的关键点

  • dispatch_semaphore_create创建信号量时, 需要使用一个正值表示资源的可用数量
  • 在每个任务中, 调用dispatch_semaphore_wait等待信号量(看信号量的值是否为正)是否有资源
  • 当调用dispatch_semaphore_wait返回时(表示有资源, 未返回表示线程被阻塞以等待资源), 获得资源, 执行任务
  • 任务执行结束后, 需要释放资源, 调用dispatch_semaphore_signal函数, 表示资源释放可用

示例 打开应用文件, 每个应用都有有限的文件描述符, 如果有一个进程任务就是去处理单量的文件, 但又不想再同一时间把所有的文件都打开, 可以使用信号量来显示文件描述符资源的使用

// Create the semaphore, specifying the initial pool size
dispatch_semaphore_t fd_sema = dispatch_semaphore_create(getdtablesize() / 2);
 
// Wait for a free file descriptor
dispatch_semaphore_wait(fd_sema, DISPATCH_TIME_FOREVER);
fd = open("/etc/services", O_RDONLY);
 
// Release the file descriptor when done
close(fd);
dispatch_semaphore_signal(fd_sema);
  • 当创建信号量时, 用特定的数字(正值)表示可用资源的数量, 执行任务前等待信号量, 即调用dispatch_semaphore_wait函数, 会将信号量的值-1, 如果此时信号量的值变为负值(<0), 函数就会告知内核, 要阻塞住线程
  • 任务执行结束后调用dispatch_semaphore_signal函数, 会将信号量的值+1, 表明资源被释放, 如果此时, 有阻塞的线程正在等在信号量资源(在等待队列中), 此时线程进入就绪队列, 并开始执行

Waiting on Groups of Queued Tasks

  • 派发组可以用来阻塞一个线程, 等待一个多个任务执行完毕, 多使用与任务执行有先后顺序的场景
  • 可以替代使用thread joins的场景, 可以将相应的任务添加到调度组中,然后等待整个组,代替启动几个子线程然后再与每个子线程联接
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
 
// Add a task to the group
dispatch_group_async(group, queue, ^{
   // Some asynchronous work
});
 
// Do some other work while the tasks execute.
 
// When you cannot make any more forward progress,
// wait on the group to block the current thread.
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
 
// Release the group when it is no longer needed.
dispatch_release(group);

Dispatch Queues and Thread Safety

注意点

  • 派发队列本身是线程安全的, 可在任意线程提交任务到队列, 不要使用锁或是其他同步方法
  • 使用dispatch_sync函数时, 不要添加任务到方法调用的所在队列, 这样活造成死锁
  • 避免提交到队列中的任务带有锁, 提交到串行队列的任务未获得锁资源, 会阻塞住串行队列, 对于并发队列, 可能会使得其他任务被阻塞执行, 如果想要同步, 可以使用串行队列
  • 避免访问底层线程的信息

理解如有错误 望指正 转载请说明出处