最近在学习.NET4.5关于“并行任务”的使用。“并行任务”有自己的同步机制,没有显示给出类似如旧版本的:事件等待句柄、信号量、lock、ReaderWriterLock……等同步基元对象,但我们可以沿溪这一编程习惯,那么这系列翻译就是给“并行任务”封装同步基元对象。翻译资源来源《(译)关于Async与Await的FAQ》
1. 构建Async同步基元,Part 1 AsyncManualResetEvent
2. 构建Async同步基元,Part 2 AsyncAutoResetEvent
3. 构建Async同步基元,Part 3 AsyncCountdownEvent
4. 构建Async同步基元,Part 4 AsyncBarrier
5. 构建Async同步基元,Part 5 AsyncSemaphore
6. 构建Async同步基元,Part 6 AsyncLock
7. 构建Async同步基元,Part 7 AsyncReaderWriterLock
开始:构建Async同步基元,Part 4 AsyncBarrier
上一篇,我们着眼于建立一个Async版本的CountdownEvent。在文章末尾,我提到了一个常见模式,即一个参与者发出到达信号并且等待其他参与者都发出信号(“signal-and-wait”)。这种典型的同步通常被称为“关卡|屏障”,并且常常运用在阶段性工作场合。对于每一个阶段,每一个参与者都将做一些工作,然后都在关卡上“signal-and-wait”后,再进入下一个阶段。
类似我们构建的AsyncCountdownEvent,我们也能轻松构建一个AsyncBarrier。待构建的目标类型如下:
public class AsyncBarrier
{
public AsyncBarrier(int participantCount);
public Task SignalAndWait();
}
首先,我们需要一些成员。像AsyncCountdown一样,我们需要知道共有多少个参与者参与。但是我们不需要永久的保持该数字,我们只需要知道尚未在当前阶段发出信号的参与者的剩余数量。在AsyncBarrier中,每当剩余数量到达0,就需要重置剩余数量为原始参与者数量,所以我们需要存储原始参与者数量和尚未在当前阶段发出信号的参与者的剩余数量。我们还需要一个TaskCompletionSource<bool>来创建任务,所有参与者在其上等待完成。
private readonly int m_participantCount;
private TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();
private int m_remainingParticipants;
在构造函数中将简单的初始化计数变量为我们期望的参与者数值。
public AsyncBarrier(int participantCount)
{
if (participantCount <= 0)
throw new ArgumentOutOfRangeException("participantCount");
m_remainingParticipants = m_participantCount = participantCount;
}
最后,我们构建SignalAndWait(),先递减“剩余数量”的值,当它的计数为0时,我们完成TaskCompletionSource<bool>。然而在这里我们需要注意操作的顺序,因为一旦一个参与者从等待中唤醒,它们就开始下一阶段的操作处理,然后在barrier上再次“signal-and-wait”,所以我们此时需要为下一阶段准备好配置,即:在计数为0之后且在任务完成之前,我们需要重置“剩余数量”为原始参与者数量,并且切换一个新的TaskCompletionSource<bool>实例用于下一个阶段,以便接下来唤醒所有等待者。
public Task SignalAndWait()
{
var tcs = m_tcs;
if (Interlocked.Decrement(ref m_remainingParticipants) == 0)
{
m_remainingParticipants = m_participantCount;
m_tcs = new TaskCompletionSource<bool>();
tcs.SetResult(true);
}
return tcs.Task;
}
完整源码如下:
public class AsyncBarrier1
{
// 存储原始参与者数量
private readonly int m_participantCount;
// 存储尚未在当前阶段发出信号的参与者的剩余数量
private int m_remainingParticipants;
private TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();
public AsyncBarrier1(int participantCount)
{
if (participantCount <= 0)
throw new ArgumentOutOfRangeException("participantCount");
m_remainingParticipants = m_participantCount = participantCount;
}
public Task SignalAndWait()
{
// 临时存储当前阶段的TaskCompletionSource<bool>实例,
// 以便“在计数为0之后且在任务完成之前以唤醒所有等待者”
var tcs = m_tcs;
if (Interlocked.Decrement(ref m_remainingParticipants) == 0)
{
m_remainingParticipants = m_participantCount;
m_tcs = new TaskCompletionSource<bool>();
tcs.SetResult(true);
}
return tcs.Task;
}
}
当然,还有其他方式实现AsyncBarrier。上面的设计中存在一个潜在的问题,即如果所有参与者选择使用同步延续,这些延续将串行化完成。处理这个问题可以留给每个参与者,或者我们帮助参与者通过使每一个参与者能获得他们对应的Task,再一起并行完成任务。
这样的设计类似下面封装:
public class AsyncBarrier2
{
// 存储原始参与者数量
private readonly int m_participantCount;
// 存储尚未在当前阶段发出信号的参与者的剩余数量
private int m_remainingParticipants;
// 线程安全的后进先出集合,每一个参与者对应一个Task
private ConcurrentStack<TaskCompletionSource<bool>> m_waiters;
public AsyncBarrier2(int participantCount)
{
if (participantCount <= 0)
throw new ArgumentOutOfRangeException("participantCount");
m_remainingParticipants = m_participantCount = participantCount;
m_waiters = new ConcurrentStack<TaskCompletionSource<bool>>();
}
public Task SignalAndWait()
{
var tcs = new TaskCompletionSource<bool>();
m_waiters.Push(tcs);
if (Interlocked.Decrement(ref m_remainingParticipants) == 0)
{
m_remainingParticipants = m_participantCount;
var waiters = m_waiters;
m_waiters = new ConcurrentStack<TaskCompletionSource<bool>>();
Parallel.ForEach(waiters, w => w.SetResult(true));
}
return tcs.Task;
}
}
这就是本节要讲的AsyncBarrier。下一节,我将实现一个async版本的Semaphore。
推荐阅读:
感谢你的观看……
原文:《Building Async Coordination Primitives, Part 4: AsyncBarrier》