原文链接
异步编程的困难在于没有简单的方法来协调多个操作、处理部分失败(多个操作中一个失败而且其他的成功)以及定义异步回调的执行行为。所以他们不会违反一些并发约束(例如他们不会尝试并行的做一些事情)。CCR通过提供一些表达什么样的协调应该发生的方法来允许和鼓励并发。另外,CCR的所有运行都是根据port上收到的消息来的,加强了不同代码段间高级别的约束。
意识到异步行为和并发之间的关系很重要:解耦和,快速开始工作和统一的使用队列来进行交互,鼓励可伸缩的和良好定义依赖的软件设计。所以如果上述的缺点可以被解决,它(异步编程)是软件组件合适的模型。
CCR提供的协调原语可以按照使用场景被分为两大类:
- 为长声明周期的面向服务组件协调进入到请求。一个常见的例子是:一个在某个网络端口监听HTTP请求的web service使用CCR port来投递所有的请求,并且使用port上的处理器独立的唤醒和服务每个请求。进一步的,它使用某些高级原语来保证当某些处理器在激活状态时另外一些处理器绝对不会被运行。
- 协调一个或者多个请求的响应,每个响应有多个可能的返回类型。例如:在一个关联到未决请求的PortSet上等待成功或者失败:当请求完成,一个成功元素或者一个失败元素将会被投递,然后应该执行适当的代码。再例如:一次发出多个请求,然后使用一个单独的原语收集所有的响应,无论响应到达的顺序和是从一个port或者多个port响应。
我们先会简要介绍每个仲裁器,然后在上述场景的上下文中对它们进行更多说明。
Arbiter静态类
Arbiter静态类用一种可发现的类型安全的方式为创建所有Aribiter类实例提供了帮助方法。下面描述的所有方法都是这个类的成员。Arbiter静态方法并不是唯一的创建CCR仲裁器的办法。为了进行更高级的配置,每个仲裁器都可以直接通过使用合适的构造函数和参数进行构造。下面的列表展示了当调用某些最常用的Arbiter类方法时会创建哪些CCR类(不完整列表)
- Arbiter.FromHandler -> Creates instance of Task
- Arbiter.Choice -> Creates instance of Choice
- Arbiter.Receive -> Creates instance of Receiver
- Arbiter.Interleave -> Creates instance of Interleave
- Arbiter.JoinedReceive ->Creates instance of JoinReceiver
- Arbiter.MultipleItemReceive -> Creates instance JoinSinglePortReceiver
上述这些类的描述在参考文档中,它们应该被用在需要附加参数/配置的高级场景中。
Port类扩展方法(Port Extension Methods)
Port类的扩展方法是Arbiter类静态方法的一个更简明的替代品(Alternative)。使用C# 3.0扩展方法,协调原语可以通过Port类的实力创建。本指南中的大多数例子都使用了Port的扩展方法来创建接收器(Receivers)、连接器(joins)等。
单元素接收器(Single Item Receiver)
一个单元素接收器通过一个Port<T>的实例与一个接受一个类型为T的参数的delegate关联。如果持久化选项被设置为true,每当有元素被投递,接收器将执行一个delegate的实例。如果持久化选项为false,接收器将只处理一个元素(执行一次),然后从port中注销(un-register)。
重要:如果已经有元素在port队列中,接收器仍然会执行他们,以提供一种可信赖的方式(reliable way)来“赶上”排队的元素并使得用户代码与元素在何时被投递无关。
例7.
var port
=
new
Port
<
int
>
();
Arbiter.Activate(_taskQueue,
Arbiter.Receive(
true
,
port,
item
=>
Console.WriteLine(item)
)
);
//
post item, so delegate executes
port.Post(
5
);
例7展示了如何创建一个Port<int>实例然后激活一个单元素接收器,这个接收器在每次有元素被投递到port时执行用户的delegate。注意,接收器是持久化的,它将一直处于激活状态直到port实例被GC掉。
例8.
//
alternate version that explicitly constructs a Receiver by passing
//
Arbiter class factory methods
var persistedReceiver
=
new
Receiver
<
int
>
(
true
,
//
persisted
port,
null
,
//
no predicate
new
Task
<
int
>
(item
=>
Console.WriteLine(item))
//
task to execute
);
Arbiter.Activate(_taskQueue, persistedReceiver);
例8和例7在运行时有同样的效果,但是展示了Arbiter.Receiver()方法只是Receiver仲裁器构造函数的一层薄薄的包装。
选择仲裁器(Choice Arbiter)
选择仲裁器只执行它所有分支中的一个,然后原子的(在一个步中,且无法被中断)从port删除所有其他嵌套的仲裁器。这保证了只有一个选择会被执行,并且是一种通用的方法来展现分支行为,用于处理有成功/失败的响应或者保护避免竞争状态。(This guarantees that only one branch of the choice will ever run and is a common way to express branching behavior, deal with responses that have success/failure, or guard against race conditions.)
例9.
//
create a simple service listening on a port
ServicePort servicePort
=
SimpleService.Create(_taskQueue);
//
create request
GetState
get
=
new
GetState();
//
post request
servicePort.Post(
get
);
//
use the extension method on the PortSet that creates a choice
//
given two types found on one PortSet. This a common use of
//
Choice to deal with responses that have success or failure
Arbiter.Activate(_taskQueue,
get
.ResponsePort.Choice(
s
=>
Console.WriteLine(s),
//
delegate for success
ex
=>
Console.WriteLine(ex)
//
delegate for failure
));
例9展示了一个选择仲裁器的常见用法:根据在PortSet上接收到的消息来执行两个不同的delegates。注意:Choice类可以接受任意数量(不只是两个)的接收器,并且协调它们。PortSet类的Choice扩展方法是创建Choice仲裁器对象的一种简单方式。然后创建两个Receiver仲裁器,每个仲裁器对应一个delegate。
重要:选择仲裁器是“父”仲裁器的一个例子:其他的仲裁器,例如单元素接收器或者连接(joins)可以被嵌套到一个选择仲裁器中。仲裁器的设计允许嵌套多层的仲裁器,并在检测到是否应该执行用户代码前以正确的顺序调用多层中的每一个仲裁器。这使得程序员可以用简单的几行代码表达复杂的情况。
连接和多元素接收器
多元素接收器分为两类:
- 也称作连接(joins)(或者操作系统课程中的WaitForMultiple)。他们尝试从一个或者多个ports中接收元素,如果一个尝试失败了,他们把所有元素投递回去,然后等到正确的条件发生时进行重试。这两个阶段的逻辑提供了一个类型安全且无死锁的机制。因为元素接收到的顺序是无关紧要的,所以它可以被用于保证对多个资源的原子访问而无需担心死锁问题。元素和ports的数量可以在运行时指定,也可以在编译时固定。连接中的元素数量和ports可以在运行时指定是一个CCR提供的超过其他形式类型连接的重要扩展。(The fact that the number of items in the join can be specified at runtime, is an important extension the CCR provides over other forms of typed joins.不是很理解这句话,暂且这么翻译吧。)
- 接收器主动从参与接收的每个port中删除元素,当所有元素数量满足条件就执行用户delegate。这个版本非常快,但是不应该被用作一个资源同步原语。它经常被用于收集多个未决请求的结果(发散/集中场景)
Joins
例10.
var portDouble
=
new
Port
<
double
>
();
var portString
=
new
Port
<
string
>
();
//
activate a joined receiver that will execute only when one
//
item is available in each port.
Arbiter.Activate(_taskQueue,
portDouble.Join(
portString,
//
port to join with
(value, stringValue)
=>
//
delegate
{
value
/=
2.0
;
stringValue
=
value.ToString();
//
post back updated values
portDouble.Post(value);
portString.Post(stringValue);
})
);
//
post items. The order does not matter, which is what Join its power
portDouble.Post(
3.14159
);
portString.Post(
"
0.1
"
);
//
after the last post the delegate above will execute
上面的例子中,我们演示了一个简单的“静态”连接(在编译类型时固定了ports的数量)。我们激活一个包含两个port的连接接收器,然后向每个port中投递元素。连接逻辑会检测是否它需要的所有东西都有了,然后调度执行delegate。
例11.
var portInt
=
new
Port
<
int
>
();
var portDouble
=
new
Port
<
double
>
();
var portString
=
new
Port
<
string
>
();
//
activate a joined receiver that will execute only when one
//
item is available in each port.
Arbiter.Activate(_taskQueue,
portDouble.Join(
portString,
//
second port to listen
(value, stringValue)
=>
{
value
/=
2.0
;
stringValue
=
value.ToString();
//
post back updated values
portDouble.Post(value);
portString.Post(stringValue);
})
);
//
activate a second joined receiver that also listens on portDouble
//
and on a new port, portInt. Because the two joins share a common port
//
between them (portDouble), there is contention when items are posted on
//
that port
Arbiter.Activate(_taskQueue,
portDouble.Join(
portInt,
//
second port to listen
(value, intValue)
=>
{
value
/=
2.0
;
intValue
=
(
int
)value;
//
post back updated values
portDouble.Post(value);
portInt.Post(intValue);
})
);
//
post items.
portString.Post(
"
0.1
"
);
portInt.Post(
128
);
//
when the double is posted there will be a race
//
between the two joins to determine who will execute first
//
The delegate that executes first will then post back a double,
//
allowing the delegate that "lost", to execute.
portDouble.Post(
3.14159
);
上面的这个例子展示了一种简单的连接竞争情况,它是对例10的一个简单扩展:两个独立的delegates监听同一个port和其他的一些不共享的ports。由于连接实现分为两个阶段,所以无法保证两个阶段都被执行,在共享port中抽取到数据后立即发回Port(也就是竞争中获胜的一方立即post数据回共享的port,使得失败一方可以执行)(Because the join implementation is two phase, there is no guarantee both will run, as soon as the value extracted from the shared port, is posted back. 这句实在不会翻译了,-_-!!!)。运行的顺序并不重要,所以竞争并不会影响结果。我们简单的演示了一个传统的跨多个资源的锁问题(locking problem),可以变成只是一个调度依赖从而被CCR解决。消息既是并发访问中所要保护的资源,也是触发代码执行的信号。(Messages are both the resource being guarded from multiple concurrent access, and the signal that triggers the execution of the code that requires it.啥意思啊,为啥是it而不是them,难道不是指的messages?)
重要:这样使用连接(join)是一个使用嵌套lock的好替代品,但是它依然是非常容易被用错的资源访问方法。后面章节讲到的Interleave原语是一个更简单、更不容易用错并且更快的选择。
例12.
int
itemCount
=
10
;
var portDouble
=
new
Port
<
double
>
();
//
post N items to a port
for
(
int
i
=
0
; i
<
itemCount; i
++
)
{
portDouble.Post(i
*
3.14159
);
}
//
activate a Join that
//
waits for N items on the same port
Arbiter.Activate(_taskQueue,
portDouble.Join(
itemCount,
items
=>
{
foreach
(
double
d
in
items)
{
Console.WriteLine(d);
}
}
)
);
上面的例子展示了“动态”连接的一种简单的情况:元素的数量只有在运行时才知道(存储在itemCount变量中),所有元素都从一个port读出。这个例子使用了一个叫做JoinSinglePortReceiver的join版本,当从一个port收到N个元素时,它会执行一个处理器。
多元素接收器
多元素接收器适用于不会出现竞争的ports。它们可以用于聚合多个未完成请求的响应。
例13.
//
create a simple service listening on a port
var servicePort
=
SimpleService.Create(_taskQueue);
//
shared response port
var responsePort
=
new
PortSet
<
string
, Exception
>
();
//
number of requests
int
requestCount
=
10
;
//
scatter phase: Send N requests as fast as possible
for
(
int
i
=
0
; i
<
requestCount; i
++
)
{
//
create request
GetState
get
=
new
GetState();
//
set response port to shared port
get
.ResponsePort
=
responsePort;
//
post request
servicePort.Post(
get
);
}
//
gather phase:
//
activate a multiple item receiver that waits for a total
//
of N responses, across the ports in the PortSet.
//
The service could respond with K failures and M successes (K+M == N)
Arbiter.Activate(_taskQueue,
responsePort.MultipleItemReceive(
requestCount,
//
total responses expected
(successes, failures)
=>
Console.WriteLine(
"
Total received:
"
+
successes.Count
+
failures.Count)
)
);
例子13展示了一种常见的使用多个delegate来收集处理多个未决定异步操作的情况。假设任意N个操作中,有K个失败,M个成功,并且K+M=N,CCR MultipleItemReceiver给出了一种简洁的办法来收集所有的结果,无论结果以什么顺序到达也无论结果是什么类型。一个delegate将会被调用,传入两个集合容器,这两个集合容器分别包含了K个失败的结果和M个成功的结果。Arbiter.MutipleItemReceive方法用于两个不同的类型,但是底层的MultipleItemGather CCR仲裁器可以用于任意数量的类型。
用于面向服务组件的协调
持久化的单元素接收器
CCR源于一个用来监听消息队列并且激活事件处理器来处理进来的消息的运行时。最简单的情况是使用持久化的Receiver仲裁器来监听一个port,然后当有元素被投递进来时激活处理器。
例子14.
///
<summary>
///
Base type for all service messages. Defines a response PortSet used
///
by all message types.
///
</summary>
public
class
ServiceOperation
{
public
PortSet
<
string
, Exception
>
ResponsePort
=
new
PortSet
<
string
, Exception
>
();
}
public
class
Stop : ServiceOperation
{
}
public
class
UpdateState : ServiceOperation
{
public
string
State;
}
public
class
GetState : ServiceOperation
{
}
///
<summary>
///
PortSet that defines which messages the services listens to
///
</summary>
public
class
ServicePort : PortSet
<
Stop, UpdateState, GetState
>
{
}
///
<summary>
///
Simple example of a CCR component that uses a PortSet to abstract
///
its API for message passing
///
</summary>
public
class
SimpleService
{
ServicePort _mainPort;
DispatcherQueue _taskQueue;
string
_state;
public
static
ServicePort Create(DispatcherQueue taskQueue)
{
var service
=
new
SimpleService(taskQueue);
service.Initialize();
return
service._mainPort;
}
private
void
Initialize()
{
//
using the supplied taskQueue for scheduling, activate three
//
persisted receivers, that will run concurrently to each other,
//
one for each item type
Arbiter.Activate(_taskQueue,
Arbiter.Receive
<
UpdateState
>
(
true
, _mainPort, UpdateHandler),
Arbiter.Receive
<
GetState
>
(
true
, _mainPort, GetStateHandler)
);
}
private
SimpleService(DispatcherQueue taskQueue)
{
//
create PortSet instance used by external callers to post items
_mainPort
=
new
ServicePort();
//
cache dispatcher queue used to schedule tasks
_taskQueue
=
taskQueue;
}
void
GetStateHandler(GetState
get
)
{
if
(_state
==
null
)
{
//
To demonstrate a failure response,
//
when state is null will post an exception
get
.ResponsePort.Post(
new
InvalidOperationException());
return
;
}
//
return the state as a message on the response port
get
.ResponsePort.Post(_state);
}
void
UpdateHandler(UpdateState update)
{
//
update state from field in the message
_state
=
update.State;
//
as success result, post the state itself
update.ResponsePort.Post(_state);
}
}
例子14中,我们展示了一个为软件组件实现了通用CCR模式的类:(In the example above we show a class implementing the common CCR pattern for a software component)
- 用于和组件交互的消息类型定义
- 一个PortSet的继承类的定义,这个类接受上面定义的消息类型。并不是必须要从PortSet继承,但是这是一个简单的重用特定个数参数的PortSet的方法。
- 一个静态的Create方法,用于初始化一个组件,并且返回一个用于和组件通讯的PortSet实例。
- 一个私有的Initialize方法,用于向公共的PortSet附加一些仲裁器。
如果在不同的处理器(译者注:用户代码)之间不存在并发竞争,简单的持久化单元素接收器可以被使用。
交替(Interleave)仲裁器
监听某个port的重要的组件,经常会有应该被小心保护不被并发访问的私有资源。一个需要多个地方更新的内部存储的数据结构必须被视为原子的。另外一个场景是一个组件实现了一个复杂的多步流程,这个流程不能被特定的外部请求抢占。CCR使得程序员只需要考虑实现复杂的流程,CCR来处理排队请求和激活处理函数知道流程完成。程序员使用交替仲裁器来声明那些代码段需要被保护。
对于熟悉多线程编程中读写锁原语的程序员,交替仲裁器是和读写锁类似的概念(它是一个偏重于写的读写锁),但是不需要锁定特定的对象,代码片被互相隔离(protected from each other)。避免了锁争用,交替仲裁器使用内部队列来创建调度依赖和管理执行,这样任务就可以被并发的独立的执行。
例15.
///
<summary>
///
Simple example of a CCR component that uses a PortSet to abstract
///
its API for message passing
///
</summary>
public
class
ServiceWithInterleave
{
ServicePort _mainPort;
DispatcherQueue _taskQueue;
string
_state;
public
static
ServicePort Create(DispatcherQueue taskQueue)
{
var service
=
new
ServiceWithInterleave(taskQueue);
service.Initialize();
return
service._mainPort;
}
private
void
Initialize()
{
//
activate an Interleave Arbiter to coordinate how the handlers of the service
//
execute in relation to each other and to their own parallel activations
Arbiter.Activate(_taskQueue,
Arbiter.Interleave(
new
TeardownReceiverGroup(
//
one time, atomic teardown
Arbiter.Receive
<
Stop
>
(
false
, _mainPort, StopHandler)
),
new
ExclusiveReceiverGroup(
//
Persisted Update handler, only runs if no other handler running
Arbiter.Receive
<
UpdateState
>
(
true
, _mainPort, UpdateHandler)
),
new
ConcurrentReceiverGroup(
//
Persisted Get handler, runs in parallel with all other activations of itself
//
but never runs in parallel with Update or Stop
Arbiter.Receive
<
GetState
>
(
true
, _mainPort, GetStateHandler)
))
);
}
private
ServiceWithInterleave(DispatcherQueue taskQueue)
{
//
create PortSet instance used by external callers to post items
_mainPort
=
new
ServicePort();
//
cache dispatcher queue used to schedule tasks
_taskQueue
=
taskQueue;
}
void
GetStateHandler(GetState
get
)
{
if
(_state
==
null
)
{
//
when state is null will post an exception
get
.ResponsePort.Post(
new
InvalidOperationException());
return
;
}
//
return the state as a message on the response port
get
.ResponsePort.Post(_state);
}
void
UpdateHandler(UpdateState update)
{
//
update state from field in the message
//
Because the update requires a read, a merge of two strings
//
and an update, this code needs to run un-interrupted by other updates.
//
The Interleave Arbiter makes this guarantee since the UpdateHandler is in the
//
ExclusiveReceiverGroup
_state
=
update.State
+
_state;
//
as success result, post the state itself
update.ResponsePort.Post(_state);
}
void
StopHandler(Stop stop)
{
Console.WriteLine(
"
Service stopping. No other handlers are running or will run after this
"
);
}
}
例15扩展了SimpleService类,使用了一个交替仲裁器来协调执行不同处理器的接收器。交替仲裁器是另一个可以嵌套其它多种接收器的父仲裁器。这个例子展示了程序员如何轻松的用并发术语说明他的意图:某些处理函数(handler)可以独立运行,而另外一些则不能。CCR不需要知道哪些资源或者多步流程需要排他的访问。它只需要知道保护那些代码。这个例子中的处理函数非常简单,但是在后面的章节中,迭代处理函数(iterator handlers)演示了交替仲裁器如何保护多步运行的复杂代码。