86

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 with TaskContinuationOptions.ExecuteSynchronously, or
  • set the result (SetResult / TrySetResult) in a way that specifies that TaskContinuationOptions.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();
    }
}
Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Another issue is that a synchronously executing continuation will share the entire thread-local state of the completing thread. This includes held locks for example. It also allows for reentrancy. – usr Mar 22 '14 at 15:09
  • 2
    I'd try to wrap `TaskCompletionSource` with my own API to prevent direct call to `ContinueWith`, since neither `TaskCompletionSource`, nor `Task` doesn't suit well for inheritance from them. – Dennis Mar 22 '14 at 15:17
  • 1
    @Dennis to be clear, it is actually the `Task` that is exposed, not the `TaskCompletionSource`. 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 – Marc Gravell Mar 22 '14 at 15:19
  • 1
    Would completing the TaskCompletionSource on another thread work? – Matt H Mar 22 '14 at 15:27
  • 2
    @MattH not really - it just rephrases the question: either you use the `ThreadPool` for this (which I already mentioned - it causes problems), or you have a dedicated "pending continuations" thread, and then they (continations with `ExecuteSynchronously` 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 – Marc Gravell Mar 22 '14 at 15:30
  • Create your own TaskScheduler, return *false* from TryExecuteTaskInline(). I think. – Hans Passant Mar 22 '14 at 15:34
  • 1
    @Hans no can do; `TaskCompletionSource` *doesn't let you specify a scheduler*, because it doesn't ever run on one; **attaching tasks** can specify a scheduler in `ContinueWith`, but if we knew all attaching tasks would get things right, the question would be moot – Marc Gravell Mar 22 '14 at 15:36
  • 1
    @MarcGravell: There's no solution for this, sorry. I have invoked `TrySet*` on a background thread as a hack. E.g., [`TrySetResultWithBackgroundContinuations` here](http://nitoasyncex.codeplex.com/SourceControl/latest#Source/Nito.AsyncEx%20(NET4,%20Win8,%20SL4,%20WP75)/TaskCompletionSourceExtensions.cs). I could not find a better way. – Stephen Cleary Mar 22 '14 at 17:04
  • @Stephen that is basically what I already have, sadly. I work some evil to try and special-case tasks that don't have continuations, but (as I suspect you know) continuations use the same mechanisms as Task.Wait (once past the SpinWait stage). Sigh. Oh well, it was worth asking... Unfortunately the "complete it on a worker" breaks apart when the worker queue is saturated. – Marc Gravell Mar 22 '14 at 17:10
  • *The example below produces output* -- can you give the output you want to get? I am kind of confused when you say you do not want to use `ThreadPool`, but I am pretty sure default `TaskScheduler` uses `ThreadPool` for tasks anyway (as your 'actual result' shows). – Andrey Shchekin Mar 22 '14 at 20:26
  • @Andrey either both on worker threads, or an exception from trying to attach an exec-sync continuation in the first place. The reason I don't want to use workers here is that I know full well the worker pool stalls if it is saturated, which is hugely problematic if they are basically blocked on these completions that can't get scheduled. It is a deadlock. And yes, one answer is "don't saturate the pool" or "don't let a worker get blocked" - but i am a library author: I cannot control the callers, so i must attempt to prevent them blowing their feet of by doing silly things (that I see often). – Marc Gravell Mar 22 '14 at 20:31
  • Ok, maybe I still do not understand, but you can return `task.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. – Andrey Shchekin Mar 22 '14 at 20:38
  • @Andrey again - I'm the author of an async library: I can't control what the callers do, so I can't control what scheduler they use etc - and adding the dummy ContinueWith would force all the tasks to have worker-serviced continuations, which would hugely exacerbate the situation – Marc Gravell Mar 22 '14 at 20:40
  • @MarcGravell: how is this different from all callers calling `ContinueWith` without `ExecuteSynchronously`? 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? – Andrey Shchekin Mar 22 '14 at 20:44
  • 3
    @Andrey that (it working as if all callers used ContinueWith without exec-sync) is precisely what I want to achieve. The problem is that if my library hands someone a Task, they can do something very undesirable: they can interrupt my reader by (inadviseably) using exec-sync. This is hugely dangerous which is why I would like to prevent it from **inside the library**. – Marc Gravell Mar 22 '14 at 20:47
  • 1
    @Andrey and to clarify: they may not mean to be malicious: they could just be trying to do some database access or similar as a continuation, without even realising that they're messing with the dedicated reader thread. Indeed, on their local machine (without intensive load) it'll work fine. On a busy server: really really bad things. – Marc Gravell Mar 22 '14 at 20:50
  • @MarcGravell: yes I understand that. so, if you want all you callers to call `ContinueWith` without sync anyway, why doing your own `ContinueWith` without sync is a bad idea? the only concern I could see is that your `ContinueWith` would be a separate work item from their next continuation, but that is something that might be solved with a custom scheduler. – Andrey Shchekin Mar 22 '14 at 20:52
  • 2
    @Andrey because a: a lot of tasks never get continuations in the first place (especially when doing batch work) - this would force **every** task to have one, and b: even those that would have had a continuation now have much more complexity, overhead, and worker ops. This matters. – Marc Gravell Mar 22 '14 at 20:55
  • @MarcGravell: fair enough, so that's the case of _the problem in that not all of the responses are going to be processed at all_. I see, thanks. I'll try to think of something. – Andrey Shchekin Mar 22 '14 at 20:58
  • 1
    @Andrey not so much that they wont be processed at all - more that when they are either waited or awaited they often have already finished, which the regular TPL / C# code can handle without adding a continuation. If they weren't going to be processed *at all*, the library offers a "fire and forget" mode that hands them an already completed dummy task - they can do what they like with that :) – Marc Gravell Mar 22 '14 at 21:00
  • so this is what you don't want to happen? http://pastebin.com/rgPyEB6Q – Fredou Mar 23 '14 at 00:51
  • @Fredou correct; because that "Main thread continuation" would in reality be interrupting the primary reader. If external clients can do that, they can cause **huge** problems - for example, if they did database access, or (for whatever reason) a `Thread.Sleep`. I'd like to be able to prevent them from attaching for sync-exec, essentially. – Marc Gravell Mar 23 '14 at 00:57
  • ok, that is just me but maybe you could use the code in the pastebin instead of the one used in the question? and change the `Console.WriteLine("Press [return]");` to `Console.WriteLine("Press [return] on thread {0}", Thread.CurrentThread.ManagedThreadId);` it explain a little bit more the issue... :-) – Fredou Mar 23 '14 at 01:04
  • @Fredou I don't see that the pastebin version shows anything that isn't in the code in the question, but I will edit to clarify – Marc Gravell Mar 23 '14 at 01:07
  • i think my answer follow the second option in your list, force to use thread pool by removing the sync option – Fredou Mar 23 '14 at 02:31
  • This could be done so much easier using Rx and an `EventLoopScheduler` as opposed to `Task`. – Aron Jul 15 '19 at 01:14
  • @Aron it could be done much more nicely these days *without* Rx too; this goes back to the old days of tasks, but the APIs are hugely improved now – Marc Gravell Jul 15 '19 at 11:44

6 Answers6

52

New in .NET 4.6:

.NET 4.6 contains a new TaskCreationOptions: RunContinuationsAsynchronously.


Since you're willing to use Reflection to access private fields...

You can mark the TCS's Task with the TASK_STATE_THREAD_WAS_ABORTED flag, which would cause all continuations not to be inlined.

const int TASK_STATE_THREAD_WAS_ABORTED = 134217728;

var stateField = typeof(Task).GetField("m_stateFlags", BindingFlags.NonPublic | BindingFlags.Instance);
stateField.SetValue(task, (int) stateField.GetValue(task) | TASK_STATE_THREAD_WAS_ABORTED);

Edit:

Instead of using Reflection emit, I suggest you use expressions. This is much more readable and has the advantage of being PCL-compatible:

var taskParameter = Expression.Parameter(typeof (Task));
const string stateFlagsFieldName = "m_stateFlags";
var setter =
    Expression.Lambda<Action<Task>>(
        Expression.Assign(Expression.Field(taskParameter, stateFlagsFieldName),
            Expression.Or(Expression.Field(taskParameter, stateFlagsFieldName),
                Expression.Constant(TASK_STATE_THREAD_WAS_ABORTED))), taskParameter).Compile();

Without using Reflection:

If anyone's interested, I've figured out a way to do this without Reflection, but it is a bit "dirty" as well, and of course carries a non-negligible perf penalty:

try
{
    Thread.CurrentThread.Abort();
}
catch (ThreadAbortException)
{
    source.TrySetResult(123);
    Thread.ResetAbort();
}
Eli Arbel
  • 22,391
  • 3
  • 45
  • 71
  • I can see I'll be looking in reflector later for this flag! Interesting. Should I even ask how you found that? – Marc Gravell Mar 23 '14 at 08:11
  • I meant more: what caused you to be looking in this particular area - a similar issue? idle curiosity? or just this question? – Marc Gravell Mar 23 '14 at 08:50
  • Sure enough, there is is: `bool allowInlining = ((this.m_stateFlags & 134217728) == 0) && (Thread.CurrentThread.ThreadState != ThreadState.AbortRequested);` (the original code presumably uses the `TASK_STATE_THREAD_WAS_ABORTED` literal, obviously) – Marc Gravell Mar 23 '14 at 09:01
  • And rather perfectly, `Wait` uses an `ITaskCompletionAction`; `ITaskCompletionAction`s are *always* inlined. This basically does everything I would want: `Wait` isn't blocked, but external callers are never inlined. Just... evil but awesome discovery, thanks. – Marc Gravell Mar 23 '14 at 09:07
  • 3
    @MarcGravell Use this to create some pseudo-sample for the TPL team and make a change request about being able to do this via constructor options or something. – Adam Houldsworth Mar 23 '14 at 09:14
  • 1
    @Adam yeah, if you had to call this flag "what it does" rather than "what causes it", it would be something like `TaskCreationOptions.DoNotInline` - and wouldn't even need a ctor signature change to `TaskCompletionSource` – Marc Gravell Mar 23 '14 at 09:15
  • 2
    @AdamHouldsworth and don't worry, I'm already emailing them the same ;p – Marc Gravell Mar 23 '14 at 09:17
  • @MarcGravell Yeah I guessed you could use the options, fingers crossed that this is considered. If a connect ticket opens for it, post it here and I'll vote on it. – Adam Houldsworth Mar 23 '14 at 09:19
  • 1
    For your interest: here it is, optimized via `ILGenerator` etc: https://github.com/StackExchange/StackExchange.Redis/blob/master/StackExchange.Redis/StackExchange/Redis/TaskSource.cs – Marc Gravell Mar 23 '14 at 10:27
  • @MarcGravell, [here are the parts touching `TASK_STATE_THREAD_WAS_ABORTED` in the .NET Reference Sources](http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs#a7a7e7a3dbcd7c34#references). If I moved on with this hack, I'd study them very closely for any possible side effects. – noseratio Mar 23 '14 at 10:55
  • 1
    @Noseratio yup, checked them - thanks; they are all OK IMO; I agree this is pure workaround, but it has exactly the correct results. – Marc Gravell Mar 23 '14 at 11:00
  • 1
    PCL is not an immediate concern, but thanks: I'm family with both Expression and reflection - emit APIs (and many other metaprogramming wrappers). Besides: most frameworks where I would care about PCL will block non-public reflection. – Marc Gravell Mar 23 '14 at 11:59
  • @MarcGravell, indeed they look ok, but the continuation would use the thread pool. Is this not a concern anymore: *"I can't use the ThreadPool as an implementation, as it needs to work when the pool is saturated"* ? – noseratio Mar 23 '14 at 13:20
  • @Noseratio you misunderstand me: I don't care if the *downstream continuations* use the pool; the issue is that the task needs to be completed ASAP to prevent a deadlock - we can't push the *task completion* onto the pool. Here it is important to know that a task can have **two different classes** of "things to do at completion" - there are *continuations*, but there are also *completion actions* (entirely internal; drive things like `Wait()`). I need the *completion actions* to happen now, but not the continuations. The hacky fix proposed does exactly this. See also: `ITaskCompletionAction` – Marc Gravell Mar 23 '14 at 13:25
  • Please note that due to a [bug in the framework](https://support.microsoft.com/en-us/kb/3118695), TaskCreationOptions.RunContinuationsAsynchronously does not run continuations asynchronously in specific cases (Applies to .Net 4.6 and 4.6.1) – Absolom Mar 04 '16 at 16:49
9

I don't think there's anything in TPL which would provides explicit API control over TaskCompletionSource.SetResult continuations. I decided to keep my initial answer for controlling this behavior for async/await scenarios.

Here is another solution which imposes asynchronous upon ContinueWith, if the tcs.SetResult-triggered continuation takes place on the same thread the SetResult was called on:

public static class TaskExt
{
    static readonly ConcurrentDictionary<Task, Thread> s_tcsTasks =
        new ConcurrentDictionary<Task, Thread>();

    // SetResultAsync
    static public void SetResultAsync<TResult>(
        this TaskCompletionSource<TResult> @this,
        TResult result)
    {
        s_tcsTasks.TryAdd(@this.Task, Thread.CurrentThread);
        try
        {
            @this.SetResult(result);
        }
        finally
        {
            Thread thread;
            s_tcsTasks.TryRemove(@this.Task, out thread);
        }
    }

    // ContinueWithAsync, TODO: more overrides
    static public Task ContinueWithAsync<TResult>(
        this Task<TResult> @this,
        Action<Task<TResult>> action,
        TaskContinuationOptions continuationOptions = TaskContinuationOptions.None)
    {
        return @this.ContinueWith((Func<Task<TResult>, Task>)(t =>
        {
            Thread thread = null;
            s_tcsTasks.TryGetValue(t, out thread);
            if (Thread.CurrentThread == thread)
            {
                // same thread which called SetResultAsync, avoid potential deadlocks

                // using thread pool
                return Task.Run(() => action(t));

                // not using thread pool (TaskCreationOptions.LongRunning creates a normal thread)
                // return Task.Factory.StartNew(() => action(t), TaskCreationOptions.LongRunning);
            }
            else
            {
                // continue on the same thread
                var task = new Task(() => action(t));
                task.RunSynchronously();
                return Task.FromResult(task);
            }
        }), continuationOptions).Unwrap();
    }
}

Updated to address the comment:

I don't control the caller - I can't get them to use a specific continue-with variant: if I could, the problem would not exist in the first place

I wasn't aware you don't control the caller. Nevertheless, if you don't control it, you're probably not passing the TaskCompletionSource object directly to the caller, either. Logically, you'd be passing the token part of it, i.e. tcs.Task. In which case, the solution might be even easier, by adding another extension method to the above:

// ImposeAsync, TODO: more overrides
static public Task<TResult> ImposeAsync<TResult>(this Task<TResult> @this)
{
    return @this.ContinueWith(new Func<Task<TResult>, Task<TResult>>(antecedent =>
    {
        Thread thread = null;
        s_tcsTasks.TryGetValue(antecedent, out thread);
        if (Thread.CurrentThread == thread)
        {
            // continue on a pool thread
            return antecedent.ContinueWith(t => t, 
                TaskContinuationOptions.None).Unwrap();
        }
        else
        {
            return antecedent;
        }
    }), TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}

Use:

// library code
var source = new TaskCompletionSource<int>();
var task = source.Task.ImposeAsync();
// ... 

// client code
task.ContinueWith(delegate
{
    Identify();
}, TaskContinuationOptions.ExecuteSynchronously);

// ...
// library code
source.SetResultAsync(123);

This actually works for both await and ContinueWith (fiddle) and is free of reflection hacks.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    I don't control the caller - I can't get them to use a specific continue-with variant: if I could, the problem would not exist in the first place – Marc Gravell Mar 23 '14 at 07:26
  • @MarcGravell, I wasn't aware you can't control the caller. I posted an update on how I'd deal with it. – noseratio Mar 23 '14 at 08:26
  • the dilemma of the library author ;p Note that somebody found a much simpler and more direct way of achieving the desired result – Marc Gravell Mar 23 '14 at 09:10
4

What about instead of doing

var task = source.Task;

you do this instead

var task = source.Task.ContinueWith<Int32>( x => x.Result );

Thus you are always adding one continuation which will be executed asynchronously and then it doesn't matter if the subscribers want a continuation in the same context. It's sort of currying the task, isn't it?

Ivan Zlatanov
  • 5,146
  • 3
  • 29
  • 45
  • 1
    That came up in the comments (see Andrey); the problem *there* is that it **forces** all tasks to have a continuation when they wouldn't have otherwise, which is something that both `ContinueWith` and `await` normally try hard to avoid (by checking for already-complete etc) - and since this would force **everything** onto the workers, it would actually exacerbate the situation. It is a positive idea, and I thank you for it: but it won't help in this scenario. – Marc Gravell Mar 22 '14 at 22:29
4

The simulate abort approach looked really good, but led to the TPL hijacking threads in some scenarios.

I then had an implementation that was similar to checking the continuation object, but just checking for any continuation since there are actually too many scenarios for the given code to work well, but that meant that even things like Task.Wait resulted in a thread-pool lookup.

Ultimately, after inspecting lots and lots of IL, the only safe and useful scenario is the SetOnInvokeMres scenario (manual-reset-event-slim continuation). There are lots of other scenarios:

  • some aren't safe, and lead to thread hijacking
  • the rest aren't useful, as they ultimately lead to the thread-pool

So in the end, I opted to check for a non-null continuation-object; if it is null, fine (no continuations); if it is non-null, special-case check for SetOnInvokeMres - if it is that: fine (safe to invoke); otherwise, let the thread-pool perform the TrySetComplete, without telling the task to do anything special like spoofing abort. Task.Wait uses the SetOnInvokeMres approach, which is the specific scenario we want to try really hard not to deadlock.

Type taskType = typeof(Task);
FieldInfo continuationField = taskType.GetField("m_continuationObject", BindingFlags.Instance | BindingFlags.NonPublic);
Type safeScenario = taskType.GetNestedType("SetOnInvokeMres", BindingFlags.NonPublic);
if (continuationField != null && continuationField.FieldType == typeof(object) && safeScenario != null)
{
    var method = new DynamicMethod("IsSyncSafe", typeof(bool), new[] { typeof(Task) }, typeof(Task), true);
    var il = method.GetILGenerator();
    var hasContinuation = il.DefineLabel();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldfld, continuationField);
    Label nonNull = il.DefineLabel(), goodReturn = il.DefineLabel();
    // check if null
    il.Emit(OpCodes.Brtrue_S, nonNull);
    il.MarkLabel(goodReturn);
    il.Emit(OpCodes.Ldc_I4_1);
    il.Emit(OpCodes.Ret);

    // check if is a SetOnInvokeMres - if so, we're OK
    il.MarkLabel(nonNull);
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldfld, continuationField);
    il.Emit(OpCodes.Isinst, safeScenario);
    il.Emit(OpCodes.Brtrue_S, goodReturn);

    il.Emit(OpCodes.Ldc_I4_0);
    il.Emit(OpCodes.Ret);

    IsSyncSafe = (Func<Task, bool>)method.CreateDelegate(typeof(Func<Task, bool>));
Community
  • 1
  • 1
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
3

Updated, I posted a separate answer to deal with ContinueWith as opposed to await (because ContinueWith doesn't care about the current synchronization context).

You could use a dumb synchronization context to impose asynchrony upon continuation triggered by calling SetResult/SetCancelled/SetException on TaskCompletionSource. I believe the current synchronization context (at the point of await tcs.Task) is the criteria TPL uses to decide whether to make such continuation synchronous or asynchronous.

The following works for me:

if (notifyAsync)
{
    tcs.SetResultAsync(null);
}
else
{
    tcs.SetResult(null);
}

SetResultAsync is implemented like this:

public static class TaskExt
{
    static public void SetResultAsync<T>(this TaskCompletionSource<T> tcs, T result)
    {
        FakeSynchronizationContext.Execute(() => tcs.SetResult(result));
    }

    // FakeSynchronizationContext
    class FakeSynchronizationContext : SynchronizationContext
    {
        private static readonly ThreadLocal<FakeSynchronizationContext> s_context =
            new ThreadLocal<FakeSynchronizationContext>(() => new FakeSynchronizationContext());

        private FakeSynchronizationContext() { }

        public static FakeSynchronizationContext Instance { get { return s_context.Value; } }

        public static void Execute(Action action)
        {
            var savedContext = SynchronizationContext.Current;
            SynchronizationContext.SetSynchronizationContext(FakeSynchronizationContext.Instance);
            try
            {
                action();
            }
            finally
            {
                SynchronizationContext.SetSynchronizationContext(savedContext);
            }
        }

        // SynchronizationContext methods

        public override SynchronizationContext CreateCopy()
        {
            return this;
        }

        public override void OperationStarted()
        {
            throw new NotImplementedException("OperationStarted");
        }

        public override void OperationCompleted()
        {
            throw new NotImplementedException("OperationCompleted");
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            throw new NotImplementedException("Post");
        }

        public override void Send(SendOrPostCallback d, object state)
        {
            throw new NotImplementedException("Send");
        }
    }
}

SynchronizationContext.SetSynchronizationContext is very cheap in terms of the overhead it adds. In fact, a very similar approach is taken by the implementation of WPF Dispatcher.BeginInvoke.

TPL compares the target synchronization context at the point of await to that of the point of tcs.SetResult. If the synchronization context is the same (or there is no synchronization context at both places), the continuation is called directly, synchronously. Otherwise, it's queued using SynchronizationContext.Post on the target synchronization context, i.e., the normal await behavior. What this approach does is always impose the SynchronizationContext.Post behavior (or a pool thread continuation if there's no target synchronization context).

Updated, this won't work for task.ContinueWith, because ContinueWith doesn't care about the current synchronization context. It however works for await task (fiddle). It also does work for await task.ConfigureAwait(false).

OTOH, this approach works for ContinueWith.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Tempting, but changing the sync-context would almost certainly impact the calling application - for example, a web or Windows application that just happens to be using my library should not find the sync context changing hundreds of times per second. – Marc Gravell Mar 23 '14 at 01:45
  • @MarcGravell, I only change it for the scope of the `tcs.SetResult` call. It kinda becomes atomic and thread-safe this way, because the continuation itself will happen on *either* another pool thread or on the original sync. context captured at `await tcs.Task`. And `SynchronizationContext.SetSynchronizationContext` itself is very cheap, much cheaper than a thread switch itself. – noseratio Mar 23 '14 at 01:49
  • This however may not satisfy your second requirement: to not use `ThreadPool`. With this solution, the TPL will indeed use `ThreadPool`, if there was no sync. context (or it was the basic default one) at `await tcs.Task`. But this is the standard TPL behavior. – noseratio Mar 23 '14 at 01:57
  • Hmmm... since the sync-context is per-thread, this might actually be viable - and I wouldn't need to keep switching the ctx - just set it once for the worker thread. I will need to play with it – Marc Gravell Mar 23 '14 at 02:06
  • However, as shown it doesn't actually work for me; the continuation still happens on the primary thread; with your code, and using `source.SetResultAsync(123);` instead of `source.TrySetResult(123)`, I still see the output `"Continuation on: Main thread"` – Marc Gravell Mar 23 '14 at 02:13
  • @MarcGravell, this approach is specific to `await`. I've updated the answer with more details regarding this, and posted [another solution](http://stackoverflow.com/a/22587012/1768303) for `ContinueWith`. It's possible to combine both solutions. – noseratio Mar 23 '14 at 05:52
  • If it is the sync-context at the point of "await" that matters, then I certainly can't change it: that would break the arbitrary caller's code – Marc Gravell Mar 23 '14 at 07:31
  • @MarcGravell, TPL compares the target s.context at the point of `await` to that of the point of `tcs.SetResult`. If the s.context is the same (or not context at all at both places), the continuation is called directly, synchronously. Otherwise, it's queued using `SynchronizationContext.Post` of the target s.context, i.e., the normal `await` behavior. What this approach does is impose the `Post` (or a pool thread if there's no target s.context). I don't understand why it would break the arbitrary caller's code. Could you explain this point? – noseratio Mar 23 '14 at 07:53
  • 1
    @Noseration ah, right: it wasn't clear that the key point was them being *different*. Will look. Thanks. – Marc Gravell Mar 23 '14 at 07:56
3

if you can and are ready to use reflection, this should do it;

public static class MakeItAsync
{
    static public void TrySetAsync<T>(this TaskCompletionSource<T> source, T result)
    {
        var continuation = typeof(Task).GetField("m_continuationObject", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance);
        var continuations = (List<object>)continuation.GetValue(source.Task);

        foreach (object c in continuations)
        {
            var option = c.GetType().GetField("m_options", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance);
            var options = (TaskContinuationOptions)option.GetValue(c);

            options &= ~TaskContinuationOptions.ExecuteSynchronously;
            option.SetValue(c, options);
        }

        source.TrySetResult(result);
    }        
}
Fredou
  • 19,848
  • 10
  • 58
  • 113
  • This hack may simply stop working in the next version of the Framework. – noseratio Mar 23 '14 at 02:37
  • @Noseratio, true but it work now and they might also implement a proper way to do this in the next version – Fredou Mar 23 '14 at 02:49
  • But why would you need this if you simply can do `Task.Run(() => tcs.SetResult(result))` ? – noseratio Mar 23 '14 at 02:51
  • @Noseratio, I don't know, ask that question to Marc :-) , I'm simply removing the flag TaskContinuationOptions.ExecuteSynchronously on all task connected to a TaskCompletionSource which make sure they all use the threadpool instead of the main thread – Fredou Mar 23 '14 at 02:55
  • The m_continuationObject hack is actually the cheat i already use to identify potentially-problematic tasks - so this isn't beyond consideration. Interesting, thanks. This is the most useful-looking option so far. – Marc Gravell Mar 23 '14 at 07:34
  • This was a nice (ab)use of reflection (don't misunderstand me: it is the type of thing I'd do happily), but this is an *even nicer* (ab)use of reflection: http://stackoverflow.com/a/22588431/23354 – Marc Gravell Mar 23 '14 at 09:09