Unexpected values for AsyncLocal.Value when mixing ExecutionContext.SuppressFlow and tasks - c#

In an application I am experiencing odd behavior due to wrong/unexpected values of AsyncLocal: Despite I suppressed the flow of the execution context, I the AsyncLocal.Value-property is sometimes not reset within the execution scope of a newly spawned Task.
Below I created a minimal reproducible sample which demonstrates the problem:
private static readonly AsyncLocal<object> AsyncLocal = new AsyncLocal<object>();
[TestMethod]
public void Test()
{
Trace.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);
var mainTask = Task.Factory.StartNew(() =>
{
AsyncLocal.Value = "1";
Task anotherTask;
using (ExecutionContext.SuppressFlow())
{
anotherTask = Task.Run(() =>
{
Trace.WriteLine(AsyncLocal.Value); // "1" <- ???
Assert.IsNull(AsyncLocal.Value); // BOOM - FAILS
AsyncLocal.Value = "2";
});
}
Task.WaitAll(anotherTask);
});
mainTask.Wait(500000, CancellationToken.None);
}
In nine out of ten runs (on my pc) the outcome of the Test-method is:
.NET 6.0.2
"1"
-> The test fails
As you can see the test fails because within the action which is executed within Task.Run the the previous value is still present within AsyncLocal.Value (Message: 1).
My concrete questions are:
Why does this happen?
I suspect this happens because Task.Run may use the current thread to execute the work load. In that case, I assume lack of async/await-operators does not force the creation of a new/separate ExecutionContext for the action. Like Stephen Cleary said "from the logical call context’s perspective, all synchronous invocations are “collapsed” - they’re actually part of the context of the closest async method further up the call stack". If that’s the case I do understand why the same context is used within the action.
Is this the correct explanation for this behavior? In addition, why does it work flawlessly sometimes (about 1 run out of 10 on my machine)?
How can I fix this?
Assuming that my theory above is true it should be enough to forcefully introduce a new async "layer", like below:
private static readonly AsyncLocal<object> AsyncLocal = new AsyncLocal<object>();
[TestMethod]
public void Test()
{
Trace.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);
var mainTask = Task.Factory.StartNew(() =>
{
AsyncLocal.Value = "1";
Task anotherTask;
using (ExecutionContext.SuppressFlow())
{
var wrapper = () =>
{
Trace.WriteLine(AsyncLocal.Value);
Assert.IsNull(AsyncLocal.Value);
AsyncLocal.Value = "2";
return Task.CompletedTask;
};
anotherTask = Task.Run(async () => await wrapper());
}
Task.WaitAll(anotherTask);
});
mainTask.Wait(500000, CancellationToken.None);
}
This seems to fix the problem (it consistently works on my machine), but I want to be sure that this is a correct fix for this problem.
Many thanks in advance

Why does this happen? I suspect this happens because Task.Run may use the current thread to execute the work load.
I suspect that it happens because Task.WaitAll will use the current thread to execute the task inline.
Specifically, Task.WaitAll calls Task.WaitAllCore, which will attempt to run it inline by calling Task.WrappedTryRunInline. I'm going to assume the default task scheduler is used throughout. In that case, this will invoke TaskScheduler.TryRunInline, which will return false if the delegate is already invoked. So, if the task has already started running on a thread pool thread, this will return back to WaitAllCore, which will just do a normal wait, and your code will work as expected (1 out of 10).
If a thread pool thread hasn't picked it up yet (9 out of 10), then TaskScheduler.TryRunInline will call TaskScheduler.TryExecuteTaskInline, the default implementation of which will call Task.ExecuteEntryUnsafe, which calls Task.ExecuteWithThreadLocal. Task.ExecuteWithThreadLocal has logic for applying an ExecutionContext if one was captured. Assuming none was captured, the task's delegate is just invoked directly.
So, it seems like each step is behaving logically. Technically, what ExecutionContext.SuppressFlow means is "don't capture the ExecutionContext", and that is what is happening. It doesn't mean "clear the ExecutionContext". Sometimes the task is run on a thread pool thread (without the captured ExecutionContext), and WaitAll will just wait for it to complete. Other times the task will be executed inline by WaitAll instead of a thread pool thread, and in that case the ExecutionContext is not cleared (and technically isn't captured, either).
You can test this theory by capturing the current thread id within your wrapper and comparing it to the thread id doing the Task.WaitAll. I expect that they will be the same thread for the runs where the async local value is (unexpectedly) inherited, and they will be different threads for the runs where the async local value works as expected.

If you can, I'd first consider whether it's possible to replace the thread-specific caches with a single shared cache. The app likely predates useful types such as ConcurrentDictionary.
If it isn't possible to use a singleton cache, then you can use a stack of async local values. Stacking async local values is a common pattern. I prefer wrapping the stack logic into a separate type (AsyncLocalValue in the code below):
public sealed class AsyncLocalValue
{
private static readonly AsyncLocal<ImmutableStack<object>> _asyncLocal = new();
public object Value => _asyncLocal.Value?.Peek();
public IDisposable PushValue(object value)
{
var originalValue = _asyncLocal.Value;
var newValue = (originalValue ?? ImmutableStack<object>.Empty).Push(value);
_asyncLocal.Value = newValue;
return Disposable.Create(() => _asyncLocal.Value = originalValue);
}
}
private static AsyncLocalValue AsyncLocal = new();
[TestMethod]
public void Test()
{
Console.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);
var mainTask = Task.Factory.StartNew(() =>
{
Task anotherTask;
using (AsyncLocal.PushValue("1"))
{
using (AsyncLocal.PushValue(null))
{
anotherTask = Task.Run(() =>
{
Console.WriteLine("Observed: " + AsyncLocal.Value);
using (AsyncLocal.PushValue("2"))
{
}
});
}
}
Task.WaitAll(anotherTask);
});
mainTask.Wait(500000, CancellationToken.None);
}
This code sample uses Disposable.Create from my Nito.Disposables library.

Related

Func doesn't work & blocked during test execution

Prerequisites:
.Net 6.0
C# 10
NUnit 3.13.3
Context:
Try to run an unit test, but run into some kind of thread blocker. The code just stop execution on
value = await getDataToCacheAsync.Invoke();
The last row that can be debugged is
return () => new Task<string?>(() => cacheValue [there]);
Q: It looks like there is some kind of deadlock happened, but it's not clear for me why and how it can be addressed
Unit test:
[Test]
public async Task GetCachedValueAsync_WithDedicatedCacheKey_ReturnsExpectedCacheValue()
{
const string cacheKey = "test-cache-key";
const string cacheValue = "test-cache-key";
var result = await _sut.GetCachedValueAsync(cacheKey, GetDataToCacheAsync(cacheValue));
Assert.AreEqual(cacheValue, result);
}
private static Func<Task<string?>> GetDataToCacheAsync(string cacheValue)
{
return () => new Task<string?>(() => cacheValue);
}
The code under test:
public async Task<T?> GetCachedValueAsync<T>(string cacheKey, Func<Task<T?>> getDataToCacheAsync)
where T : class
{
// [Bloked here, nothing happens then, I'm expecting that it should return "test-cache-value"]
value = await getDataToCacheAsync.Invoke(); [Blocked]
...
return value
}
After
return () => new Task<string?>(() => cacheValue [there]);
was replaces with
return () => Task.FromResult(cacheValue);
it started work
UPD:
It seems like the root cause is that a task should be started directly before awaiting it, in such cases (e.g. Task.Run(...), TaskFactory ... etc.).
Task.FromResult returns already completed task with a result
As written in the docs generally you should try to avoid creating tasks via constructor:
This constructor should only be used in advanced scenarios where it is required that the creation and starting of the task is separated.
Rather than calling this constructor, the most common way to instantiate a Task object and launch a task is by calling the static Task.Run(Action) or TaskFactory.StartNew(Action) method.
The issue being that task created by constructor is a "cold" one (and according to guidelines you should avoid returning "cold" tasks from methods, only "hot" ones) - it is not started so await will result in endless wait (actually no deadlock happening here).
There are multiple ways to fix this code, for example:
Use Task.Run:
return () => Task.Run(() => cacheValue);
Start created task manually (though in this case there is no reason to. Also as noted by #Theodor Zoulias - it is recommended to specify explicitly the scheduler when calling the Start method. Otherwise the task will be scheduled on the ambient TaskScheduler.Current, which can be a source of some issues):
return () =>
{
var task = new Task<string?>(() => cacheValue);
task.Start();
return task;
}
Return a completed task (which I think is the best way in this case, unless you are testing a very specific scenario):
return () => Task.FromResult(cacheValue);

Why does my code run on multiple threads?

Since a pretty long time I'm trying to understand async-await stuff in .NET, but I struggle to succeed, there's always something totally unexpected happening when I use async.
Here's my application:
namespace ConsoleApp3
{
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Hello World!");
var work1 = new WorkClass();
var work2 = new WorkClass();
while(true)
{
work1.DoWork(500);
work2.DoWork(1500);
}
}
}
public class WorkClass
{
public async Task DoWork(int delayMs)
{
var x = 1;
await Task.Delay(delayMs)
var y = 2;
}
}
}
It's just a sample that I created to check how the code will be executed. There are a few things that surprise me.
First off, there are many threads involved! If I set a breakpoint on var y = 2; I can see that threadId is not the same there, it can be 1, or 5, or 6, or something else.
Why is that? I thought that async/await does not use additional threads on its own unless I explicitly command that (by using Task.Run or creating a new Thread). At least this article tries to say that I think.
Ok, but let's say that there are some other threads for whatever reason - even if they are, my await Task.Delay(msDelay); does not have ConfigureAwait(false)! As I understand it, without this call, thread shouldn't change.
It's really difficult for me to grasp the concept well, because I cannot find any good resource that would contain all information instead of just a few pieces of information.
When an asynchronous method awaits something, if it's not complete, it schedules a continuation and then returns. The question is which thread the continuation runs on. If there's a synchronization context, the continuation is scheduled to run within that context - typically a UI thread, or potentially a specific pool of threads.
In your case, you're running a console app which means there is no synchronization context (SynchronizationContext.Current will return null). In that case, continuations are run on thread pool threads. It's not that a new thread is specifically created to run the continuation - it's just that the thread pool will pick up the continuation, whereas the "main" thread won't run the continuation.
ConfigureAwait(false) is used to indicate that you don't want to return to the current synchronization context for the continuation - but as there's no synchronization context anyway in your case, it would make no difference.
Async/await does not use additional threads on its own, but in your example it is not on its own. You are calling Task.Delay, and this method schedules a continuation to run in a thread-pool thread. There is no thread blocked during the delay though. A new thread is not created. When the time comes an existing thread is used to run the continuation, which in your case has very little work to do (just run the var y = 2 assignment), because you are not even awaiting the task returned by DoWork. When this work is done (a fraction of a microsecond later) the thread-pool thread is free again to do other jobs.
Instead of Task.Delay you could await another method that makes no use of threads at all, or a method that creates a dedicated long running thread, or a method that starts a new process. Async/await is not responsible for any of these. Async/await is just a mechanism for creating task continuations in a developer-friendly way.
Here is your application modified for a world without async/await:
class Program
{
static Task Main(string[] args)
{
Console.WriteLine("Hello World!");
var work1 = new WorkClass();
var work2 = new WorkClass();
while (true)
{
work1.DoWork(500);
work2.DoWork(1500);
}
}
}
public class WorkClass
{
public Task DoWork(int delayMs)
{
var x = 1;
int y;
return Task.Delay(delayMs).ContinueWith(_ =>
{
y = 2;
});
}
}

Should/Could this "recursive Task" be expressed as a TaskContinuation?

In my application I have the need to continually process some piece(s) of Work on some set interval(s). I had originally written a Task to continually check a given Task.Delay to see if it was completed, if so the Work would be processed that corresponded to that Task.Delay. The draw back to this method is the Task that checks these Task.Delays would be in a psuedo-infinite loop when no Task.Delay is completed.
To solve this problem I found that I could create a "recursive Task" (I am not sure what the jargon for this would be) that processes the work at the given interval as needed.
// New Recurring Work can be added by simply creating
// the Task below and adding an entry into this Dictionary.
// Recurring Work can be removed/stopped by looking
// it up in this Dictionary and calling its CTS.Cancel method.
private readonly object _LockRecurWork = new object();
private Dictionary<Work, Tuple<Task, CancellationTokenSource> RecurringWork { get; set; }
...
private Task CreateRecurringWorkTask(Work workToDo, CancellationTokenSource taskTokenSource)
{
return Task.Run(async () =>
{
// Do the Work, then wait the prescribed amount of time before doing it again
DoWork(workToDo);
await Task.Delay(workToDo.RecurRate, taskTokenSource.Token);
// If this Work's CancellationTokenSource is not
// cancelled then "schedule" the next Work execution
if (!taskTokenSource.IsCancellationRequested)
{
lock(_LockRecurWork)
{
RecurringWork[workToDo] = new Tuple<Task, CancellationTokenSource>
(CreateRecurringWorkTask(workToDo, taskTokenSource), taskTokenSource);
}
}
}, taskTokenSource.Token);
}
Should/Could this be represented with a chain of Task.ContinueWith? Would there be any benefit to such an implementation? Is there anything majorly wrong with the current implementation?
Yes!
Calling ContinueWith tells the Task to call your code as soon as it finishes. This is far faster than manually polling it.

The difference between Rx Throttle(...).ObserveOn(scheduler) and Throttle(..., scheduler)

I have the following code:
IDisposable subscription = myObservable.Throttle(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
.Subscribe(_ => UpdateUi());
As expected, UpdateUi() will always execute on the main thread. When I change the code to
IDisposable subscription = myObservable.Throttle(TimeSpan.FromMilliseconds(50))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ => UpdateUi());
UpdateUI() will be executed in a background thread.
Why is not Throttle(...).ObserveOn(scheduler) equivalent to Throttle(..., scheduler)?
In both examples in code you've given UpdateUi will always be invoked on the scheduler specified by RxApp.MainThreadScheduler. I can say this with some certainty since ObserveOn is a decorator that ensures the OnNext handler of subscribers is called on the specified scheduler. See here for an in-depth analysis.
So that said, this is a bit puzzling. Either RxApp.MainThreadScheduler is not referring to the correct dispatcher scheduler or UpdateUi is transitioning off the dispatcher thread. The former is not unprecedented - see https://github.com/reactiveui/ReactiveUI/issues/768 where others have run into this. I have no idea what the issue was in that case. Perhaps #PaulBetts can weigh in, or you could raise an issue at https://github.com/reactiveui/. Whatever the case, I would carefully check your assumptions here since I would expect this to be a well tested area. Do you have a complete repro?
As to your specific question, the difference between Throttle(...).ObserveOn(scheduler) and Throttle(..., scheduler) is as follows:
In the first case when Throttle is specified without a scheduler it will use the default platform scheduler to introduce the concurrency necessary to run it's timer - on WPF this would use a thread pool thread. So all the throttling will be done on a background thread and, due to the following ObserveOn the released events only will be passed to the subscriber on the specified scheduler.
In the case where Throttle specifies a scheduler, the throttling is done on that scheduler - both suppressed events and released events will be managed on that scheduler and the subscriber will be called on that same scheduler too.
So either way, the UpdateUi will be called on the RxApp.MainThreadScheduler.
You are best off throttling ui events on the dispatcher in most cases since it's generally more costly to run separate timers on a background thread and pay for the context switch if only a fraction of events are going to make it through the throttle.
So, just to check you haven't run into an issue with RxApp.MainThreadScheduler, I would try specifying the scheduler or SynchronizationContext explicitly via another means. How to do this will depend on the platform you are on - ObserveOnDispatcher() is hopefully available, or use a suitable ObserveOn overload. There are options for controls, syncronizationcontexts and schedulers given the correct Rx libraries are imported.
After some investigation I believe this is caused by a different version of Rx being used run time than I expect (I develop a plugin for a third-party application).
I'm not sure why, but it seems that the default RxApp.MainThreadScheduler fails to initialize correctly. The default instance is a WaitForDispatcherScheduler (source). All functions in this class rely attemptToCreateScheduler:
IScheduler attemptToCreateScheduler()
{
if (_innerScheduler != null) return _innerScheduler;
try {
_innerScheduler = _schedulerFactory();
return _innerScheduler;
} catch (Exception) {
// NB: Dispatcher's not ready yet. Keep using CurrentThread
return CurrentThreadScheduler.Instance;
}
}
What seems to happen in my case is that _schedulerFactory() throws, resulting in CurrentThreadScheduler.Instance to be returned instead.
By manually initializing the RxApp.MainThreadScheduler to new SynchronizationContextScheduler(SynchronizationContext.Current) behavior is as expected.
I've just bumped into an issue that first led me to this question and then to some experimenting.
It turns out, Throttle(timeSpan, scheduler) is clever enough to "cancel" an already scheduled debounced event X, in case the source emits another event Y before X gets observed. Thus, only Y will be eventually observed (provided it's the last debounced event).
With Throttle(timeSpan).ObserveOn(scheduler), both X and Y will be observed.
So, conceptually, that's an important difference between the two approaches. Sadly, Rx.NET docs are scarce, but I believe this behavior is by design and it makes sense to me.
To illustrate this with an example (fiddle):
#nullable enable
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using static System.Console;
public class Program
{
static async Task ThrottleWithScheduler()
{
WriteLine($"\n{nameof(ThrottleWithScheduler)}\n");
var sc = new CustomSyncContext();
var scheduler = new SynchronizationContextScheduler(sc);
var subj = new BehaviorSubject<string>("A");
subj
.Do(v => WriteLine($"Emitted {v} on {sc.Elapsed}ms"))
.Throttle(TimeSpan.FromMilliseconds(500), scheduler)
.Subscribe(v => WriteLine($"Observed {v} on {sc.Elapsed}ms"));
await Task.Delay(100);
subj.OnNext("B");
await Task.Delay(200);
subj.OnNext("X");
await Task.Delay(550);
subj.OnNext("Y");
await Task.Delay(2000);
WriteLine("Finished!");
}
static async Task ThrottleWithObserveOn()
{
WriteLine($"\n{nameof(ThrottleWithObserveOn)}\n");
var sc = new CustomSyncContext();
var scheduler = new SynchronizationContextScheduler(sc);
var subj = new BehaviorSubject<string>("A");
subj
.Do(v => WriteLine($"Emitted {v} on {sc.Elapsed}ms"))
.Throttle(TimeSpan.FromMilliseconds(500))
.ObserveOn(scheduler)
.Subscribe(v => WriteLine($"Observed {v} on {sc.Elapsed}ms"));
await Task.Delay(100);
subj.OnNext("B");
await Task.Delay(200);
subj.OnNext("X");
await Task.Delay(550);
subj.OnNext("Y");
await Task.Delay(2000);
WriteLine("Finished!");
}
public static async Task Main()
{
await ThrottleWithScheduler();
await ThrottleWithObserveOn();
}
}
class CustomSyncContext : SynchronizationContext
{
private readonly Stopwatch _sw = Stopwatch.StartNew();
public long Elapsed { get { lock (_sw) { return _sw.ElapsedMilliseconds; } } }
public override void Post(SendOrPostCallback d, object? state)
{
WriteLine($"Scheduled on {Elapsed}ms");
Task.Delay(100).ContinueWith(
continuationAction: _ =>
{
WriteLine($"Executed on {Elapsed}ms");
d(state);
},
continuationOptions: TaskContinuationOptions.ExecuteSynchronously);
}
}
Output:
ThrottleWithScheduler
Emitted A on 18ms
Emitted B on 142ms
Emitted X on 351ms
Scheduled on 861ms
Emitted Y on 907ms
Executed on 972ms
Scheduled on 1421ms
Executed on 1536ms
Observed Y on 1539ms
Finished!
ThrottleWithObserveOn
Emitted A on 4ms
Emitted B on 113ms
Emitted X on 315ms
Scheduled on 837ms
Emitted Y on 886ms
Executed on 951ms
Observed X on 953ms
Scheduled on 1391ms
Executed on 1508ms
Observed Y on 1508ms
Finished!

Why does this async call not complete? [duplicate]

I have come across a problem with a unit test that failed because a TPL Task never executed its ContinueWith(x, TaskScheduler.FromCurrentSynchronizationContext()).
The problem turned out to be because a Winforms UI Control was accidentally being created before the Task was started.
Here is an example that reproduces it. You will see that if you run the test as-is, it passes. If you run the test with the Form line uncommented, it fails.
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
// Create new sync context for unit test
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
var waitHandle = new ManualResetEvent(false);
var doer = new DoSomethinger();
//Uncommenting this line causes the ContinueWith part of the Task
//below never to execute.
//var f = new Form();
doer.DoSomethingAsync(() => waitHandle.Set());
Assert.IsTrue(waitHandle.WaitOne(10000), "Wait timeout exceeded.");
}
}
public class DoSomethinger
{
public void DoSomethingAsync(Action onCompleted)
{
var task = Task.Factory.StartNew(() => Thread.Sleep(1000));
task.ContinueWith(t =>
{
if (onCompleted != null)
onCompleted();
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
Can anyone explain why this is the case?
I thought it might have been because the wrong SynchronizationContext is used, but actually, the ContinueWith never executes at all! And besides, in this unit test, whether or not it is the correct SynchronizationContext is irrelevant because as long as the waitHandle.set() is called on any thread, the test should pass.
I overlooked the comments section in your code, Indeed that fails when uncommenting the var f = new Form();
Reason is subtle, Control class will automatically overwrite the synchronization context to WindowsFormsSynchronizationContext if it sees that SynchronizationContext.Current is null or its is of type System.Threading.SynchronizationContext.
As soon as Control class overwrite the SynchronizationContext.Current with WindowsFormsSynchronizationContext, all the calls to Send and Post expects the windows message loop to be running in order to work. That's not going to happen till you created the Handle and you run a message loop.
Relevant part of the problematic code:
internal Control(bool autoInstallSyncContext)
{
...
if (autoInstallSyncContext)
{
//This overwrites your SynchronizationContext
WindowsFormsSynchronizationContext.InstallIfNeeded();
}
}
You can refer the source of WindowsFormsSynchronizationContext.InstallIfNeeded here.
If you want to overwrite the SynchronizationContext, you need your custom implementation of SynchronizationContext to make it work.
Workaround:
internal class MyContext : SynchronizationContext
{
}
[TestMethod]
public void TestMethod1()
{
// Create new sync context for unit test
SynchronizationContext.SetSynchronizationContext(new MyContext());
var waitHandle = new ManualResetEvent(false);
var doer = new DoSomethinger();
var f = new Form();
doer.DoSomethingAsync(() => waitHandle.Set());
Assert.IsTrue(waitHandle.WaitOne(10000), "Wait timeout exceeded.");
}
Above code works as expected :)
Alternatively you could set WindowsFormsSynchronizationContext.AutoInstall to false, that will prevent automatic overwriting of the synchronization context mentioned above.(Thanks for OP #OffHeGoes for mentioning this in comments)
With the line commented out, your SynchronizationContext is the default one you created. This will cause TaskScheduler.FromCurrentSynchrozisationContext() to use the default scheduler, which will run the continuation on the thread pool.
Once you create a Winforms object like your Form, the current SynchronizationContext becomes a WindowsFormsSynchronizationContext, which in turn will return a scheduler that depends on the WinForms message pump to schedule the continuation.
Since there is no WinForms pump in a unit test, the continuation never gets run.

Categories

Resources