I have some library (socket networking) code that provides a Task
-based API for pending responses to requests, based on TaskCompletionSource<T>
. However, there's an annoyance in the TPL in that it seems to be impossible to prevent synchronous continuations. What I would like to be able to do is either:
- tell a
TaskCompletionSource<T>
that is should not allow callers to attach withTaskContinuationOptions.ExecuteSynchronously
, or - set the result (
SetResult
/TrySetResult
) in a way that specifies thatTaskContinuationOptions.ExecuteSynchronously
should be ignored, using the pool instead
Specifically, the issue I have is that the incoming data is being processed by a dedicated reader, and if a caller can attach with TaskContinuationOptions.ExecuteSynchronously
they can stall the reader (which affects more than just them). Previously, I have worked around this by some hackery that detects whether any continuations are present, and if they are it pushes the completion onto the ThreadPool
, however this has significant impact if the caller has saturated their work queue, as the completion will not get processed in a timely fashion. If they are using Task.Wait()
(or similar), they will then essentially deadlock themselves. Likewise, this is why the reader is on a dedicated thread rather than using workers.
So; before I try and nag the TPL team: am I missing an option?
Key points:
- I don't want external callers to be able to hijack my thread
- I can't use the
ThreadPool
as an implementation, as it needs to work when the pool is saturated
The example below produces output (ordering may vary based on timing):
Continuation on: Main thread
Press [return]
Continuation on: Thread pool
The problem is the fact that a random caller managed to get a continuation on "Main thread". In the real code, this would be interrupting the primary reader; bad things!
Code:
using System;
using System.Threading;
using System.Threading.Tasks;
static class Program
{
static void Identify()
{
var thread = Thread.CurrentThread;
string name = thread.IsThreadPoolThread
? "Thread pool" : thread.Name;
if (string.IsNullOrEmpty(name))
name = "#" + thread.ManagedThreadId;
Console.WriteLine("Continuation on: " + name);
}
static void Main()
{
Thread.CurrentThread.Name = "Main thread";
var source = new TaskCompletionSource<int>();
var task = source.Task;
task.ContinueWith(delegate {
Identify();
});
task.ContinueWith(delegate {
Identify();
}, TaskContinuationOptions.ExecuteSynchronously);
source.TrySetResult(123);
Console.WriteLine("Press [return]");
Console.ReadLine();
}
}
TaskCompletionSource
with my own API to prevent direct call toContinueWith
, since neitherTaskCompletionSource
, norTask
doesn't suit well for inheritance from them. – PolynuclearTask
that is exposed, not theTaskCompletionSource
. That (exposing a different API) is technically an option, but it is a pretty extreme thing to do just for this... I'm not sure it justifies it – LibertyThreadPool
for this (which I already mentioned - it causes problems), or you have a dedicated "pending continuations" thread, and then they (continations withExecuteSynchronously
specified) can hijack that one instead - which causes exactly the same problem, because it means that continuations for other messages can be stalled, which again impacts multiple callers – LibertyTaskCompletionSource<T>
doesn't let you specify a scheduler, because it doesn't ever run on one; attaching tasks can specify a scheduler inContinueWith
, but if we knew all attaching tasks would get things right, the question would be moot – LibertyTrySet*
on a background thread as a hack. E.g.,TrySetResultWithBackgroundContinuations
here. I could not find a better way. – FoxtrotThreadPool
, but I am pretty sure defaultTaskScheduler
usesThreadPool
for tasks anyway (as your 'actual result' shows). – Disposetask.ContinueWith(t => t.Result)
instead of the original task? Is your concern that in this case you are scheduling unnecessary async work? But you want the clients to do async work anyway, and you can pass a custom scheduler that may even force them all to be sync with relation to that new task. – DisposeContinueWith
withoutExecuteSynchronously
? Or do you expect callers to use their own schedulers? Or is the problem in that not all of the responses are going to be processed at all? – DisposeContinueWith
without sync anyway, why doing your ownContinueWith
without sync is a bad idea? the only concern I could see is that yourContinueWith
would be a separate work item from their next continuation, but that is something that might be solved with a custom scheduler. – DisposeThread.Sleep
. I'd like to be able to prevent them from attaching for sync-exec, essentially. – LibertyConsole.WriteLine("Press [return]");
toConsole.WriteLine("Press [return] on thread {0}", Thread.CurrentThread.ManagedThreadId);
it explain a little bit more the issue... :-) – JapethEventLoopScheduler
as opposed toTask
. – Encephaloma