How can I prevent synchronous continuations on a Task?
Asked Answered
L

6

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();
    }
}
Liberty answered 22/3, 2014 at 14:56 Comment(27)
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.Kiwanis
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.Polynuclear
@Polynuclear 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 itLiberty
Would completing the TaskCompletionSource on another thread work?Jugurtha
@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 callersLiberty
Create your own TaskScheduler, return false from TryExecuteTaskInline(). I think.Knuckleduster
@Hans no can do; TaskCompletionSource<T> 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 mootLiberty
@MarcGravell: There's no solution for this, sorry. I have invoked TrySet* on a background thread as a hack. E.g., TrySetResultWithBackgroundContinuations here. I could not find a better way.Foxtrot
@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.Liberty
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).Dispose
@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).Liberty
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.Dispose
@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 situationLiberty
@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?Dispose
@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.Liberty
@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.Liberty
@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.Dispose
@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.Liberty
@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.Dispose
@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 :)Liberty
so this is what you don't want to happen? pastebin.com/rgPyEB6QJapeth
@Japeth 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.Liberty
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... :-)Japeth
@Japeth I don't see that the pastebin version shows anything that isn't in the code in the question, but I will edit to clarifyLiberty
i think my answer follow the second option in your list, force to use thread pool by removing the sync optionJapeth
This could be done so much easier using Rx and an EventLoopScheduler as opposed to Task.Encephaloma
@Encephaloma 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 nowLiberty
C
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();
}
Cassiecassil answered 23/3, 2014 at 8:6 Comment(15)
I can see I'll be looking in reflector later for this flag! Interesting. Should I even ask how you found that?Liberty
I meant more: what caused you to be looking in this particular area - a similar issue? idle curiosity? or just this question?Liberty
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)Liberty
And rather perfectly, Wait uses an ITaskCompletionAction; ITaskCompletionActions 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.Liberty
@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.Nodab
@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 TaskCompletionSourceLiberty
@AdamHouldsworth and don't worry, I'm already emailing them the same ;pLiberty
@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.Nodab
For your interest: here it is, optimized via ILGenerator etc: github.com/StackExchange/StackExchange.Redis/blob/master/…Liberty
@MarcGravell, here are the parts touching TASK_STATE_THREAD_WAS_ABORTED in the .NET Reference Sources. If I moved on with this hack, I'd study them very closely for any possible side effects.Barabarabarabas
@Noseratio yup, checked them - thanks; they are all OK IMO; I agree this is pure workaround, but it has exactly the correct results.Liberty
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.Liberty
@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" ?Barabarabarabas
@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: ITaskCompletionActionLiberty
Please note that due to a bug in the framework, TaskCreationOptions.RunContinuationsAsynchronously does not run continuations asynchronously in specific cases (Applies to .Net 4.6 and 4.6.1)Jimjams
B
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.

Barabarabarabas answered 23/3, 2014 at 4:16 Comment(3)
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 placeLiberty
@MarcGravell, I wasn't aware you can't control the caller. I posted an update on how I'd deal with it.Barabarabarabas
the dilemma of the library author ;p Note that somebody found a much simpler and more direct way of achieving the desired resultLiberty
S
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?

Song answered 22/3, 2014 at 22:25 Comment(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.Liberty
L
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>));
Liberty answered 1/9, 2014 at 12:30 Comment(0)
B
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.

Barabarabarabas answered 23/3, 2014 at 1:42 Comment(9)
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.Liberty
@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.Barabarabarabas
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.Barabarabarabas
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 itLiberty
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"Liberty
@MarcGravell, this approach is specific to await. I've updated the answer with more details regarding this, and posted another solution for ContinueWith. It's possible to combine both solutions.Barabarabarabas
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 codeLiberty
@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?Barabarabarabas
@Noseration ah, right: it wasn't clear that the key point was them being different. Will look. Thanks.Liberty
J
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);
    }        
}
Japeth answered 23/3, 2014 at 2:22 Comment(6)
This hack may simply stop working in the next version of the Framework.Barabarabarabas
@Noseratio, true but it work now and they might also implement a proper way to do this in the next versionJapeth
But why would you need this if you simply can do Task.Run(() => tcs.SetResult(result)) ?Barabarabarabas
@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 threadJapeth
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.Liberty
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: https://mcmap.net/q/95080/-how-can-i-prevent-synchronous-continuations-on-a-taskLiberty

© 2022 - 2024 — McMap. All rights reserved.