Reputation: 105
I have three grains (A, B and C) doing different jobs in a pipeline. GrainA will pass the result to grainB, and grainB will pass the result to grainC. I want to guarantee sequential message sending between successive grains which can be achieved by below.
// client code
foreach(var i in list)
{
await grainA.job(i);
}
//grain A code
async Task job(var i)
{
DoSomeWorkA(i);
await grainB.job(i);
}
//grain B code
async Task job(var i)
{
DoSomeWorkB(i);
await grainC.job(i);
}
//grain C code
async Task job(var i)
{
DoSomeWorkC(i);
Console.WriteLine(i);
}
However, the problem with this code is that there is no pipelining. grainA is given the net object only when the current object goes through all the grainB and grainC (because of the await statement). One way to get pipelining is not use await and directly send objects one after the other. However, that leads to out-of-order delivery as explained in this post.
I want to let the execution be fully pipelined, which means grainA continues on to its next job when grainB receives the result from grainA. However, message ordering is also important as I send some control messages. How to do that in Orleans?
Upvotes: 2
Views: 828
Reputation: 2401
In order to make highly concurrent systems easy to program and reason about, Orleans limits the parallelism and concurrency of each grain (the unit of computation & state) to 1. That means that at most one message will be processed concurrently and at most one thread will be executing a given grain activation's code at any point in time.
This greatly simplifies what the developer needs to concentrate on since there is no longer a need for thread synchronization primitives like locks.
However, the behavior is configurable. If you do not want this default, 'safe', behavior then you can mark a grain as [Reentrant]
or mark individual methods as [AlwaysInterleave]
on the grain's interface.
This allows for many messages to be processed concurrently. At each await
point in the grain method, execution is yielded back to the scheduler and the scheduler can begin processing another message. The scheduler still ensures single-threadedness for each grain activation, so locks are still not needed, but messages are allowed to interleave at those await
points (i.e, cooperative multitasking). This means that the developer now needs to consider how internal state might be mutated by other requests between these await
points.
For more information, see the Reentrancy page in the Orleans documentation.
In order to increase parallelism (for CPU-bound tasks), a developer can either use Stateless Workers or External Tasks via Task.Run
or similar.
Note also that messages sent from grain activation A to grain activation B will always be sent in-order regardless of concurrency configuration. Likewise for results sent from B to A. Note that it still becomes harder to reason about the order of messages once concurrency is increased, however, since a second call to B may finish before the first call and therefore its result will be sent earlier, also. This means that the results may appear to be received out-of-order. I imagine that this is what you would expect, though.
Upvotes: 3