coding with objc & swift

一起来构建Dispatch Groups

| Comments

Dispatch Group是一个方便的用于同步多任务的工具,一个匿名的读者建议将它作为今天的「一起来构建」的主题。

回顾

Dispatch group提供了4种基本的操作:

  1. 进入(Enter),表明任务开始。
  2. 退出(Exit),表明任务完成。
  3. 通知(Notify),用于在所有任务完成后调用一个block。
  4. 等待(Wait),有点像notify,但是是同步的。

你可以用它来分拆一堆并行操作,等待它们完成:

1
2
3
4
5
6
7
8
9
dispatch_group_t group = dispatch_group_create();
for(int i = 0; i < 100; i++)
{
   dispatch_group_enter(group);
   DoAsyncWorkWithCompletionBlock(^{
       dispatch_group_leave(group);
   });
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

你也可以用它来在所有操作完成后异步的调用一个block:

1
2
3
4
5
6
7
8
9
10
11
dispatch_group_t group = dispatch_group_create();
for(int i = 0; i < 100; i++)
{
   dispatch_group_enter(group);
   DoAsyncWorkWithCompletionBlock(^{
       dispatch_group_leave(group);
   });
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
   UpdateUI();
});

考虑到它是GCD的一部分,并且我们正在谈论的也是异步操作,应该不言而喻的是dispatch group的一大特点就是,所有的操作都是线程安全的。

代码

与往常一样,我已经将我这个重新实现版本的完整代码发布到了Github:

https://github.com/mikeash/MADispatchGroup

接口

ma_dispatch_group的API高度模仿了dispatch_group

1
2
3
4
5
6
7
8
typedef struct ma_dispatch_group_internal *ma_dispatch_group_t;

ma_dispatch_group_t ma_dispatch_group_create(void);
void ma_dispatch_group_destroy(ma_dispatch_group_t group);
void ma_dispatch_group_enter(ma_dispatch_group_t group);
void ma_dispatch_group_leave(ma_dispatch_group_t group);
void ma_dispatch_group_notify(ma_dispatch_group_t group, void (^block)(void));
void ma_dispatch_group_wait(ma_dispatch_group_t group);

和dispatch_group的接口只有几个不同的地方:

  1. ma_dispatch_group_t并不是一个dispatch对象,所以它并不使用retain/release等语法,而是使用一个单一的destroy函数来进行清理操作。
  2. 没有dispatch_group_async函数。这里只是围绕enterleavedispatch_async进行了简单的封装,因此并没有实现所有的接口。
  3. notify函数没有dispatch queue参数,而是立即执行了block。它只是简单的将block包装到了一个dispatch_async里面,没有太大的变化。
  4. wait函数没有timeout参数。这使得在阐明整体概念的同时也大大简化了代码。

字段

结构体ma_dispatch_group_internal包含了两个字段,一个counter和一个action block:

1
2
3
4
struct ma_dispatch_group_internal {
   uint32_t counter;
   void (^action)(void);
};

counter跟踪记录了没有匹配exitenter函数被调用的次数。action block是通过notify函数设置的动作。

创建和销毁

创建一个新的group是非常简单的。分配一个内存块,用calloc确保其被零初始化:

1
2
3
4
5
ma_dispatch_group_t ma_dispatch_group_create(void)
{
   ma_dispatch_group_t group = calloc(1, sizeof *group);
   return group;
}

销毁同样简单。我假定所有通过notify设置的操作(action)都会在group销毁前被触发,并且销毁该action block也属于销毁操作的一部分。因此,在destroy里面除了简单地调用一下free函数,并没有什么其他的需要清理。

1
2
3
4
void ma_dispatch_group_destroy(ma_dispatch_group_t group)
{
   free(group);
}

Enter

ma_dispatch_group_enter的实现非常简单。它只是一个原子增量操作,使用内建的原子编译器:

1
2
3
4
void ma_dispatch_group_enter(ma_dispatch_group_t group)
{
   __sync_fetch_and_add(&group->counter, 1);
}

使用内建的原子操作确保了线程安全。ma_dispatch_group_leave的实现要稍微复杂一点。它首先执行了一个原子递减操作:

1
2
3
void ma_dispatch_group_leave(ma_dispatch_group_t group)
{
   uint32_t newCounterValue = __sync_sub_and_fetch(&group->counter, 1);

内建函数__sync_sub_and_fetch先执行一个原子递减,然后返回计数器的新值。如果是最后一次调用leavenewCounterValue的值会是0,这就是执行group的通知操作的时候了。

1
2
   if(newCounterValue == 0)
   {

这个时候,action可能还不存在,例如在所有enter都有成对的leave之前调用了notify,所以要检查一下:

1
2
   if(group->action)
   {

如果已经设置了action,则执行:

1
       group->action();

执行完成后,销毁掉block并把action设置为NULL:

1
2
3
4
5
       Block_release(group->action);
       group->action = NULL;
   }
   }
}

Notify

ma_dispatch_group_notify的实现很有趣,但最终也非常简单。从概念上讲,这里有两种完全不同的情况需要考虑:

  1. 仍然有enter在等待对应的leave被调用。在这种情况下,设置group的action block。
  2. 所有enter其对应的leave都已经被调用。在这种情况下,立即执行action block。

似乎很简单。然而,第一种情况简单实现会产生一个竞争条件。考虑一下以下的时间顺序:

  1. notify函数检查count并发现它是一个非零值。
  2. 待执行的操作调用leave,count被减少至0。
  3. 将count减少至0的操作检查发现action还没有被设置,所以什么也不做。
  4. notify函数设置group的action。
  5. action永远不会被执行,因为没有剩下任何代码来执行它。

这里有一个简洁优雅的解决方案,即修复了竞争条件问题,又用同一段代码整合了上面提到的两种完全不同的情况。该解决方案是将action的赋值操作包在一个enter/leave对之间。这有效地消除第二种情况,因为在对action赋值的时候总有至少一个待平衡的enter存在。也由于赋值操作之后还有至少一个leave没有被调用,所以这也同样解决了潜在的竞争条件问题。下面是该函数的样子:

1
2
3
4
5
6
void ma_dispatch_group_notify(ma_dispatch_group_t group, void (^block)(void))
{
   ma_dispatch_group_enter(group);
   group->action = Block_copy(block);
   ma_dispatch_group_leave(group);
}

Wait

ma_dispatch_group_wait的实现概念上很简单,但代码有点复杂。它用ma_dispatch_group_notify来完成大部分的工作。这个想法很简单,就是在调用notify的时候传入block,然后等待该block被执行。秘诀在于如何能够高效地等待。

不关心效率问题仅用轮询也是行得通的。例如,下面就是一个有效的实现,虽然愚蠢:

1
2
3
4
5
6
7
8
9
10
void ma_dispatch_group_wait(ma_dispatch_group_t group)
{
   __block volatile int done = 0;
   ma_dispatch_group_notify(group, ^{
       done = 1;
   });

   while(!done)
       /* nothing */;
}

然而,无缘无故就让CPU转到100%是个坏主意,所以让我们尝试改进一下吧。

有许多不同的方式来实现这一点。我选择使用pthread条件变量。一对条件变量加一个互斥锁,允许一个线程阻塞并等待另一个线程的信号。在发出信号的线程,你这样做:

1
2
3
4
pthread_mutex_lock(&lock);
// make your change
pthread_cond_broadcast(&cond); // or _signal
pthread_mutex_unlock(&lock);

锁确保了修改操作相对于等待线程是原子的。然后调用cond_broadcast以通知唤醒等待中的线程。

在等待中的线程中,你这样做:

1
2
3
4
pthread_mutex_lock(&lock);
while(!condition)
   pthread_cond_wait(&cond);
pthread_mutex_unlock(&lock);

锁确保了对condition的检查操作相对于发信号的线程是原子操作。while循环有两个目的。首先,condition可能事先已经被设置了。这种情况下,while避免了对pthread_cond_wait的调用,否则这会造成永远等待,因为在这之前信号已经发过了。其次,pthread_cond_wait可能甚至在没有任何给条件变量的信号前就返回了。这被称之为虚假唤醒(spurious wakeup),跟条件变量在内部是如何实现的有关。在虚假唤醒的情况下,while循环确保了等待的线程不会过早退出。

ma_dispatch_group_wait函数首先定义初始化了一个互斥量和一个条件变量:

1
2
3
4
5
6
7
void ma_dispatch_group_wait(ma_dispatch_group_t group)
{
   pthread_mutex_t mutex;
   pthread_cond_t cond;

   pthread_mutex_init(&mutex, NULL);
   pthread_cond_init(&cond, NULL);

接下来,它获取了这些变量的指针:

1
2
   pthread_mutex_t *mutexPtr = &mutex;
   pthread_cond_t *condPtr = &cond;

这样做是为了解决与block的一个冲突。如果传递给notify的block直接捕获mutexcond,它们会被拷贝。这些数据类型不容许被拷贝。具体点讲,pthread_mutex_t有一些内部对齐规则检查,至少在一些实现中是这样。比起想出如何强制编译器去满足程序库的对齐需求,pthread_mutex_t在内部设置了一些额外的存储空间,然后正确的内部存储对齐是在它被初始化的时候进行的。从本质上说,有个内部字段存在(至少)两个可能的位置,并且该位置是由init决定的。当变量被拷贝的时候,对齐可能不再是正确的,这可能会导致崩溃。通过捕获这些变量的指针,避免了拷贝和潜在的崩溃。

同样还定义了一个done变量,用于追踪block何时被实际执行:

1
   __block int done = 0;

现在notify被调用的时候,用了一个获取锁、设置done、通知等待线程并释放锁的block:

1
2
3
4
5
6
   ma_dispatch_group_notify(group, ^{
       pthread_mutex_lock(mutexPtr);
       done = 1;
       pthread_cond_broadcast(condPtr);
       pthread_mutex_unlock(mutexPtr);
   });

于是,函数只需等待done被设置:

1
2
3
4
5
   pthread_mutex_lock(mutexPtr);
   while(!done)
       pthread_cond_wait(condPtr, mutexPtr);
   pthread_mutex_unlock(mutexPtr);
}

就这些!

总结

Dispatch group是一个非常有用的API,它可以很容易地协调多个异步操作并在所有操作完成后执行后续的代码。该API一览无余,但非常实用。如此有用的一个工具实现起来却相当简单。有了正确的想法,一小段代码也大有用途。

译自:Friday Q&A 2013-08-16: Let’s Build Dispatch Groups

Comments