Dispatch Queues调度队列

Posted Beche

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Dispatch Queues调度队列相关的知识,希望对你有一定的参考价值。

前言-死锁案例

// 在主线程中执行
dispatch_queue_t queueMain = dispatch_get_main_queue();
dispatch_sync(queueMain, ^{
        NSLog(@"+++++++");
});
NSLog(@"hahahaha");

案例分析:运行结果是程序阻塞在dispatch_sync()处。由于main线程执行到dispatch_sync()处,线程处于等待状态。将block任务块添加到主串行队列最后,block等待当前任务(即正在主线程中执行的任务)执行完毕,而当前任务因为阻塞无法结束,导致两边都在等待,所以出现死锁。

 

一、简介

Dispatch queues是一种异步和并发执行任务的简单方式,将任务通过function或者block object的方式添加到dispatch queue。使用Dispatch queue还是使用thread?Dispatch queue中只需要封装任务,再将任务加到合适的Disptch queue,由系统去管理线程。如果用thread,需要做的工作就多了。使用Dispatch queue实现同步,实现效果比加锁要有效。

由dispatch queue管理提交的任务,所有任务遵从先进先出的规则,先添加到dispatch queue中的任务会最先开始执行。GCD提供了一些现成的dispatch queue,或者也可以自己定制dispatch queue。dispatch queue可分为3类:

1、Serial queues串行队列

serial queues(private dispatch queue)按dispatch queue中的顺序一个时间执行一个任务,常用于同步的获取指定资源。你可以随意创建serial queues,多个serial queues之间是并行的。换句话说,创建了4个serial queues,单个queue中的任务串行,queue与queue中的任务是并行的。

2、Concurrent queues并发队列

concurrent queues(global dispatch queue)并发的执行一个或多个任务,但也是按照先进先出的顺序开始启动任务。在某个时间点,可执行任务数量的上限取决于系统状态。在ios5以后,可以通过函数dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)获取4中queue,也可以通过以下代码创建:

dispatch_queue_t queue = dispatch_queue_create("com.dispatch.concurrent", DISPATCH_QUEUE_CONCURRENT)

3、Main dispatch queue主队列

main dispatch queue(globally available serial queue)中的任务在主线程上串行执行。此queue和应用的run loop配合工作,主线程中在执行run loop的事件源后,插入queue任务。main dispatch queue为应用的主要同步queue。尽管不需要自己创建main dispatch queue,但使用时要注意。

 

二、与Dispatch Queues相关的技术

1、Dispatch groups

2、Dispatch semaphores

3、Dispatch sources(specific types of system events)

 

三、Block任务

1、block使用准则:

1)dispatch queue中的blocks异步地执行,最好在block只捕捉上下文中纯变量,不要试图捕捉结构体或者其它基于指针的变量,这些变量由调用上下文来分配和回收。原因是在block执行中,捕获的变量内存可能已经被回收。当然有解决方案,比如为该对象分配内存,然后blcok指向并retain当前内存。

2)dispatch queue会copy添加到其中的block。等任务结束再释放block。换句话说,在添加到queue时不需要明确写copy代码。

3)尽管在执行小任务时,queues调度执行任务比原始的threads更有效,但调度和执行block仍然有开销。如果block中的任务轻,直接执行反而更高效。

4)不要缓存与基础线程相关的数据,不要试图从其它的block中获取数据。如果希望同一个queue中的数据共享,使用dispatch queue的context指针来实现。

5)如果block内部创建了很多的OC对象,将block代码加到@autorelease块中,这样能更及时释放不再使用的对象。尽管dispatch queue中带有自动释放池,但不保证会及时回收那些OC对象。

 

 四、创建和管理Dispatch Queue

1、获取Global Concurrent Dispatch Queues。

每个应用程序有4个global concurrent dispatch queues,他们之间只有优先级的区别,包括的优先级有:DISPATCH_QUEUE_PRIORITY_HIGHDISPATCH_QUEUE_PRIORITY_DEFAULT、DISPATCH_QUEUE_PRIORITY_LOW、DISPATCH_QUEUE_PRIORITY_BACKGROUND。尽管dispatch queue存在引用计数,但不需要retain/release该队列,因为他们在应用中是全局可访问的,只需要通过方法获取该queue即可,代码如下:

dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

2、创建Serial Dispatch Queues

dispatch_queue_t queue = dispatch_queue_create("com.example.MyQueue", NULL);

第一个参数是队列的名称,第二个暂时固定为NULL。

3、获取runtime的Common Queues

1)dispatch_get_current_queue()获取当前队列。如果在block内部调用,则返回的是block添加到的queue。如果是block外部调用,则返回当前queue。

2)dispatch_get_main_queue()获取主线程队列。

3)dispatch_get_global_queue()获取全局并发队列。

4、Dispatch Queue的内存管理

并发队列和主队列都不需要考虑内存管理的问题,只需要考虑自己定义的串行队列。通过dispatch_retain和dispatch_release方法增加和减少队列对象的引用计数器。

见到过在dealloc方法中,调用dispatch_release释放自定义的串行队列。

5、Dispatch Queue中数据存储-Context

dispatch queue能通过dispatch_set_context函数关联context data,通过dispatch_get_context方法来获取context data,context data存储了指向OC对象或者是数据结构的指针。context data需要手动分配和释放内存,可使用queue的finalizer函数完成释放。代码如下:

dispatch_queue_t storeData()
{
    MyDataContext *contextData = (MyDataContext*) malloc(sizeof(MyDataContext));
    myInitializeDataContextFunction(contextData);
 
    // 创建串行队列,设置context data
    dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL);
    dispatch_set_context(serialQueue, contextData);

    // 回收context data
    dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);
 
    return serialQueue;
}

void myFinalizerFunction(void *context)
{
    MyDataContext* contextData = (MyDataContext*)context;
 
    // 清理内容结构
    myCleanUpDataContextFunction(contextData);
 
    // 释放
    free(contextData);
}

 

五、往Dispatch Queues中添加任务

1、添加单一任务

dispatch_sync()同步调用,会阻塞方法所在线程。

dispatch_async()异步调用,不会阻塞线程。

 

dispatch_barrier_async()。等待前面的任务全部结束,该task才能执行,然后后面的任务才能启动执行

dispatch_once(&onceToken,^{});  // static dispatch_once_t onceToken;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(2.0*NSEC_PER_SEC)),dispatch_get_main_queue(), ^{});// 将2秒后需执行的任务......

2、Completion Block

在dispatch queue中的某个任务执行结束后,通知做些后续操作。不同于回调机制,dispatch queue采用completion block实现。具体操作是,在其任务最后添加将completion block提交到指定queue中。

//  myBlock和myQueue分别就是completion block和提交至的queue
void sum_async(int data01, int data02, dispatch_queue_t myQueue, void (^myBlock)(int))
{
   // Retain the queue provided by the user to make sure it does not disappear before the completion block can be called.
  dispatch_retain(myQueue);
 
   // Do the work on the default concurrent queue, then call the user-provided block with the results.        
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    int sumValue = data + len;
    dispatch_async(myQueue, ^{ myBlock(sumValue); 
  });
 
  // Release the user-provided queue when done
  dispatch_release(myQueue);
  });
}

3、并发执行循环迭代dispatch_apply()

在执行指定次数的循环中,且单个循环间彼此独立、执行的先后顺序无关紧要。这时不应该再采用最初的for循环依次遍历,而应考虑通过concurrent dispatch queue提高性能。有个函数dispatch_apply(),能一次性的将所有循环提交给queue,当提交给concurrent queue时,就能同一时间执行多个循环。

当然也可以将任务提交到serial queue,但没什么优势和意义,不建议。

注意:跟普通的循环一样,dispatch_apply函数需等所有的循环运行结束后才会返回。因为会堵塞当前线程,在主线程中要慎用,避免执行所有循环阻碍主线程及时响应事件。如果循环要求比较长的处理时间,你应该在其它线程上调用dispatch_apply。

避免出现死锁场景:执行dispatch_apply所在线程的queue和入参中的queue是同一个,且queue是serial queue,那么线程执行到dispatch_apply函数时,需等待所有循环运行结束,而serial queue中的任务又只执行到dispatch_apply,得等当前任务执行完毕,双方互相等待彼此结束任务。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// count是总共循环次数,i是当前循环数
dispatch_apply(count, queue, ^(size_t i) {
   printf("%u\n",i);
});

4、在主线程上执行任务

GCD提供的一个特殊的main dispatch queue,其中的任务在主线程中串行。可通过dispatch_get_main_queue函数可获取该queue,它由应用自动提供且自动回收,给主线程创建了一个run loop。

如果创建的不是cocoa应用程序,也不想明确创建run loop,那必须调用dispatch_main函数去明确执行main dispatch queue中的任务,否则,queue中的任务永远不会执行。

5、在任务中使用OC对象

GCD建立在cocoa内存管理的基础上,可以在任务中任意使用OC对象,每一个dispatch queue都拥有自己的autorelease pool。但如果应用内存要求严格,且在任务中添加了很多的autoreleased对象,想要及时释放这些对象,创建自己的autorelease pool。

 

六、暂停和恢复Queue

通过调用dispatch_suspend函数暂时停止queue中的任务执行,调用dispatch_resume函数恢复执行。有一个suspension reference count,当暂停时该引用计数增加,当恢复时引用计数减少,为0时,queue是暂停状态。因此,必须平衡调用suspend和resume方法。

注意:这两个方法都是异步执行,在queue中任务之间生效,也就是说暂停不会让正在执行的任务中止,中止是下一个任务。

 

7、使用Dispatch Semaphore来有效使用有限资源

给某个有限资源,指定能同时访问的最大数量,可以通过dispatch semaphore实现。dispatch semaphore跟大多数semaphore一样,除了获取dispatch semaphore比系统的semaphore要快。这是因为GCD一般不访问内核,只有在资源不可用时,才会去内核调用,系统暂停线程以待semaphore发出信号。使用如下:

1、通过dispatch_semaphore_create函数创建semaphore,可以选择指定一个正整数来显示一次可访问上限。

2、在每个任务,通过dispatch_semaphore_wait函数等待信号量。

3、当信号量发出信号,就能获取资源执行任务。

4、当使用资源结束后,通过dispatch_semaphore_signal函数释放信号并发出信号。

这些步骤如何工作可参考系统的file descriptor。每个应用提供了有限的file descriptors,如果某个任务是处理大量的文件,你不想一次打开这么多文件以至于耗尽file descriptors,这时,就可以采用semaphore去限制使用file descriptors的数量,使用部分代码如下:

// 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);

 

八、Group Queue组任务队列

 dispatch group阻塞线程直到一个或多个任务执行结束。可用的场景是等待所有指定任务完成后,再执行某任务。另一个类似于使用dispatch group的是thread join,两者实现方式不同。

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和线程安全

任何时候实现并发,都需要知道以下几点:

1、dispatch queue本身是线程安全的,换句话说,可以从任何线程提交任务到queue中,不需要考虑queue的锁和同步问题。

2、dispatch_sync调用所在的queueA,与入参queueB,如果queueA和queueB是同一个queue,那么会造成死锁的情况。使用dispatch_async函数就不会。

3、避免在任务中使用锁。如果是串行队列,如果获取不到锁,会阻塞整个队列中的任务执行。如果是并行队列,获取不到锁,会阻塞队列中其它任务的执行。所以需要同步执行代码,使用serial dispatch queue代替锁。

4、尽管可以获取任务所在基础线程信息,最好不要这样做。

 

十、该博客翻译自苹果官方文档

https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW8

以上是关于Dispatch Queues调度队列的主要内容,如果未能解决你的问题,请参考以下文章

Lavel 5.x与Redis Queues生成大量日志

来自dispatch_async全局崩溃的C函数调用,但在主队列上工作

dispatch

如何在 Swift 3 中创建调度队列

调度队列时应用程序阻塞

iOS:未发布的调度队列