Since we expect to be reading frequently, and for us to often be reading when data is already available to be consumed, should SendLoopAsync return ValueTask rather than Task, so that we can make it allocation-free?
// Caller
_ = Task.Factory.StartNew(_ => SendLoopAsync(cancellationToken), TaskCreationOptions.LongRunning, cancellationToken);
// Method
private async ValueTask SendLoopAsync(CancellationToken cancellationToken)
{
while (await _outputChannel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false))
{
while (_outputChannel.Reader.TryRead(out var message))
{
using (await _mutex.LockAsync(cancellationToken).ConfigureAwait(false))
{
await _clientWebSocket.SendAsync(message.Data.AsMemory(), message.MessageType, true, cancellationToken).ConfigureAwait(false);
}
}
}
}
No, there is no value at the SendLoopAsync returning a ValueTask instead of Task. This method is invoked only once in your code. The impact of avoiding a single allocation of a tiny object is practically zero. You should consider using ValueTasks for asynchronous methods that are invoked repeatedly in loops, especially in hot paths. That's not the case in the example presented in the question.
As a side note, invoking asynchronous methods with the Task.Factory.StartNew+TaskCreationOptions.LongRunning combination is pointless. The new Thread that is going to be created, will have a very short life. It's gonna be terminated immediately when the code reaches the first await of an incomplete awaitable inside the async method. Also you are getting back a nested Task<Task>, which is tricky to handle. Using Task.Run is preferable. You can read here the reasons why.
Also be aware that the Nito.AsyncEx.AsyncLock class is not optimized for memory-efficiency. Lots of allocations are happening every time the lock is acquired. If you want a low-allocation synchronization primitive that can be acquired asynchronously, your best bet currently is probably to use a Channel<object> instance, initialized with a single null value: retrieve the value to enter, store it back to release.
The idiomatic way to use channels doesn't need locks, semaphores or Task.Factory.StartNew. The typical way to use a channel is to have a method that accepts just a ChannelReader as input. If the method wants to use a Channel as output, it should create it itself and only return a ChannelReader that can be passed to other methods. By owning the channel the method knows when it can be closed.
In the questions case, the code is simple enough though. A simple await foreach should be enough:
private async ValueTask SendLoopAsync(ChannelReader<Message> reader,
CancellationToken cancellationToken)
{
await foreach (var msg in reader.ReadAllAsync(cancellationToken))
{
await _clientWebSocket.SendAsync(message.Data.AsMemory(),
message.MessageType,
true, cancellationToken);
}
}
This method doesn't need an external Task.Run or Task.Factory.New to work. To run it, just call it and store its task somewhere, but not discarded:
public MyWorker(ChannelReader<Message> reader,CancellationToken token)
{
.....
_loopTask=SendLoopAsync(reader,token);
}
This way, once the input channel completes, the code can await for _loopTask to finish processing any pending messages.
Any blocking code should run inside it with Task.Run(), eg
private async ValueTask SendLoopAsync(ChannelReader<Message> reader,
CancellationToken cancellationToken)
{
await foreach (var msg in reader.ReadAllAsync(cancellationToken))
{
var new_msg=await Task.Run(()=>SomeHeavyWork(msg),cancellationToken);
await _clientWebSocket.SendAsync(message.Data.AsMemory(),
message.MessageType,
true, cancellationToken);
}
}
Concurrent Workers
This method could be used to start multiple concurrent workers too :
var tasks=Enumerable.Range(0,dop).Select(_=>SendLoopAsync(reader,token));
_loopTask=Task.WhenAll(tasks);
...
await _loopTask;
In .NET 6, Parallel.ForEachAsync can be used to process multiple messages with less code:
private async ValueTask SendLoopAsync(ChannelReader<Message> reader,
CancellationToken cancellationToken)
{
var options=new ParallelOptions {
CancellationToke=cancellationToken,
MaxDegreeOfParallellism=4
};
var input=reader.ReadAllAsync(cancellationToken);
await Parallel.ForEachAsync(input,options,async (msg,token)=>{
var new_msg=await Task.Run(()=>SomeHeavyWork(msg),token);
await _clientWebSocket.SendAsync(message.Data.AsMemory(),
message.MessageType,
true, token);
});
}
Idiomatic Channel Producers
Instead of using a class-level channel stored in a field, create the channel inside the producer method and only return its reader. This way the producer method has control of the channel's lifecycle and can close it when it's done. That's one of the reasons a Channel can only be accessed accessed only through its Reader and Writer classes.
A method can consume a ChannelReader and return another. This allows creating methods that can be chained together into a pipeline.
A simple producer can look like this:
ChannelReader<Message> Producer(CancellationToke token)
{
var channel=Channel.CreateUnbounded<Message>();
var writer=channel.Writer;
_ = Task.Run(()=>{
while(!token.IsCancellationRequested)
{
var msg=SomeHeavyJob();
await writer.SendAsync(msg);
},token)
.ContinueWith(t=>writer.TryComplete(t));
return channel.Reader;
}
When cancellation is signaled, the worker exits or an exception is thrown, the main task exists and ContinueWith calls TryComplete on the writer with any exception that may have been thrown. That's a simple non-blocking operation so it doesn't matter what thread it runs on.
A transforming method would look like this:
ChannelReader<Msg2> Transform(ChannelReader<Msg1> input,CancellationToke token)
{
var channel=Channel.CreateUnbounded<Msg2>();
var writer=channel.Writer;
_ = Task.Run(()=>{
await foreach(var msg1 in input.ReadAllAsync(token))
{
var msg2=SomeHeavyJob(msg1);
await writer.SendAsync(msg2);
},token)
.ContinueWith(t=>writer.TryComplete(t));
return channel.Reader;
}
Turning those methods into static extension methods would allow chaining them one after the other :
var task=Producer()
.Transformer()
.Consumer();
I don't set SingleWriter because this doesn't seem to be doing anything yet. Searching for this in the .NET runtime repo on Github doesn't show any results beyond test code.
Related
I find myself writing code like this a lot:
try
{
cancellationTokenSource.Cancel();
await task.ConfigureAwait(false); // this is the task that was cancelled
}
catch(OperationCanceledException)
{
// Cancellation expected and requested
}
Given that I requested the cancellation, it is expected and I'd really like the exception to be ignored. This seems like a common case.
Is there a more concise way to do this? Have I missed something about cancellation? It seems like there should be a task.CancellationExpected() method or something.
There is a built-in mechanism, the Task.WhenAny method used with a single argument, but it's not very intuitive.
Creates a task that will complete when any of the supplied tasks have completed.
await Task.WhenAny(task); // await the task ignoring exceptions
if (task.IsCanceled) return; // the task is completed at this point
var result = await task; // can throw if the task IsFaulted
It is not intuitive because the Task.WhenAny is normally used with at least two arguments. Also it is slightly inefficient because the method accepts a params Task<TResult>[] tasks argument, so on every invocation an array is allocated in the heap.
I don't think there is anything built-in, but you could capture your logic in extension methods (one for Task, one for Task<T>):
public static async Task IgnoreWhenCancelled(this Task task)
{
try
{
await task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
}
public static async Task<T> IgnoreWhenCancelled<T>(this Task<T> task)
{
try
{
return await task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return default;
}
}
Then you can write your code simpler:
await task.IgnoreWhenCancelled();
or
var result = await task.IgnoreWhenCancelled();
(You might still want to add .ConfigureAwait(false) depending on your synchronization needs.)
I assume whatever task is doing uses CancellationToken.ThrowIfCancellationRequested() to check for cancellation. That throws an exception by design.
So your options are limited. If task is an operation you wrote, you could make it not use ThrowIfCancellationRequested() and instead check IsCancellationRequested and end gracefully when needed. But as you know, the task's status won't be Canceled if you do that.
If it uses code you didn't write, then you don't have a choice. You'll have to catch the exception. You can use extension methods to avoid repeating code (Matt's answer), if you want. But you'll have to catch it somewhere.
The cancellation pattern available in C# in called cooperative cancellation.
This basically means that, in order to cancel any operation, there should be two actors which need to collaborate. One of them is the actor requesting the cancellation and the other is the actor listening to cancellation requests.
In order to implement this pattern you need an instance of CancellationTokenSource, which is an object that you can use in order to get an instance of CancellationToken. The cancellation is requested on the CancellationTokenSource instance and is propagated to the CancellationToken.
The following piece of code shows you this pattern in action and hopefully clarifies your doubt about cancellation:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp2
{
public static class Program
{
public static async Task Main(string[] args)
{
using (var cts = new CancellationTokenSource())
{
CancellationToken token = cts.Token;
// start the asyncronous operation
Task<string> getMessageTask = GetSecretMessage(token);
// request the cancellation of the operation
cts.Cancel();
try
{
string message = await getMessageTask.ConfigureAwait(false);
Console.WriteLine($"Operation completed successfully before cancellation took effect. The message is: {message}");
}
catch (OperationCanceledException)
{
Console.WriteLine("The operation has been canceled");
}
catch (Exception)
{
Console.WriteLine("The operation completed with an error before cancellation took effect");
throw;
}
}
}
static async Task<string> GetSecretMessage(CancellationToken cancellationToken)
{
// simulates asyncronous work. notice that this code is listening for cancellation
// requests
await Task.Delay(500, cancellationToken).ConfigureAwait(false);
return "I'm lost in the universe";
}
}
}
Pay attention to the comment and notice that all the 3 outputs for the program are possible.
There is no way to predict which of them will be the actual program result.
The point is that when you await for the task completion you don't know what actually is going to happen. The operation may succeeds or fails before the cancellation took effect, or maybe the cancellation request can be observed by the operation before it runs to completion or fails for an error. From the calling code point of view, all these outcomes are possible and you have no way to make a guess. You need to handle all cases.
So, basically, your code is correct and you are handling the cancellation the way you should.
This book is an excellent reference to learn these things.
My final solution was to create an extension method as suggested by Matt Johnson-Pint. However, I return a boolean indicating whether the task was canceled as shown in Vasil Oreshenski's answer.
public static async Task<bool> CompletionIsCanceledAsync(this Task task)
{
if (task.IsCanceled) return true;
try
{
await task.ConfigureAwait(false);
return false;
}
catch (OperationCanceledException)
{
return true;
}
}
This method has been fully unit tested. I picked the name to be similar to the WaitForCompletionStatus() method in the ParallelExtensionsExtras sample code and the IsCanceled property.
If you are expecting the task to be cancelled BEFORE the await you should check the state of the cancellation token source.
if (cancellationTokenSource.IsCancellationRequested == false)
{
await task;
}
EDIT: As mentioned in the comments this won't do any good if the task is cancelled while awaited.
EDIT 2: This approach is overkill because it acquires additional resource - in hot path this may have performance hit. (i am using SemaphoreSlim but you can use another sync. primitive with the same success)
This is an extension method over existing task. The extension method will return new task which holds information if the original task was cancelled.
public static async Task<bool> CancellationExpectedAsync(this Task task)
{
using (var ss = new SemaphoreSlim(0, 1))
{
var syncTask = ss.WaitAsync();
task.ContinueWith(_ => ss.Release());
await syncTask;
return task.IsCanceled;
}
}
Here is a simple usage:
var cancelled = await originalTask.CancellationExpectedAsync();
if (cancelled) {
// do something when cancelled
}
else {
// do something with the original task if need
// you can acccess originalTask.Result if you need
}
How it works:
Overall it waits for the original task to complete and returns information if was cancelled. The SemaphoraSlim is usually used to limit the access to some resource(expensive) but in this case i am using it to await until the original task has finished.
Notes:
It does not returns the original task. So if you need something that has been returned from it you should inspect the original task.
Is it possible to rewrite the code below using the async await syntax?
private void PullTablePages()
{
var taskList = new List<Task>();
var faultedList = new List<Task>();
foreach (var featureItem in featuresWithDataName)
{
var task = Task.Run(() => PullTablePage();
taskList.Add(task);
if(taskList.Count == Constants.THREADS)
{
var index = Task.WaitAny(taskList.ToArray());
taskList.Remove(taskList[index]);
}
}
if (taskList.Any())
{
Task.WaitAll(taskList.ToArray());
}
//todo: do something with faulted list
}
When I rewrote it as below, the code doesn't block and the console application finishes before most of the threads complete.
It seems like the await syntax doesn't block as I expected.
private async void PullTablePagesAsync()
{
var taskList = new List<Task>();
var faultedList = new List<Task>();
foreach (var featureItem in featuresWithDataName)
{
var task = Task.Run(() => PullTablePage();
taskList.Add(task);
if(taskList.Count == Constants.THREADS)
{
var anyFinished = await Task.WhenAny(taskList.ToArray());
await anyFinished;
for (var index = taskList.Count - 1; index >= 0; index--)
{
if (taskList[index].IsCompleted)
{
taskList.Remove(taskList[index]);
}
}
}
}
if (taskList.Any())
{
await Task.WhenAll(taskList);
}
//todo: what to do with faulted list?
}
Is it possible to do so?
WaitAll doesn't seem to wait for all tasks to complete. How do I get it to do so? The return type says that it returns a task, but can't seem to figure out the syntax.## Heading ##
New to multithreading, please excuse ignorance.
Is it possible to rewrite the code below using the async await syntax?
Yes and no. Yes, because you already did it. No, because while being equivalent from the function implementation standpoint, it's not equivalent from the function caller perspective (as you already experienced with your console application). Contrary to many other C# keywords, await is not a syntax sugar. There is a reason why the compiler forces you to mark your function with async in order to enable await construct, and the reason is that now your function is no more blocking and that puts additional responsibility to the callers - either put themselves to be non blocking (async) or use a blocking calls to your function. Every async method in fact is and should return Task or Task<TResult> and then the compiler will warn the callers that ignore that fact. The only exception is async void which is supposed to be used only for event handlers, which by nature should not care what the object being notified is doing.
Shortly, async/await is not for rewriting synchronous (blocking code), but for easy turning it to asynchronous (non blocking). If your function is supposed to be synchronous, then just keep it the way it is (and your original implementation is perfectly doing that). But if you need asynchronous version, then you should change the signature to
private async Task PullTablePagesAsync()
with the await rewritten body (which you already did correctly). And for backward compatibility provide the old synchronous version using the new implementation like this
private void PullTablePages() { PullTablePagesAsync().Wait(); }
It seems like the await syntax doesn't block as I expected.
You're expecting the wrong thing.
The await syntax should never block - it's just that the execution flow should not continue until the task is finished.
Usually you are using async/await in methods that return a task. In your case you're using it in a method with a return type void.
It takes times to get your head around async void methods, that's why their use is usually discouraged. Async void methods run synchronously (block) to the calling method until the first (not completed) task is awaited. What happens after depends on the execution context (usually you're running on the pool). What's important: The calling method (the one that calls PullTablePAgesAsync) does not know of continuations and can't know when all code in PullTablePagesAsync is done.
Maybe take a look on the async/await best practices on MSDN
How could I use DataflowBlockOptions.CancellationToken?
If I create instance of BufferBlock like this:
var queue = new BufferBlock<int>(new DataflowBlockOptions { BoundedCapacity = 5, CancellationToken = _cts.Token });
then having consumer/producer methods that use queue, how can I use its CancellationToken to handle cancellation?
E.g. in producer method, how can I check the cancellation token - I haven't found any property to access the token..
EDIT:
Sample of produce/consume methods:
private static async Task Produce(BufferBlock<int> queue, IEnumerable<int> values)
{
foreach (var value in values)
{
await queue.SendAsync(value);
}
queue.Complete();
}
private static async Task<IEnumerable<int>> Consume(BufferBlock<int> queue)
{
var ret = new List<int>();
while (await queue.OutputAvailableAsync())
{
ret.Add(await queue.ReceiveAsync());
}
return ret;
}
Code to call it:
var queue = new BufferBlock<int>(new DataflowBlockOptions { BoundedCapacity = 5, CancellationToken = _cts.Token });
// Start the producer and consumer.
var values = Enumerable.Range(0, 10);
Produce(queue, values);
var consumer = Consume(queue);
// Wait for everything to complete.
await Task.WhenAll(consumer, queue.Completion);
EDIT2:
If I call _cts.Cancel(), the Produce method does not cancel and finishes without interruption.
If you want to cancel produce process you should pass token in it, like this:
private static async Task Produce(
BufferBlock<int> queue,
IEnumerable<int> values,
CancellationToken token
)
{
foreach (var value in values)
{
await queue.SendAsync(value, token);
Console.WriteLine(value);
}
queue.Complete();
}
private static async Task<IEnumerable<int>> Consume(BufferBlock<int> queue)
{
var ret = new List<int>();
while (await queue.OutputAvailableAsync())
{
ret.Add(await queue.ReceiveAsync());
}
return ret;
}
static void Main(string[] args)
{
var cts = new CancellationTokenSource();
var queue = new BufferBlock<int>(new DataflowBlockOptions { BoundedCapacity = 5, CancellationToken = cts.Token });
// Start the producer and consumer.
var values = Enumerable.Range(0, 100);
Produce(queue, values, cts.Token);
var consumer = Consume(queue);
cts.Cancel();
try
{
Task.WaitAll(consumer, queue.Completion);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
foreach (var i in consumer.Result)
{
Console.WriteLine(i);
}
Console.ReadKey();
Normally you use the CancellationToken option in order to control the cancellation of a dataflow block, using an external CancellationTokenSource. Canceling the block (assuming that its a TransformBlock) has the following immediate effects:
The block stops accepting incoming messages. Invoking its Post returns false, meaning that the offered message is rejected.
The messages that are currently stored in the block's internal input buffer are immediately discarded. These messages are lost. They will not be processed or propagated.
If the block is not currently processing any messages, the following effects will also follow immediately. Otherwise they will follow when the processing of all currently processed messages is completed:
All the processed messages that are currently stored in this block's output buffer are discarded. The last processed messages (the messages that were in the middle of processing when the cancellation occurred) will not be propagated to linked blocks downstream.
Any pending asynchronous SendAsync operations targeting the block, that were in-flight when the cancellation occurred, will complete with a result of false (meaning "non accepted").
The Task that represents the Completion of the block transitions to the Canceled state. In other words this task's IsCanceled property becomes true.
You can achieve all but the last effect directly, without using the CancellationToken option, by invoking the block's Fault method. This method is accessible through the IDataflowBlock interface that all blocks implement. You can use it like this:
((IDataflowBlock)block).Fault(new OperationCanceledException());
The difference is that the Completion task will now become Faulted instead of Canceled. This difference may or may not be important, depending on the situation. If you just await the Completion, which is how this property is normally used, in both cases a OperationCanceledException will be thrown. So if you don't need to do anything fancy with the Completion property, and you also want to avoid configuring the CancellationToken for some reason, you could consider this trick as an option.
Update: Behavior when the cancellation occurs after the Complete method has been invoked, in other words when the block is already in its completion phase, but has not completed yet:
If the block is a processing block, like a TransformBlock, all of the above will happen just the same. The block will transition soon to the Canceled state.
If the block is a non-processing block, like a BufferBlock<T>, the (3) from the list above will not happen. The output buffer of a BufferBlock<T> is not emptied, when the cancellation happen after the invocation of the Complete method. See this GitHib issue for a demonstration of this behavior. Please take into consideration that the Complete method may be invoked not only manually, but also automatically, if the block has been linked as the target of a source block, with the PropagateCompletion configuration enabled. You may want to check out this question, to understand fully the implications of this behavior. Long story short, canceling all the blocks of a dataflow pipeline that contains a BufferBlock<T>, does not guarantee that the pipeline will terminate.
Side note: When both the Complete and Fault methods are invoked, whatever was invoked first prevails regarding the final status of the block. If the Complete was invoked first, the block will complete with status RanToCompletion. If the Fault was invoked first, the block will complete with status Faulted. Faulting a Completed block has still an effect though: it empties its internal input buffer.
I have a blocking operation that reads from a queue, but it can take a timeout. I can easily convert this to an "async" operation:
public async Task<IMessage> ReceiveAsync(CancellationToken cancellationToken)
{
return await Task.Run(() =>
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
// Try receiving for one second
IMessage message = consumer.Receive(TimeSpan.FromSeconds(1));
if (message != null)
{
return message;
}
}
}, cancellationToken).ConfigureAwait(false);
}
Aborting a thread is generally considered bad practice since you can leak resources, so the timeout seems like the only way to cleanly stop a thread. So I have three questions:
What is a generally accepted timeout value for "immediate" cancellation?
For libraries that provide built-in async methods, does immediate cancellation truly exist or do they also use timeouts and loops to simulate it? Maybe the question here is how would you make use of software interrupts and if these also have to do some sort of polling to check if there are interrupts, even if it's at the kernel/CPU level.
Is there some alternate way I should be approaching this?
Edit: So I may have found part of my answer with Thread.Interrupt() and then handling ThreadInterruptedException. Is this basically a kernel-level software interrupt and as close to "immediate" as we can get? Would the following be a better way of handling this?
public async Task<IMessage> ReceiveAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var completionSource = new TaskCompletionSource<IMessage>();
var receiverThread = new Thread(() =>
{
try
{
completionSource.SetResult(consumer.Receive());
}
catch (ThreadInterruptedException)
{
completionSource.SetCanceled();
}
catch (Exception ex)
{
completionSource.SetException(ex);
}
});
cancellationToken.Register(receiverThread.Interrupt);
receiverThread.Name = "Queue Receive";
receiverThread.Start();
return await completionSource.Task.ConfigureAwait(false);
}
It depends on your specific needs. A second could be immediate for some and slow for others.
Libraries (good ones) which provide async API do so from the bottom up. They usually don't wrap blocking (synchronous) operations with a thread to make them seem asynchronous. They use TaskCompletionSource to create truly async methods.
I'm not sure what you mean by queue (the built-in Queue in .Net doesn't have a Receive method) but you should probably be using a truly async data structure like TPL Dataflow's BufferBlock.
About your specific code sample.
You are holding up a thread throughout the entire operation (that's async over sync) which is costly. You could instead try to consume quickly and then wait asynchronously for the timeout to end, or for the CancellationToken to be cancelled.
There's also no point in using another thread with Task.Run. You can simply have the async lambda be the content of ReceiveAsync:
public async Task<IMessage> ReceiveAsync(CancellationToken cancellationToken)
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
// Try receiving for one second
IMessage message;
if (!consumer.TryReceive(out message))
{
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
if (message != null)
{
return message;
}
}
}
If your queue implements IDisposable a different (harsher) option would be to call Dispose on it when the CancellationToken is cancelled. Here's how.
I'm trying to create an async ProducerConsumerCollection and for that, I'm using this msdn page (http://msdn.microsoft.com/en-us/library/hh873173.aspx (bottom of the page)).
I'm now trying to add a timeout, here is what I do :
public async Task<T> TakeWithTimeout(int timeout)
{
Task<T> takeTask = this.Take();
if (timeout <= 0 || takeTask == await Task.WhenAny(this.tasks.Take(), Task.Delay(timeout)))
{
return await takeTask;
}
else
{
// Timeout
return default(T);
}
}
}
The problem with this code is that, in case of timeout, it does not cancel the task created by the Take() method.
Since this task has been "created" by the TaskCompletionSource, I cannot give it a cancellationToken?
So, how to proceed to cancel it and properly implement this Take with timeout ?
Thanks :)
Writing a cancel-safe async-friendly producer/consumer collection is non-trivial. What you need to do is change Take to accept a CancellationToken as a parameter, and it should register a handler so that when it is cancelled the TaskCompletionSource is cancelled.
I highly recommend you use BufferBlock<T>, which has cancellation support built-in.
If you can't use TPL Dataflow (e.g., you're working in a PCL or have target platforms unsupported by Dataflow), then you can use the producer/consumer collections in my open-source AsyncEx library (such as AsyncProducerConsumerQueue or AsyncCollection). These are both based on AsyncLock and AsyncConditionVariable, a design I describe briefly on my blog (which does not get into the cancellation details). The key behind supporting cancellation in a producer/consumer collection with this design is to support cancellation in AsyncConditionVariable.WaitAsync; once your condition variable type supports cancellation, then your collection will easily support it, too.
I'm just going to post my solution to the question How to cancel a task from a TaskCompletionSource because that is what I needed myself.
I'm guessing this could be used for your specific need, but it's not tied to a specific timeout functionality, so this is a general solution (or so I hope).
This is an extension method:
public static async Task WaitAsync<T>(this TaskCompletionSource<T> tcs, CancellationToken ctok)
{
CancellationTokenSource cts = null;
CancellationTokenSource linkedCts = null;
try {
cts = new CancellationTokenSource();
linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, ctok);
var exitTok = linkedCts.Token;
Func<Task> listenForCancelTaskFnc = async () => {
await Task.Delay(-1, exitTok).ConfigureAwait(false);
};
var cancelTask = listenForCancelTaskFnc();
await Task.WhenAny(new Task[] { tcs.Task, cancelTask }).ConfigureAwait(false);
cts.Cancel();
} finally {
if(linkedCts != null) linkedCts.Dispose();
}
}
Usage:
async Task TestAsync(CancellationToken ctok) {
var tcs = new TaskCompletionSource<bool>();
if (somethingOrTheOther) {
tcs.TrySetResult(true);
}
await tcs.WaitAsync(ctok);
}
The idea is to have a supervisory async Task waiting essentially forever until it is cancelled, which we can use to 'exit early' in case the TaskCompletionSource is not yet satisfied, but we need to exit anyhow due to a cancel request.
The supervisory Task is guaranteed to be cancelled at the end of WaitAsync regardless how it falls out of the WhenAny. Either the TaskCompletionSource is satisfied with a result, and WhenAny completes, briefly leaving the supervisory sleeper task in tact until the next line where cts.Cancel() is called, or it was cancelled with the exitToken, which is a combined token of the passed in ctok or the internal one cts.Token.
Anyhow, I hope this makes sense -- please let me know if this code has any problems...