关闭

IOS并发编程——Grand Center Dispatch

标签: ios并发gcd
535人阅读 评论(0) 收藏 举报
分类:

并发编程往往能够提高程序的效率,在其他平台中进行并发编程往往就是多线程的编程,在IOS中同样可以进行多线程编程,但是Apple的官方文档却告诉我们,尽量不要使用原生线程,而是使用其他替代技术。为什么呢?有如下几点理由:

1、原生线程编程往往需要涉及同步,线程资源获取释放等操作,相对复杂。

2、原生多线程编程线程切换运行由人为控制,不如直接交给操作系统来管理线程效率高(操作系统会根据系统实时状况灵活操作多线程)。

3、每一次对原生线程的操作,都要进行内核层的操作。而内核层操作花费时间大。而由操作系统代劳的并发操作只会在必要时机切换到内核层。

OK,既然将并发操作交给系统有那么多好处,那我们就来了解一下IOS系统的并发编程。

IOS并发编程技术 包括

  • Dispatch Queues
  • Dispatch Source
  • Operation Queues
首先,我们来了解下Dispatch Queues,即GCD。


什么是GCD

Grand Central Dispatch (GCD) ,大中枢派发。是Apple系统的一种执行task block的技术。这些task block通过提交给系统或自创建的Dispatch queue,由系统来并发或顺序的执行。
这里涉及到两点:
1、我们要执行的任务——task block,
2、要让我们的任务执行,需要将我们的task block提交到dispatch queue中。要并发的执行,就将task block提交到并发dispatch queue,要顺序执行,就将任务提交到顺序dispatch queue中。
之后,我们就让系统来给我们代劳代码执行,并发任务切换等活啦~ 怎么样,简单吧。
下面,我们就根据这两点:task block, dispatch queue来深入了解GCD。

block object(task block)

task block,即block object。类似于C语言中的函数指针,它是一个C风格代码块,可以用于OC,C,C++代码中。其中包含了独立的代码运行单元。当然,它的用途并不仅限于GCD,我们可以像使用函数指针一样,来使用block object。
block object本质上是一个heap上的数据结构,类似于普通的OC对象,由编译器产生并管理。
block object的使用如下,它可以接受参数,并有返回值。
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 object接受的参数

block 可以接受外界传入的参数,默认的,参数会被copy到block结构体中,即外界的传入参数与block内部的参数具有相同的value却在不同的内存地址分配。
但这要求我们对传入参数做只读操作。
如果对参数进行了改变,编译器会提示我们在外部的参数前加上__block前缀,加上__block前缀后,在block中,参数不再是按值传递,而是操作的外部参数本身,即同一块内存地址。由于我们没有提供相应的同步条件,因此要求block的执行必须是synchronously的。
可以结合下面代码来理解关于block的参数传递
代码:
NSLog(@"%d", &a);
    
    dispatch_sync(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
        
        NSLog(@"%d", &a);
    });
   
    NSLog(@"%d", &a);

输出:


代码:
int a = 13;
    NSLog(@"%d", &a);
    
    dispatch_sync(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
        
        NSLog(@"%d", &a);
    });
   
    NSLog(@"%d", &a);

输出:

关于block设计的指导方针

1、对于asynchronously执行的block,应当避免接收由外界创建并释放的大数据结构或指针类型的变量,因为当你的block开始执行时,所传入的指针指向的那块内存可能已经被释放掉。
2、对于提交到dispatch queue中的block,系统会自动引用block,并在执行完block后将其释放,无需人工干涉。
3、虽然比原生线程,创建block的花销更小,但是其也是要消耗系统资源的。可以考虑用内联函数代替block。
4、不用直接在多个block间共享变量。如果是属于同一个dispatch queue的block,可以利用dispatch queue的context pointer来共享数据。Storing Custom Context Information with a Queue.
5、虽然dispatch queue会自动释放内存,但并不保证内存释放的时机。若我们在block中创建了OC变量,最好有我们自己同时创建autorelease pool来释放内存。


Dispatch Queues

要想让我们的task block,还必须将task block提交到对应的dispatch queue中。dispatch queue类似于一个工作队列,系统采用FIFO的原则,对提交到dispatch queue中的task block进行处理。

Dispatch queue分类

1、Serial queue, 想让task block顺序执行,则将task block提交到Serial queue。
2、Concurrent queue,想让task block并发执行,则将block提交到Concurrent queue。
3、Main dispatch queue,是App全局的Serial queue,负责运行程序主线程上的任务并处理程序UI事件等,同时,也会处理你提交到main dispatch queue中的任务。注意,由于其和UI响应相关,应注意不要使自己提交的task阻塞main dispatch queue而导致程序界面停止响应。
注意,不管是Serial queue还是Concurrent queue,均遵循FIFO的原则,只不过Serial queue是在等待上一个task block完成后才放出下一个task block,而Concurrent queue则不管上一个task block是否完成,均会在适当的时间将task block放出。

获取Dispatch queue

获取Dispatch queue有两种方式:
1、直接获取系统提供的dispatch queue
2、创建自己的dispatch queue

获取系统dispatch queue

IOS系统为我们提供了现成的Dispatch queue,多数情况下,我们在需要时直接拿来用即可。
获取System 定义的的全局Concurrent queue
dispatch_get_global_queue
dispatch_queue_t dispatch_get_global_queue ( long identifier, unsigned long flags );

该函数会返回一个系统全局的concurrent queue,因为该queue是系统管理的,因此我们调用
dispatch_suspenddispatch_resume, 或 dispatch_set_context等函数操作该queue时,是无效的。

获取与程序main thread 相关的Serial queue

dispatch_get_main_queue

创建自己的Dispatch queue

可以通过函数

dispatch_queue_create

 dispatch_queue_t dispatch_queue_create ( const char *label, dispatch_queue_attr_t attr ); 

创建自己的Dispatch queue。label为Dispatch queue名称,而attr可以设置为DISPATCH_QUEUE_SERIAL (or NULL)创建serial queue,DISPATCH_QUEUE_CONCURRENT来创建concurrent queue。


Dispatch queue的内存管理

除了上述系统管理的global queue与main queue不需要我们操心外,我们需要对自己创建的queue进行引用计数管理。即使在我们在创建具有垃圾回收功能的程序,而dispatch queue不支持垃圾回收机制。
dispatch queue的计数管理函数如下:
dispatch_retain
dispatch_release

在dispatch queue中存储自定义信息

我们可以通过调用函数
dispatch_set_context
dispatch_get_context

在dispatch queue中设置,提取自定义数据。系统并不会理会这些数据,因此,我们需要手动来分配释放或解引用OC对象。而这一个操作,可以放在dispatch queue的finalizer function中执行(类似于类的析构函数)。

Dispatch queue的finalizer function

对于dispatch queue,我们可以设置finalizer function,让Dispatch queue的引用计数为0时,自动调用该函数。注意,仅当我们为Dispatch queue设置了自定义数据时,finalizer 函数才会被调用。

设置Dispatch queue自定义数据及finalizer function实例:
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);

    if (serialQueue)

    {

        dispatch_set_context(serialQueue, data);

        dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);

    }

 

    return serialQueue;

}




将task block提交到Dispatch queue中

将task block提交到dispatch queue有两种方式,同步提交与异步提交,分别对应的函数为
同步提交
dispatch_sync 
dispatch_sync_f

异步提交
dispatch_async 
dispatch_async_f

当然,我们应优先使用异步提交到Dispatch queue,因为同步提交会使我们的线程阻塞直到提交的block完成为止。同时,对于同步提交,可能会造成死锁现象,如在提交的Dispatch serial queue中的task block中再次调用同步提交block到同一个Dispatch serial queue,则必定会造成死锁。对于concurrent queue,Apple官方文档也不建议在同一个queue中调用同步提交函数。

关于GCD的其他

利用并发queue优化循环操作

Apple文档还提到,当我们在使用循环时,如果每次循环的结果间是相互独立并且循环的执行顺序没有要求,那么可以使用
dispatch_apply
dispatch_apply_f
函数来优化循环操作。这两个函数会根据循环次数,提交若干block或函数到指定的concurrent queue中,这样,每个循环逻辑就能够并发的执行了。官方示例如下:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

 

dispatch_apply(count, queue, ^(size_t i) {

   printf("%u\n",i);

});

当然,这种优化并不是一定的,毕竟操作queue也是需要系统开销的,应根据具体情况而定。
Queue Task的内存管理
Queue Task自带autorelease pool,因此所有的OC对象均会自动释放,但系统并不保证何时会自动释放这些对象。因此,如果我们的程序对内存使用还是比较敏感,应自己再写需要的autorelease pool。

暂停与恢复Queue

我们可以控制Dispatch queue的暂停与恢复,它们分别对应如下函数:
dispatch_suspend
dispatch_resume
上面两个函数会对应的增加或减少Dispatch queue的暂停引用计数。因此,要让Dispatch queue恢复运行,还需要调用与dispatch_suspend相对应次数的dispatch_resume函数。

通过Dispatch Semaphores来代替传统的Semaphores

类似于传统信号量,我们也可以通过Dispatch Semaphores来对于关键资源的访问进行控制。但是GCD使用的Semaphores更为高效,因为GCD仅当真正需要阻塞线程来等待信号量时才会真正与系统内核层交互,减少了系统消耗。 系统运用dispatch semaphores步骤如下:
  1. When you create the semaphore (using the dispatch_semaphore_create function), you can specify a positive integer indicating the number of resources available.

  2. In each task, call dispatch_semaphore_wait to wait on the semaphore.

  3. When the wait call returns, acquire the resource and do your work.

  4. When you are done with the resource, release it and signal the semaphore by calling thedispatch_semaphore_signal function.

以下是Apple的官方实例
// 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 group来等待Dispatch queue中的任务结束

有时候,我们的下一步操作需要上一步任务的结果,但在并发执行的情况下,我们就需要在程序的某个点停下来,等待或询问那个并发执行的任务是否已经完成。对于提交到concurrent queue中的任务,IOS提供了Dispatch group来进行这种等待或询问。
官方实例如下:
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 group进行等待,一般步骤如下
1、同往常一样,获取concurrent queue。
2、创建Dispatch group对象。
3、通过dispatch_group_async函数,将任务提交到Dispatch queue中,同时,将task,dispatch queue与dispatch group相联系起来。
4、在适当的时刻,调用dispatch_group_wait函数等待提交到dispatch queue中的任务完成。

Dispatch queue与线程安全

虽然Dispatch queue由系统控制,但是对于dispatch queue的线程安全,Apple还是希望你知道如下几点:
1、Dispatch queue本身是线程安全的。即是说,你可以在多个线程中同时操作dispatch queue,而不必进行线程同步操作。系统会为你代劳。
2、如前面提到过的,不要在同一个serial queue中调用dispatch_sync函数提交任务,这显而易见会造成死锁
3、尽量不要在提交的任务中使用锁,这样会降低dispatch queue的效率。可能的话考虑使用 serial queue来代替锁。
4、最后一条不是很理解是为什么,先抄录在这里吧
Although you can obtain information about the underlying thread running a task, it is better to avoid doing so. For more information about the compatibility of dispatch queues with threads, seeCompatibility with POSIX Threads.

参考文献:
https://developer.apple.com/library/mac/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102-SW23

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:62269次
    • 积分:1254
    • 等级:
    • 排名:千里之外
    • 原创:61篇
    • 转载:6篇
    • 译文:0篇
    • 评论:18条
    博客专栏
    最新评论