I have a Windows Service that monitors my application by running a couple of tests every second. A bug report has been submitted that said that the service stoppes running after a while, and I'm trying to figure out why.
I suspect that the code below is the culprit, but I have trouble understanding exactly how it works. The ContinueWith statement has recently been commented out, but I dont know if it is needed
private Task CreateTask(Action action)
{
var ct = _cts.Token;
return Task.Run(async () =>
{
ct.ThrowIfCancellationRequested();
var sw = new Stopwatch();
while (true)
{
sw.Restart();
action();
if (ct.IsCancellationRequested)
{
_logger.Debug("Cancellation requested");
break;
}
var wait = _settings.loopStepFrequency - sw.ElapsedMilliseconds;
if (wait <= 0) // No need to delay
continue;
// If ContinueWith is needed wrap this in an ugly try/catch
// handling the exception
await Task.Delay(
(int)(_settings.loopStepFrequency - sw.ElapsedMilliseconds),
ct); //.ContinueWith(tsk => { }, ct);
}
_logger.Debug("Task was cancelled");
}, _cts.Token);
}
Are there any obvious problems with this code?
Are there any obvious problems with this code?
The one that jumps out to me is the calculation for the number of milliseconds to delay. Specifically, there's no floor. If action() takes an unusually long time, then the task could fail in a possibly unexpected way.
There are several ways for the task to complete in either a cancelled or failed state, or it can delay forever:
The task can be cancelled before the delegate begins, due to the cancellation token passed to Task.Run.
The task can be cancelled by the ThrowIfCancellationRequested call.
The task can complete successfully after being cancelled, due to the IsCancellationRequested logic.
The task can be cancelled by the cancellation token passed to Task.Delay.
The task may fail with an ArgumentOutOfRangeException if _settings.loopStepFrequency - sw.ElapsedMilliseconds is less than -1. This is probably a bug.
The task may delay indefinitely (until cancelled) if _settings.loopStepFrequency - sw.ElapsedMilliseconds happens to be exactly -1. This is probably a bug.
To fix this code, I recommend two things:
The code is probably intending to do await Task.Delay((int) wait, ct); instead of await Task.Delay((int)(_settings.loopStepFrequency - sw.ElapsedMilliseconds), ct);. This will remove the last two conditions above.
Choose one method of cancellation. The standard pattern to express cancellation is via OperationCanceledExcpetion; this is the pattern used by ThrowIfCancellationRequested and by Task.Delay. The IsCancellationRequested check is using a different pattern; it will successfully complete the task on cancellation, instead of cancelling it.
There are so many problems with this code, that makes more sense to rewrite it than attempt to fix it. Here is a possible way to rewrite this method, with some (possibly superfluous) argument validation added:
private Task CreateTask(Action action)
{
if (action == null) throw new ArgumentNullException(nameof(action));
var ct = _cts.Token;
var delayMsec = _settings.loopStepFrequency;
if (delayMsec <= 0) throw new ArgumentOutOfRangeException("loopStepFrequency");
return Task.Run(async () =>
{
while (true)
{
var delayTask = Task.Delay(delayMsec, ct);
action();
await delayTask;
}
}, ct);
}
The responsibility for logging a possible exception/cancellation belongs now to the caller of the method, that (hopefully) awaits the created task.
var task = CreateTask(TheAction);
try
{
await task; // If the caller is async
//task.GetAwaiter().GetResult(); // If the caller is sync
_logger.Info("The task completed successfully");
}
catch (OperationCanceledException)
{
_logger.Info("The task was canceled");
}
catch (Exception ex)
{
_logger.Error("The task failed", ex);
}
Related
This question already has an answer here:
Faulted vs Canceled task status after CancellationToken.ThrowIfCancellationRequested
(1 answer)
Closed last month.
According to this and this, passing a cancellation token to a task constructor, or Task.Run, will cause the task to be associated with said token, causing the task to transition to Canceled instead of Faulted if a cancellation exception occurs.
I've been fiddling with these examples for a while, and I can't see any benefits other than preventing a cancelled task to start.
Changing the code on this MSDN example from
tc = Task.Run(() => DoSomeWork(i, token), token);
to
tc = Task.Run(() => DoSomeWork(i, token));
produced the exact same output:
This code also results in two cancelled state tasks with the same exceptions thrown:
var token = cts.Token;
var t1 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
});
var t2 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
}, token);
Console.ReadKey();
try
{
cts.Cancel();
Task.WaitAll(t1, t2);
}
catch(Exception e)
{
if (e is AggregateException)
{
foreach (var ex in (e as AggregateException).InnerExceptions)
{
Console.WriteLine(e.Message);
}
}
else
Console.WriteLine(e.Message);
}
Console.WriteLine($"without token: { t1.Status }");
Console.WriteLine($"with token: { t2.Status }");
Console.WriteLine("Done.");
Apparently, throwing OperationCanceledException from within the task is enough to make it transition to Canceled instead of Faulted. So my question is: is there a reason for passing the token to the task other than preventing a cancelled task to run?
Is there a reason for passing the token to the task other than preventing a cancelled task to run?
In this particular case, No. Past the point in time that the task has started running, the token has no effect to the outcome.
The Task.Run method has many overloads. This case is peculiar because of the infinite while loop.
var t1 = Task.Run(() =>
{
while (true)
{
Thread.Sleep(1000);
token.ThrowIfCancellationRequested();
};
});
The compiler has to choose between these two overloads:
public static Task Run(Action action);
public static Task Run(Func<Task> function);
...and for a reason explained in this question it chooses the later. Here is the implementation of this overload:
public static Task Run(Func<Task?> function, CancellationToken cancellationToken)
{
if (function == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.function);
// Short-circuit if we are given a pre-canceled token
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
// Kick off initial Task, which will call the user-supplied function and yield a Task.
Task<Task?> task1 = Task<Task?>.Factory.StartNew(function, cancellationToken,
TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
// Create a promise-style Task to be used as a proxy for the operation
// Set lookForOce == true so that unwrap logic can be on the lookout for OCEs thrown
// as faults from task1, to support in-delegate cancellation.
UnwrapPromise<VoidTaskResult> promise = new UnwrapPromise<VoidTaskResult>(task1,
lookForOce: true);
return promise;
}
The important detail is the lookForOce: true. Let's look inside the UnwrapPromise class:
// "Should we check for OperationCanceledExceptions on the outer task and interpret them
// as proxy cancellation?"
// Unwrap() sets this to false, Run() sets it to true.
private readonly bool _lookForOce;
..and at another point below:
case TaskStatus.Faulted:
List<ExceptionDispatchInfo> edis = task.GetExceptionDispatchInfos();
ExceptionDispatchInfo oceEdi;
if (lookForOce && edis.Count > 0 &&
(oceEdi = edis[0]) != null &&
oceEdi.SourceException is OperationCanceledException oce)
{
result = TrySetCanceled(oce.CancellationToken, oceEdi);
}
else
{
result = TrySetException(edis);
}
break;
So although the internally created Task<Task?> task1 ends up in a Faulted state, its unwrapped version ends up as Canceled, because the type of the exception is
OperationCanceledException (abbreviated as oce in the code).
That's a quite convoluted journey in the history of TPL, with methods introduced at different times and frameworks, in order to serve different purposes. The end result is a little bit of inconsistency, or nuanced behavior if you prefer to say it so. A relevant article that you might find interesting is this: Task.Run vs Task.Factory.StartNew by Stephen Toub.
I have a simple console application
class Program
{
private static void MyTask(object obj)
{
var cancellationToken = (CancellationToken) obj;
if(cancellationToken.IsCancellationRequested)
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine("MyTask() started");
for (var i = 0; i < 10; i++)
{
try
{
if (cancellationToken.IsCancellationRequested)
cancellationToken.ThrowIfCancellationRequested();
}
catch (Exception ex)
{
return;
}
Console.WriteLine($"Counter in MyTask() = {i}");
Thread.Sleep(500);
}
Console.WriteLine("MyTask() finished");
}
static void Main(string[] args)
{
var cancelationTokenSource = new CancellationTokenSource();
var task = Task.Factory.StartNew(MyTask, cancelationTokenSource.Token,
cancelationTokenSource.Token);
Thread.Sleep(3000);
try
{
cancelationTokenSource.Cancel();
task.Wait();
}
catch (Exception ex)
{
if(task.IsCanceled)
Console.WriteLine("Task has been cancelled");
Console.WriteLine(ex.Message);
}
finally
{
cancelationTokenSource.Dispose();
task.Dispose();
}
Console.WriteLine("Main finished");
Console.ReadLine();
}
}
I'm trying to start new Task and after some time cancel it. Is there any other way to achieve this result instead of using this
if(cancellationToken.IsCancellationRequested)
cancellationToken.ThrowIfCancellationRequested();
on every iteration in for loop? Why do we have to check cancellationToken.IsCancellationRequested on every iteration, maybe we can to use something else?
In this specific case you could avoid the .ThrowIfCancellationRequested(), and instead simply use a break to stop the execution of the loop and then finish the Task. The ThrowIfCancellationRequested is more useful in deeper task trees, where there are many more descendants and it is more difficult to maintain cancellation.
if (cancellationToken.IsCancellationRequested)
{
break;
}
Stephen Toub has a good explanation on how the throwing of the OCE is more of an acknowledgement.
If the body of the task is also monitoring the cancellation token and throws an OperationCanceledException containing that token (which is what ThrowIfCancellationRequested does), then when the task sees that OCE, it checks whether the OCE's token matches the Task's token. If it does, that exception is viewed as an acknowledgement of cooperative cancellation and the Task transitions to the Canceled state (rather than the Faulted state).
Not sure what your objection to checking on every iteration is, but if you do not want to check on every iteration, for whatever reason, check the value of i:
E.g. this checks every 10th loop:
if (i % 10 == 0 && cancellationToken.IsCancellationRequested)
The reason you must check is so that you can decide where is best to stop the task so that work is not left in an inconsistent state. Here though all you will achieve by checking less frequently is a task that is slower to cancel, maybe that will lead to a less responsive UX. E.g. where the task would end in 500ms before, now it can take up to 10x that, 5 secs.
However if each loop was very fast, and checking the flag every loop proves to significantly increase the overall time the task takes, then checking every n loops makes sence.
I've just started working with tasks and I've come to some things I don't quite understand about calling methods within the task. I have started a new task like this:
var ts = new CancellationTokenSource();
var token = ts.Token;
Task.Run(() => Control(), token);
void Control()
{
while(!token.IsCancellationRequested)
{
token.ThrowIfCancellationRequested();
switch(ENUM)
{
case SOMETHING:
StartSomething();
break;
}
Task.Delay(50, token).wait();
}
}
Now I don't understand the behavior of StartSomething() once token has been cancelled. What if StartSomething() as well contains a while loop, can I as well use?
!token.IsCancellationRequested
and
token.ThrowIfCancellationRequested();
As well, if the Cancellation exception is being thrown inside that StartSomething() loop, will it instantly cancel task?
Yes, you can easily pass the same token onto StartSomething and exceptions from it will bubble up to Control and cancel the task. If you don't then it will keep running even if the CancellationTokenwas cancelled until it returns control toControl` that observes the token:
void StartSomething(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested(); // Will cancel the task.
// ...
}
}
Keep in mind though that token.ThrowIfCancellationRequested() will raise exception and the task will be canceled while !token.IsCancellationRequested will simply complete the task without marking it as canceled.
So here's the situation: I need to make a call to a web site that starts a search. This search continues for an unknown amount of time, and the only way I know if the search has finished is by periodically querying the website to see if there's a "Download Data" link somewhere on it (it uses some strange ajax call on a javascript timer to check the backend and update the page, I think).
So here's the trick: I have hundreds of items I need to search for, one at a time. So I have some code that looks a little bit like this:
var items = getItems();
Parallel.ForEach(items, item =>
{
startSearch(item);
var finished = isSearchFinished(item);
while(finished == false)
{
finished = isSearchFinished(item); //<--- How do I delay this action 30 Secs?
}
downloadData(item);
}
Now, obviously this isn't the real code, because there could be things that cause isSearchFinished to always be false.
Obvious infinite loop danger aside, how would I correctly keep isSearchFinished() from calling over and over and over, but instead call every, say, 30 seconds or 1 minute?
I know Thread.Sleep() isn't the right solution, and I think the solution might be accomplished by using Threading.Timer() but I'm not very familiar with it, and there are so many threading options that I'm just not sure which to use.
It's quite easy to implement with tasks and async/await, as noted by #KevinS in the comments:
async Task<ItemData> ProcessItemAsync(Item item)
{
while (true)
{
if (await isSearchFinishedAsync(item))
break;
await Task.Delay(30 * 1000);
}
return await downloadDataAsync(item);
}
// ...
var items = getItems();
var tasks = items.Select(i => ProcessItemAsync(i)).ToArray();
await Task.WhenAll(tasks);
var data = tasks.Select(t = > t.Result);
This way, you don't block ThreadPool threads in vain for what is mostly a bunch of I/O-bound network operations. If you're not familiar with async/await, the async-await tag wiki might be a good place to start.
I assume you can convert your synchronous methods isSearchFinished and downloadData to asynchronous versions using something like HttpClient for non-blocking HTTP request and returning a Task<>. If you are unable to do so, you still can simply wrap them with Task.Run, as await Task.Run(() => isSearchFinished(item)) and await Task.Run(() => downloadData(item)). Normally this is not recommended, but as you have hundreds of items, it sill would give you a much better level of concurrency than with Parallel.ForEach in this case, because you won't be blocking pool threads for 30s, thanks to asynchronous Task.Delay.
You can also write a generic function using TaskCompletionSource and Threading.Timer to return a Task that becomes complete once a specified retry function succeeds.
public static Task RetryAsync(Func<bool> retryFunc, TimeSpan retryInterval)
{
return RetryAsync(retryFunc, retryInterval, CancellationToken.None);
}
public static Task RetryAsync(Func<bool> retryFunc, TimeSpan retryInterval, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<object>();
cancellationToken.Register(() => tcs.TrySetCanceled());
var timer = new Timer((state) =>
{
var taskCompletionSource = (TaskCompletionSource<object>) state;
try
{
if (retryFunc())
{
taskCompletionSource.TrySetResult(null);
}
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
}, tcs, TimeSpan.FromMilliseconds(0), retryInterval);
// Once the task is complete, dispose of the timer so it doesn't keep firing. Also captures the timer
// in a closure so it does not get disposed.
tcs.Task.ContinueWith(t => timer.Dispose(),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
return tcs.Task;
}
You can then use RetryAsync like this:
var searchTasks = new List<Task>();
searchTasks.AddRange(items.Select(
downloadItem => RetryAsync( () => isSearchFinished(downloadItem), TimeSpan.FromSeconds(2)) // retry timout
.ContinueWith(t => downloadData(downloadItem),
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Default)));
await Task.WhenAll(searchTasks.ToArray());
The ContinueWith part specifies what you do once the task has completed successfully. In this case it will run your downloadData method on a thread pool thread because we specified TaskScheduler.Default and the continuation will only execute if the task ran to completion, i.e. it was not canceled and no exception was thrown.
I've discovered that TaskCompletionSource.SetResult(); invokes the code awaiting the task before returning. In my case that result in a deadlock.
This is a simplified version that is started in an ordinary Thread
void ReceiverRun()
while (true)
{
var msg = ReadNextMessage();
TaskCompletionSource<Response> task = requests[msg.RequestID];
if(msg.Error == null)
task.SetResult(msg);
else
task.SetException(new Exception(msg.Error));
}
}
The "async" part of the code looks something like this.
await SendAwaitResponse("first message");
SendAwaitResponse("second message").Wait();
The Wait is actually nested inside non-async calls.
The SendAwaitResponse(simplified)
public static Task<Response> SendAwaitResponse(string msg)
{
var t = new TaskCompletionSource<Response>();
requests.Add(GetID(msg), t);
stream.Write(msg);
return t.Task;
}
My assumption was that the second SendAwaitResponse would execute in a ThreadPool thread but it continues in the thread created for ReceiverRun.
Is there anyway to set the result of a task without continuing its awaited code?
The application is a console application.
I've discovered that TaskCompletionSource.SetResult(); invokes the code awaiting the task before returning. In my case that result in a deadlock.
Yes, I have a blog post documenting this (AFAIK it's not documented on MSDN). The deadlock happens because of two things:
There's a mixture of async and blocking code (i.e., an async method is calling Wait).
Task continuations are scheduled using TaskContinuationOptions.ExecuteSynchronously.
I recommend starting with the simplest possible solution: removing the first thing (1). I.e., don't mix async and Wait calls:
await SendAwaitResponse("first message");
SendAwaitResponse("second message").Wait();
Instead, use await consistently:
await SendAwaitResponse("first message");
await SendAwaitResponse("second message");
If you need to, you can Wait at an alternative point further up the call stack (not in an async method).
That's my most-recommended solution. However, if you want to try removing the second thing (2), you can do a couple of tricks: either wrap the SetResult in a Task.Run to force it onto a separate thread (my AsyncEx library has *WithBackgroundContinuations extension methods that do exactly this), or give your thread an actual context (such as my AsyncContext type) and specify ConfigureAwait(false), which will cause the continuation to ignore the ExecuteSynchronously flag.
But those solutions are much more complex than just separating the async and blocking code.
As a side note, take a look at TPL Dataflow; it sounds like you may find it useful.
As your app is a console app, it runs on the default synchronization context, where the await continuation callback will be called on the same thread the awaiting task has become completed on. If you want to switch threads after await SendAwaitResponse, you can do so with await Task.Yield():
await SendAwaitResponse("first message");
await Task.Yield();
// will be continued on a pool thread
// ...
SendAwaitResponse("second message").Wait(); // so no deadlock
You could further improve this by storing Thread.CurrentThread.ManagedThreadId inside Task.Result and comparing it to the current thread's id after the await. If you're still on the same thread, do await Task.Yield().
While I understand that SendAwaitResponse is a simplified version of your actual code, it's still completely synchronous inside (the way you showed it in your question). Why would you expect any thread switch in there?
Anyway, you probably should redesign your logic the way it doesn't make assumptions about what thread you are currently on. Avoid mixing await and Task.Wait() and make all of your code asynchronous. Usually, it's possible to stick with just one Wait() somewhere on the top level (e.g. inside Main).
[EDITED] Calling task.SetResult(msg) from ReceiverRun actually transfers the control flow to the point where you await on the task - without a thread switch, because of the default synchronization context's behavior. So, your code which does the actual message processing is taking over the ReceiverRun thread. Eventually, SendAwaitResponse("second message").Wait() is called on the same thread, causing the deadlock.
Below is a console app code, modeled after your sample. It uses await Task.Yield() inside ProcessAsync to schedule the continuation on a separate thread, so the control flow returns to ReceiverRun and there's no deadlock.
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication
{
class Program
{
class Worker
{
public struct Response
{
public string message;
public int threadId;
}
CancellationToken _token;
readonly ConcurrentQueue<string> _messages = new ConcurrentQueue<string>();
readonly ConcurrentDictionary<string, TaskCompletionSource<Response>> _requests = new ConcurrentDictionary<string, TaskCompletionSource<Response>>();
public Worker(CancellationToken token)
{
_token = token;
}
string ReadNextMessage()
{
// using Thread.Sleep(100) for test purposes here,
// should be using ManualResetEvent (or similar synchronization primitive),
// depending on how messages arrive
string message;
while (!_messages.TryDequeue(out message))
{
Thread.Sleep(100);
_token.ThrowIfCancellationRequested();
}
return message;
}
public void ReceiverRun()
{
LogThread("Enter ReceiverRun");
while (true)
{
var msg = ReadNextMessage();
LogThread("ReadNextMessage: " + msg);
var tcs = _requests[msg];
tcs.SetResult(new Response { message = msg, threadId = Thread.CurrentThread.ManagedThreadId });
_token.ThrowIfCancellationRequested(); // this is how we terminate the loop
}
}
Task<Response> SendAwaitResponse(string msg)
{
LogThread("SendAwaitResponse: " + msg);
var tcs = new TaskCompletionSource<Response>();
_requests.TryAdd(msg, tcs);
_messages.Enqueue(msg);
return tcs.Task;
}
public async Task ProcessAsync()
{
LogThread("Enter Worker.ProcessAsync");
var task1 = SendAwaitResponse("first message");
await task1;
LogThread("result1: " + task1.Result.message);
// avoid deadlock for task2.Wait() with Task.Yield()
// comment this out and task2.Wait() will dead-lock
if (task1.Result.threadId == Thread.CurrentThread.ManagedThreadId)
await Task.Yield();
var task2 = SendAwaitResponse("second message");
task2.Wait();
LogThread("result2: " + task2.Result.message);
var task3 = SendAwaitResponse("third message");
// still on the same thread as with result 2, no deadlock for task3.Wait()
task3.Wait();
LogThread("result3: " + task3.Result.message);
var task4 = SendAwaitResponse("fourth message");
await task4;
LogThread("result4: " + task4.Result.message);
// avoid deadlock for task5.Wait() with Task.Yield()
// comment this out and task5.Wait() will dead-lock
if (task4.Result.threadId == Thread.CurrentThread.ManagedThreadId)
await Task.Yield();
var task5 = SendAwaitResponse("fifth message");
task5.Wait();
LogThread("result5: " + task5.Result.message);
LogThread("Leave Worker.ProcessAsync");
}
public static void LogThread(string message)
{
Console.WriteLine("{0}, thread: {1}", message, Thread.CurrentThread.ManagedThreadId);
}
}
static void Main(string[] args)
{
Worker.LogThread("Enter Main");
var cts = new CancellationTokenSource(5000); // cancel after 5s
var worker = new Worker(cts.Token);
Task receiver = Task.Run(() => worker.ReceiverRun());
Task main = worker.ProcessAsync();
try
{
Task.WaitAll(main, receiver);
}
catch (Exception e)
{
Console.WriteLine("Exception: " + e.Message);
}
Worker.LogThread("Leave Main");
Console.ReadLine();
}
}
}
This is not much different from doing Task.Run(() => task.SetResult(msg)) inside ReceiverRun. The only advantage I can think of is that you have an explicit control over when to switch threads. This way, you can stay on the same thread for as long as possible (e.g., for task2, task3, task4, but you still need another thread switch after task4 to avoid a deadlock on task5.Wait()).
Both solutions would eventually make the thread pool grow, which is bad in terms of performance and scalability.
Now, if we replace task.Wait() with await task everywhere inside ProcessAsync in the above code, we will not have to use await Task.Yield and there still will be no deadlocks. However, the whole chain of await calls after the 1st await task1 inside ProcessAsync will actually be executed on the ReceiverRun thread. As long as we don't block this thread with other Wait()-style calls and don't do a lot of CPU-bound work as we're processing messages, this approach might work OK (asynchronous IO-bound await-style calls still should be OK, and they may actually trigger an implicit thread switch).
That said, I think you'd need a separate thread with a serializing synchronization context installed on it for processing messages (similar to WindowsFormsSynchronizationContext). That's where your asynchronous code containing awaits should run. You'd still need to avoid using Task.Wait on that thread. And if an individual message processing takes a lot of CPU-bound work, you should use Task.Run for such work. For async IO-bound calls, you could stay on the same thread.
You may want to look at ActionDispatcher/ActionDispatcherSynchronizationContext from #StephenCleary's
Nito Asynchronous Library for your asynchronous message processing logic. Hopefully, Stephen jumps in and provides a better answer.
"My assumption was that the second SendAwaitResponse would execute in a ThreadPool thread but it continues in the thread created for ReceiverRun."
It depends entirely on what you do within SendAwaitResponse. Asynchrony and concurrency are not the same thing.
Check out: C# 5 Async/Await - is it *concurrent*?
A little late to the party, but here's my solution which i think is added value.
I've been struggling with this also, i've solved it by capturing the SynchronizationContext on the method that is awaited.
It would look something like:
// just a default sync context
private readonly SynchronizationContext _defaultContext = new SynchronizationContext();
void ReceiverRun()
{
while (true) // <-- i would replace this with a cancellation token
{
var msg = ReadNextMessage();
TaskWithContext<TResult> task = requests[msg.RequestID];
// if it wasn't a winforms/wpf thread, it would be null
// we choose our default context (threadpool)
var context = task.Context ?? _defaultContext;
// execute it on the context which was captured where it was added. So it won't get completed on this thread.
context.Post(state =>
{
if (msg.Error == null)
task.TaskCompletionSource.SetResult(msg);
else
task.TaskCompletionSource.SetException(new Exception(msg.Error));
});
}
}
public static Task<Response> SendAwaitResponse(string msg)
{
// The key is here! Save the current synchronization context.
var t = new TaskWithContext<Response>(SynchronizationContext.Current);
requests.Add(GetID(msg), t);
stream.Write(msg);
return t.TaskCompletionSource.Task;
}
// class to hold a task and context
public class TaskWithContext<TResult>
{
public SynchronizationContext Context { get; }
public TaskCompletionSource<TResult> TaskCompletionSource { get; } = new TaskCompletionSource<Response>();
public TaskWithContext(SynchronizationContext context)
{
Context = context;
}
}