I have the following code snippet of a client and a grain in Orleans. (Although the recommended way to develop in Orleans is to await Tasks, the following code doesn't await at some points purely for experimentation purposes)
// client code
while(true)
{
Console.WriteLine("Client giving another request");
double temperature = random.NextDouble() * 40;
var grain = client.GetGrain<ITemperatureSensorGrain>(500);
Task t = sensor.SubmitTemperatureAsync((float)temperature);
Console.WriteLine("Client Task Status - "+t.Status);
await Task.Delay(5000);
}
// ITemperatureSensorGrain code
public async Task SubmitTemperatureAsync(float temperature)
{
long grainId = this.GetPrimaryKeyLong();
Console.WriteLine($"{grainId} outer received temperature: {temperature}");
Task x = SubmitTemp(temperature); // SubmitTemp() is another function in the same grain
x.Ignore();
Console.WriteLine($"{grainId} outer received temperature: {temperature} exiting");
}
public async Task SubmitTemp(float temp)
{
for(int i=0; i<1000; i++)
{
Console.WriteLine($"Internal function getting awaiting task {i}");
await Task.Delay(1000);
}
}
The output when I run the above code is the following:
Client giving another request
Client Task Status - WaitingForActivation
500 outer received temperature: 23.79668
Internal function getting awaiting task 0
500 outer received temperature: 23.79668 exiting
Internal function getting awaiting task 1
Internal function getting awaiting task 2
Internal function getting awaiting task 3
Internal function getting awaiting task 4
Client giving another request
Client Task Status - WaitingForActivation
500 outer received temperature: 39.0514
Internal function getting awaiting task 0 <------- from second call to SubmitTemp
500 outer received temperature: 39.0514 exiting
Internal function getting awaiting task 5 <------- from first call to SubmitTemp
Internal function getting awaiting task 1
Internal function getting awaiting task 6
Internal function getting awaiting task 2
Internal function getting awaiting task 7
Internal function getting awaiting task 3
Internal function getting awaiting task 8
Internal function getting awaiting task 4
Internal function getting awaiting task 9
The output makes sense from the perspective of a normal .Net application. If I can take help from this stackoverflow post, what is happening here is that:
ITemperatureSendorGrain
and proceeds ahead. When the await
is hit, the client thread is returned to the threadpool. SubmitTemperatureAsync
receives the request and calls a local async function SubmitTemp
.SubmitTemp
prints statement corresponding to i=0 after which point it hits await. Await causes the rest of the for loop
to be scheduled as a continuation of the awaitable (Task.Delay) and the control returns to the calling function SubmitTemperatureAsync
. Note here that the thread isn't returned to the threadpool when it encounters await in SubmitTemp
function. The thread control is actually returned to the calling function SubmitTemperatureAsync
. So, a turn
, as defined in Orleans docs, ends when the top level method encounters an await. When a turn ends, the thread is returned to the threadpool. SubmitTemp
returns after 1s, it gets a thread from the threadpool and schedules the rest of the for loop
on it.for loop
is scheduled corresponding to the second call to SubmitTemp
.My first question is have I correctly described what's happening in the code, especially about the thread not being returned to the thread pool when await is hit in the function SubmitTemp
.
According to single-threaded nature of grains, at any time only one thread will be executing a grain's code. Also, once a request to a grain begins execution, it will be completed fully before the next request is taken up (called chunk based execution
in the Orleans docs). On a high-level, this is true for the above code because the next call to SubmitTemperatureAsync
will happen only when the current call to the method exits.
However, SubmitTemp
was actually a sub-function of SubmitTemperatureAsync
. Although SubmitTemperatureAsync
exited, SubmitTemp
is still executing and while it does that, Orleans allowed another call to SubmitTemperatureAsync
to execute. Doesn't this violate the single-threaded nature of Orleans grain is my second question?
Consider that SubmitTemp
in its for loop
needs to access some data members of the grain class. So, the ExecutionContext
will be captured when await is encountered and when Task.Delay(1000)
returns, the captured ExecutionContext
will be passed to the scheduling of the remainder for loop
on a thread. Because ExecutionContext
is passed, the remainder for loop
will be able to access the data members in spite of running on a different thread. This is something which happens in any normal .Net asynchronous application.
My third question is about SynchronizationContext
. I did a cursory search in the Orleans repository but couldn't find any implementation of SynchronizationContext.Post()
, which leads me to believe that no SynchronizationContext
is required to run Orleans methods. Can anyone confirm this? If this isn't true, and a SynchronizationContext
is required, wouldn't the parallel execution of the various invocations of SubmitTemp
(as shown in the above code), run the risk of ending in a deadlock (if someone holds on to the SynchronizationContext
and doesn't release it)?
Your description looks roughly correct to me, but here's a few finer points:
TaskScheduler
.TaskScheduler
.await
was hit which wasn't synchronously completed, or maybe the user isn't using await
at all and is programming using ContinueWith
or custom awaitables.await SubmitTemp(x)
instead of .Ignoring()
it, then the turn would end when the Task.Delay(...)
was hit inside SubmitTemp(x)
.No, there is only ever a single thread executing the grain's code at a given time. However, that 'thread' has to split its time between the various tasks which are scheduled on the activation's TaskScheduler
. i.e, there will never be a time where you suspend the process and discover that two threads are executing your grain's code at the same time.
As far as the runtime is concerned, the processing of your message ends when the Task
(or other awaitable type) returned from the top level method completes. Until that occurs, no new messages will be scheduled for execution on your activation. Background tasks spawned from your methods are always allowed to interleave with other tasks.
.NET allows child tasks to be attached to their parent task. In that case, the parent task only completes when all child tasks complete. This is not the default behavior, however, and it's generally suggested that you avoid opting-in to this behavior (eg, by passing TaskCreationOptions.AttachedToParent
to Task.Factory.StartNew
).
If you did make use of that behavior (please don't), then you would see your activation loop on the first call to SubmitTemp()
indefinitely, and no more messages will be processed.
SynchronizationContext
?Orleans does not use SynchronizationContext
. Instead, it uses custom TaskScheduler
implementations. See ActivationTaskScheduler.cs
. Each activation has its own ActivationTaskScheduler
and all messages are scheduler using that scheduler.
Regarding the follow-on question, the Task
instances (each one representing a synchronous piece of work) which are scheduled against the activation are inserted into the same queue and so they are allowed to interleave, but the ActivationTaskScheduler
is only executed by one thread at a time.