zoukankan      html  css  js  c++  java
  • 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
    ",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

  • 相关阅读:
    与灵感之源的vb.net对应的SmartExcel的C#版本
    winform下提高control在UI中的响应速度
    做了一个petoolkit的gui版本,放几张图上来。
    Reflection中对于out类型的获取
    对企业管理软件“代码后”问题的思考
    看了dannyr的java中文类,自己写了一个。
    所有的WMI可以使用的class
    庆祝浪潮集团成为微软在中国的第四家全球战略合作伙伴!(暂时放首页两天)
    一种系统间集成的同步事务异常处理方法和装置
    一种实现多线并行拣选的方法
  • 原文地址:https://www.cnblogs.com/zhouyi-ios/p/5223968.html
Copyright © 2011-2022 走看看