Call C# Task Repeatedly on conclusion - c#

I have a C# app that must run blocks of code in parallel. Here is the basic structure of two of those blocks of code. In reality, there will be many more.
private async Task MyFirstTask()
{
// do stuff
await FirstTaskImplementation();
// cleanup
}
private async Task MySecondTask()
{
// do stuff
await SecondTaskImplementation();
// cleanup
}
Some of these blocks of code will run on a timer. Some will run repeatedly. In an attempt to accomplish, I have the following:
Task.Run(() => MyFirstTask());
Task.Run(() => MySecondTask());
When MyFirstTask has completed, I want to run it again. In fact, I want to run it over-and-over again until the program stops. Yet, I want MySecondTask to run in parallel of MyFirstTask. My question is, how do I execute MyFirstTask repeatedly, while still being parallel to MySecondTask?
I reviewed several of the related SO questions. I also do not see a Complete kind of event handler. So, I'm kind of lost in terms of how to implement this. I appreciate your help!

The beauty of async/await is that you can write asynchronous code in much the same way you'd write synchronous code. How would you repeat a synchronous operation? You could use a loop. You could do the same here, e.g.:
private async Task MyFirstTask(CancellationToken token) {
while (!token.IsCancellationRequested) {
// do stuff
await FirstTaskImplementation();
// cleanup
}
}
You can embed the loop in your current method or lift it into a wrapper method, but it should work either way.
You can continue scheduling your tasks the same way you're doing it now, though you really should await your async methods:
CancellationTokenSource cts = new CancellationTokenSource();
Task.Run(async () => await MyFirstTask(cts.Token));
Task.Run(async () => await MySecondTask());
// Request cancellation via `cts` when you want the looping to end.
And although you didn't ask about it, if you wanted to insert a delay between each iteration, you could simply place an await Task.Delay(...) statement at the end of the loop body.

You don't necessarily need to use Task.Run for the first task. You can use a Timer for the first task, with AutoReset set to true. That way it'll run forever and you don't have to worry about it anymore.
private static System.Timers.Timer aTimer;
public static void Main()
{
SetTimer(); //MyFirstTask starts running over and over in another thread
Task.Run(() => MySecondTask());
}
private static void SetTimer()
{
// Create a timer with a 1ms interval (has to be > 0)
aTimer = new System.Timers.Timer(1);
// Hook up the Elapsed event for the timer.
aTimer.Elapsed += MyFirstTask;
aTimer.AutoReset = true;
aTimer.Enabled = true;
}
private static async void MyFirstTask(Object source, ElapsedEventArgs e)
{
// do stuff
await FirstTaskImplementation();
// cleanup
}
private async Task MySecondTask()
{
// do stuff
await SecondTaskImplementation();
// cleanup
}

Another approach would be something like this:
var first=MyFirstTask().ToObservable();
var second=MySecondTask().ToObservable();
first.Repeat().Merge(second).Subscribe(x=>Console.WriteLine("Task completing."));
That's illustrative, not tested code. If you expect MySecondTask to complete, then perhaps this:
first.Repeat().TakeUntil(second).Subscribe(x=>Console.WriteLine("Task completing."));
If you want to add timeouts to second you could do this:
first.Repeat().TakeUntil(second.Timeout(TimeSpan.FromMilliseconds(100))).Subscribe(...)
If you want to show something on each task completion, declare the observables as:
var first=MyFirstTask().ToObservable().Do(Console.WriteLine("First completing"));
The above requires System.Reactive.Linq namespaces. I find these Rx based solutions to concurrency generally more elegant than the TPL, but that's just subjective.
Finally, if you do not want to start the tasks until the subscription is called, or something is ready to start watching, you can use Observable.FromAsync , as per info here

Related

.net MAUI c# Background Task ContinueWith and notification Event

[EDIT]Solved, see below[/EDIT]
this is a newbie-question.
I'm just digging in to c# and async and whyt i would like to have:
click Button
run several tasks in order but in background-thread, one after another
running tasks should notifiy their progress if possible
right now i can click the botton and start the task-chain, but within the completition event i would like (for testing) show a message-box every time a task has finished. this may lead to a crash (?) and i don't know why since i thought i would be within the ui-thread ...
here are some parts of the code:
AppViewModel:
void handlePhaseCompletedEvent(object sender, SyncPhaseCompletedEventArgs e)
{
Shell.Current.DisplayAlert("TEST", "PHASE " + e.phase.ToString(), "OK"); // <<<< doesn't show up, maybe because its crashing a short time after?
syncToolService.StartSyncPhaseAsync(e.phase + 1, this); // <<<< seems to crash here?
}
[RelayCommand]
async Task StartSyncAsync()
{
syncToolService.NotifySyncPhaseCompleted += handlePhaseCompletedEvent;
syncToolService.StartSyncPhaseAsync(0, this);
}
syncToolService:
public event EventHandler<SyncPhaseCompletedEventArgs> NotifySyncPhaseCompleted;
public async Task StartSyncPhaseAsync(int phase, AppViewModel viewModel)
{
// search for Remote-peer
if (phase == 0)
{
Task t = new Task(() => Task.Delay(100)); // dummy, not implemented yet
t.ConfigureAwait(false);
t.ContinueWith(t => NotifySyncPhaseCompleted?.Invoke(this, new SyncPhaseCompletedEventArgs { phase = phase }));
t.Start();
return;
}
// Remote Sync start preparations
if (phase == 1)
{
Task t = new Task(() => Task.Delay(100)); // dummy, not implemented yet
t.ConfigureAwait(false);
t.ContinueWith(t => NotifySyncPhaseCompleted?.Invoke(this, new SyncPhaseCompletedEventArgs { phase = phase }));
t.Start();
return;
}
//////// LOCAL PREPARATIONS
// read local files
if (phase == 2)
{
Task t = new Task(() => BPMSyncToolService.loadLocalData(viewModel.DataFiles));
t.ConfigureAwait(false);
t.ContinueWith(t => NotifySyncPhaseCompleted?.Invoke(this, new SyncPhaseCompletedEventArgs { phase = phase }));
t.Start();
return;
}
}
basicly i thought StartSyncPhaseAsync would run a Task (and it seems to do so)
and it also seems to trigger the event (whicht seems not to raise the exeption)
when running line by line in debug it crashes after syncToolService.StartSyncPhaseAsync(e.phase + 1, this);
with this stack:
> [Exception] WinRT.Runtime.dll!WinRT.ExceptionHelpers.ThrowExceptionForHR.__Throw|20_0(int hr)
[Exception] Microsoft.WinUI.dll!Microsoft.UI.Xaml.Controls.ContentDialog._IContentDialogFactory.CreateInstance(object baseInterface, out System.IntPtr innerInterface)
[Exception] Microsoft.WinUI.dll!Microsoft.UI.Xaml.Controls.ContentDialog.ContentDialog()
[Exception] Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Platform.AlertManager.AlertRequestHelper.OnAlertRequested(Microsoft.Maui.Controls.Page sender, Microsoft.Maui.Controls.Internals.AlertArguments arguments)
System.Private.CoreLib.dll!System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
System.Private.CoreLib.dll!System.Threading.Tasks.Task.ThrowAsync.AnonymousMethod__128_1(object state)
System.Private.CoreLib.dll!System.Threading.QueueUserWorkItemCallbackDefaultContext.Execute()
System.Private.CoreLib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib.dll!System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
i also may have a general problem in my design, any help would be great!
[UPDATE]
it runs now as expected.
newbie-thoughts:
the answer from ToolmakerSteve https://stackoverflow.com/a/73409415/4232410 i thought "hey, but thats what i tried first and it locked UI". Then i've watched https://www.youtube.com/watch?v=2moh18sh5p4&t=0s and https://www.youtube.com/watch?v=ZTKGRJy5P2M and i saw "hey, its basicly what was mentioned and there it works, so where is my fault (now as i'm writing this i saw his update, thanks alot!)
Ryan mentioned "ReportProgress" (that was the way i stumbled across the above videos), and it worked, also thank you!
so this is basicly the actual working code that seems NOT to lock the UI and doesn't crash (the crash was because of Microsoft.VisualBasic.FileIO.TextFieldParser that tried to read a line and found a field beginning with a quote and thought it would be an enclosing quote which it wasn't)
AppViewModel:
private void HandleSyncProgressChanged(object sender, SyncStatus e)
{
NumFilesProcessed = e.filesProcessed;
NumFilesNotFound = e.filesNotFound;
AktueleAufgabe = e.workingPhase;
}
[RelayCommand]
async Task StartSyncAsync()
{
Progress<SyncStatus> progress=new Progress<SyncStatus>();
progress.ProgressChanged += HandleSyncProgressChanged;
await BPMSyncToolService.StartSyncPhaseAsync(this, progress);
}
syncToolService:
public static async Task StartSyncPhaseAsync(AppViewModel viewModel, IProgress<SyncStatus> progress)
{
SyncStatus report = new SyncStatus();
report.workingPhase = "Suche Synchronisationspartner";
progress.Report(report);
// search for Remote-peer
await Task.Delay(100); // dummy, not implemented yet
report.workingPhase = "Starte Vorbereitungen beim Synchronisationspartner";
progress.Report(report);
// Remote Sync start preparations
await Task.Delay(100); // dummy, not implemented yet
//////// LOCAL PREPARATIONS
report.workingPhase = "lese lokale Dateien";
progress.Report(report);
// read local files
await BPMSyncToolService.LoadLocalDataAsync(viewModel.DataFiles, progress, report);
// [...]
}
what i actually can't see is the counting up of processed files, maybe it's too fast, don't know, will see in further tasks that will require more time
anyways, thanks, both answers helped, i will mark the one as solution, that was closer to the core problem (i think)
Given async/await, it is almost never necessary to use task continuations or ConfigureAwait.
To start a sequence in the background, wrap the sequence in Task.Run.
To report progress on UI thread, use Dispatcher.Dispatch.
Example:
// IMPORTANT: `await`.
// Otherwise, current method would continue before Task.Run completes.
await Task.Run(async () =>
{
// Now on background thread.
...
// Report progress to UI.
Dispatcher.Dispatch(() =>
{
// Code here is queued to run on MainThread.
// Assuming you don't need to wait for the result,
// don't need await/async here.
}
// Still on background thread.
...
};
// This is effectively the "continuation": Code here runs after Task.Run completes.
...
UPDATE
In response to comment, this is how you use async/await to start a sequence of tasks, without waiting for the result:
If your top-level code does UI calls:
// This queues an independent execution to MainThread.
// We don't "await" the Dispatch, because we want it to run independently.
Dispatcher.Dispatch(async () => await TopMethod());
If your top-level code does not do UI calls:
// This queues an independent execution to the Thread Pool.
// We don't "await" the Run, because we want it to run independently.
Task.Run(async () => await TopMethod());
In either case, instead of using continuations, TopMethod uses awaits to sequence the tasks:
async void TopMethod()
{
await ..Task1..;
await ..Task2..;
await ..Task3..;
}
This is equivalent to Task1.ContinueWith(Task2.ContinueWith(Task3));
(Off the top of my head; I may not have the syntax quite right on this.)
If you are on a background thread (did Task.Run), then to do UI calls, simply wrap in Dispatcher.Dispatch( ... ). As shown in first code snippet.
You can capture SynchronizationContext in your syncToolService in constructor, or by defining explicitly API for capturing, kinda:
public void CaptureSynchronizationContext(SynchronizationContext context)
{
var current = SynchronizationContext.Current;
if (context is null)
{
this.capturedScheduler = TaskScheduler.Current;
return;
}
SynchronizationContext.SetSynchronizationContext(context);
this.capturedScheduler = TaskScheduler.FromCurrentSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(current);
}
Add make some wrapper for your logic to be called in specified context:
private void RunTaskWithContinuation(Task task, Action<Task> continuation)
{
task.ConfigureAwait(false);
task.ContinueWith(t => continuation(t), capturedScheduler);
task.Start();
}
So, somewhere in your UI:
// afaik you should call it once per every Window
syncToolService.CaptureSynchronizationContext(SynchronizationContext.Current);
And your code above would look like this:
// read local files
if (phase == 2)
{
Task t = new Task(() => BPMSyncToolService.loadLocalData(viewModel.DataFiles));
RunTaskWithContinuation(t, () => NotifySyncPhaseCompleted?.Invoke(this, new SyncPhaseCompletedEventArgs { phase = phase }));
}
Not tested, but i would try this idea first.
Btw, if SynchronizationContext is null, guess your problem would be persisted.
There is space for refactoring, just wanted to show the idea.
UPDATE
There is ReportProgress type - right tool for reports in multithreaded environment. May be this is what you are looking for.
But it works the same way, as i did above - via context capturing.

Should I be using await inside my Task.Run()?

** I've summarised this question at the bottom with an edit **
This has been asked before but I think my circumstances are different.
I am processing multiple requests simultaneously.
This is my code to do that, it runs in a loop. I've removed a bit of code that handles the taskAllocated variable for brevity.
while (!taskAllocated)
{
lock (_lock)
{
// Find an empty slot in the task queue to insert this task
for (i = 0; i < MaxNumTasks; i++)
{
if (_taskQueue[i] == null)
{
_taskQueue[i] = Task.Run(() => Process());
_taskQueue[i].ContinueWith(ProcessCompleted);
break;
}
}
}
}
Process is a typical async Task Process() { CpuIntensiveStuff(); } method.
I've been running the above code, and it has been working fine. It multithreads nicely. Whenever an item comes in, it will find an empty slot in the task queue, and kick it off. When the task completes, the ProcessCompleted method runs, and frees up the slot.
But then I thought, shouldn't I be using await inside my Task.Run? Something like:
_taskQueue[i] = Task.Run(async () => await Process());
After thinking about it, I'm not sure. ContinueWith triggers correctly, when the task has completed, so perhaps it's not necessary.
I ask because I wanted to monitor and log how long each task takes to complete.
So Instead of Process(), I would make another method like:
async Task DoProcess()
{
var sw = Stopwatch.StartNew();
Process();
sw.Stop();
Log(sw.ElapsedMilliseconds);
}
And it occurred to me that if I did that, I wasn't sure if I'd need to await Process(); or not, in addition to not knowing if I should await inside the Task.Run()
I'm in a bit of a tizz about this. Can anyone offer guidance?
Edit:
To summarise:
If Somemethod is:
void SomeMethod() { }
Then
Task.Run(() => SomeMethod()); is great, calls SomeMethod on a new 'thread' (not technically, but you know what I mean).
However, my SomeMethod is actually:
async Task SomeMethod() { }
Do you need to do anything special with Task.Run()?
My code, I am not, I am just straight up ignoring that it's an async Task, and that seems to work:
Task.Run(() => SomeMethod()); // SomeMethod is async Task but I am ignoring that
But I'm not convinced that it a) should work or b) is a good idea. The alternative could be to do:
Task.Run(async() => await SomeMethod());
But is there any point? And this is compounded by the fact I want to really do:
Task.Run(() =>
{
someCode();
var x = startTimer();
SomeMethod();
var y = stopTimer();
someMoreCode()
});
but without await I'm not sure it will wait for somemethod to finish and the timer will be wrong.
Things become more clear if you do not use anonymous methods. For example,
Task.Run(() => Process())
is equivalent to this:
Task.Run(DoSomething);
Task DoSomething() {
return Process();
}
Whereas
Task.Run(async () => await Process())
is equivalent to this:
Task.Run(DoSomething);
async Task DoSomething() {
await Process();
}
In most cases, there is no functional difference between return SomethingThatReturnsATask() and return await SomethingThatReturnsATask(), and you usually want to return the Task directly and not use await (for reasons described here). When used inside Task.Run, things could easily go bad if the .NET team didn't have your back.
It is important to note that asynchronous methods start running on the same thread just like any other method. The magic happens at the first await that acts on an incomplete Task. At that point, await returns its own incomplete Task. That's important - it returns, with a promise to do the rest later.
This could have meant that the Task returned from Task.Run would complete whenever Process() returns a Task. And since Process() returns a Task at the first await, that would happen when it has not yet totally completed.
The .NET team has your back
That is not the case however, because Task.Run has a specific overload for when you give it a method returning a Task. And if you look at the code, it returns a Task *that is tied to the Task you return.
That means that the Task returned from Task.Run(() => Process()) will not complete until the Task returned from Process() has completed.
So your code is fine the way it is.

Task.ContinueWith seems to be called more often than there are actual tasks

First, this is from something much bigger and yes, I could completely avoid this using await under normal/other circumstances. For anyone interested, I'll explain below.
To track how many tasks I still have left before my program continues, I've built the following:
A counter:
private static int counter;
Some method:
public static void Test()
{
List<Task> tasks = new List<Task>();
for (int i = 0; i < 10000; i++)
{
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
var task = DoTaskWork();
task.ContinueWith(t => // After DoTaskWork
{
// [...] Use t's Result
counter--; // Decrease counter
tcs.SetResult(null); // Finish the task that the UI or whatever is waiting for
});
tasks.Add(tcs.Task); // Store tasks to wait for
}
Task.WaitAll(tasks.ToArray()); // Wait for all tasks that actually only finish in the ContinueWith
Console.WriteLine(counter);
}
My super heavy work to do:
private static Task DoTaskWork()
{
counter++; // Increase counter
return Task.Delay(500);
}
Now, interestingly I do not receive the number 0 at the end when looking at counter. Instead, the number varies with each execution. Why is this? I tried various tests, but cannot find the reason for the irregularity. With the TaskCompletionSource I believed this to be reliable. Thanks.
Now, for anyone that is interested in why I do this:
I need to create loads of tasks without starting them. For this I need to use the Task constructor (one of its rare use cases). Its disadvantage to Task.Run() is that it cannot handle anything with await and that it needs a return type from the Task to properly run (hence the null as result). Therefore, I need a way around that. Other ideas welcome...
Well. I am stupid. Just 5 minutes in, I realize that.
I just did the same while locking a helper object before changing the counter in any way and now it works...
private static int counter;
private static object locker = new object();
// [...]
task.ContinueWith(t =>
{
lock(locker)
counter--;
tcs.SetResult(null);
});
// [...]
private static Task DoTaskWork()
{
lock (locker)
counter++;
return Task.Delay(500);
}
I need to create loads of tasks without starting them ... Therefore, I need a way around that. Other ideas welcome...
So, if I read it correct you want to build a list of tasks without actually run them on creation. You could do that by building a list of Func<Task> objects you invoke when required:
async Task Main()
{
// Create list of work to do later
var tasks = new List<Func<Task>>();
// Schedule some work
tasks.Add(() => DoTaskWork(1));
tasks.Add(() => DoTaskWork(2));
// Wait for user input before doing work to demonstrate they are not started right away
Console.ReadLine();
// Execute and wait for the completion of the work to be done
await Task.WhenAll(tasks.Select(t => t.Invoke()));
Console.WriteLine("Ready");
}
public async Task DoTaskWork(int taskNr)
{
await Task.Delay(100);
Console.WriteLine(taskNr);
}
This will work, even if you use Task.Run like this:
public Task DoTaskWork(int taskNr)
{
return Task.Run(() =>
{
Thread.Sleep(100); Console.WriteLine(taskNr);
});
}
It this is not want you want can you elaborate more about the tasks you want to create?

Running several infinite loops with async/await

I am developing android messanger app based on xamarin and .net 5 async/awaits.
In my app i have producer/consumer pattern for processing messages which is made on infinite loops.
for example ReadTcpClientAsync producer:
async Task ReadTcpClientAsync(CancellationToken cancellationToken)
{
cde.Signal();
while (!cancellationToken.IsCancellationRequested)
{
byte[] buffer = await atc.ReadAsync(cancellationToken);
// queue message...
}
}
or SendStatementsAsync consumer which deque messages and awaits WriteAsync
private async Task SendStatementsAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var nextItem = await _outputStatements.Take();
cancellationToken.ThrowIfCancellationRequested();
// misc ...
await atc.WriteAsync(call.Serialize());
}
}
and some consumers just await on Take calls
var update = await _inputUpdateStatements.Take();
this construction works pretty well on tests, but there is one method where i think i made a huge mistake.
this method intent to run entire client backend, starting 3 pro/con while (true) loops simultaneously.
here it is:
public async Task RunAsync()
{
_isRunning = true;
_progress.ProgressChanged += progress_ProgressChanged;
await InitMTProto(_scheme).ConfigureAwait(false); // init smth...
// various init stuf...
await atc.ConnectAsync().ConfigureAwait(false); // open connection async
// IS IT WRONG?
try
{
await Task.WhenAny(SendStatementsAsync(_cts.Token),
ReadTcpClientAsync(_cts.Token),
ProcessUpdateAsync(_cts.Token, _progress)).ConfigureAwait(false);
}
catch (OperationCanceledException oce)
{
}
catch (Exception ex)
{
}
}
Forget about android for now, think any UI (WinForm, WPF, etc) OnCreate method in UI context to call RunAsync
protected async override void OnCreate(Bundle bundle)
{
// start RA
await client.RunAsync()
// never gets here - BAD, but nonblock UI thread - good
Debug.WriteLine("nevar");
}
so, as you can see there is a problem. I can't do anything after RunAsync await call because it will never returns from Task.WhenAny(...). And i need perform status check there, but i need this pro/cons methods started, because my check wait on ManualResetEvent for it:
if (!cde.Wait(15000))
{
throw new TimeoutException("Init too long");
}
Also, my check is async too, and it works like a charm :)
public async Task<TLCombinatorInstance> PerformRpcCall(string combinatorName, params object[] pars)
{
// wait for init on cde ...
// prepare call ...
// Produce
ProduceOutput(call);
// wait for answer
return await _inputRpcAnswersStatements.Take();
}
I think i should use another approach for starting this infinite loops, but i already have async Task methods all the way - so i really have no idea what to do.
Any help please?
Ok, after a lot of reading (nothing found) and #svick's advice i decided to call this methods without "await" as separate Task.Run's.
Aso i decided to run it in ThreadPool.
My final code is:
try
{
/*await Task.WhenAny(SendStatementsAsync(_cts.Token),
ReadTcpClientAsync(_cts.Token),
ProcessUpdateAsync(_cts.Token, _progress)).ConfigureAwait(false);*/
Task.Run(() => SendStatementsAsync(_cts.Token)).ConfigureAwait(false);
Task.Run(() => ReadTcpClientAsync(_cts.Token)).ConfigureAwait(false);
Task.Run(() => ProcessUpdateAsync(_cts.Token, _progress)).ConfigureAwait(false);
Trace.WriteLineIf(clientSwitch.TraceInfo, "Worker threads started", "[Client.RunAsync]");
}
Everything works fine as expected..
i'm not sure what problems it will cause in exception handling, as i know they will be lost
Of course such calls produce warning
Because this call is not awaited, execution of the current method
continues before the call is completed. Consider applying the 'await'
operator to the result of the call.
which can be easily suppressed this way
// just save task into variable
var send = Task.Run(() => SendStatementsAsync(_cts.Token)).ConfigureAwait(false);
Also, if anyone know better solution i will be grateful to hear it.

Aborting a long running task in TPL

Our application uses the TPL to serialize (potentially) long running units of work. The creation of work (tasks) is user-driven and may be cancelled at any time. In order to have a responsive user interface, if the current piece of work is no longer required we would like to abandon what we were doing, and immediately start a different task.
Tasks are queued up something like this:
private Task workQueue;
private void DoWorkAsync
(Action<WorkCompletedEventArgs> callback, CancellationToken token)
{
if (workQueue == null)
{
workQueue = Task.Factory.StartWork
(() => DoWork(callback, token), token);
}
else
{
workQueue.ContinueWork(t => DoWork(callback, token), token);
}
}
The DoWork method contains a long running call, so it is not as simple as constantly checking the status of token.IsCancellationRequested and bailing if/when a cancel is detected. The long running work will block the Task continuations until it finishes, even if the task is cancelled.
I have come up with two sample methods to work around this issue, but am not convinced that either are proper. I created simple console applications to demonstrate how they work.
The important point to note is that the continuation fires before the original task completes.
Attempt #1: An inner task
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
var token = cts.Token;
token.Register(() => Console.WriteLine("Token cancelled"));
// Initial work
var t = Task.Factory.StartNew(() =>
{
Console.WriteLine("Doing work");
// Wrap the long running work in a task, and then wait for it to complete
// or the token to be cancelled.
var innerT = Task.Factory.StartNew(() => Thread.Sleep(3000), token);
innerT.Wait(token);
token.ThrowIfCancellationRequested();
Console.WriteLine("Completed.");
}
, token);
// Second chunk of work which, in the real world, would be identical to the
// first chunk of work.
t.ContinueWith((lastTask) =>
{
Console.WriteLine("Continuation started");
});
// Give the user 3s to cancel the first batch of work
Console.ReadKey();
if (t.Status == TaskStatus.Running)
{
Console.WriteLine("Cancel requested");
cts.Cancel();
Console.ReadKey();
}
}
This works, but the "innerT" Task feels extremely kludgey to me. It also has the drawback of forcing me to refactor all parts of my code that queue up work in this manner, by necessitating the wrapping up of all long running calls in a new Task.
Attempt #2: TaskCompletionSource tinkering
static void Main(string[] args)
{ var tcs = new TaskCompletionSource<object>();
//Wire up the token's cancellation to trigger the TaskCompletionSource's cancellation
CancellationTokenSource cts = new CancellationTokenSource();
var token = cts.Token;
token.Register(() =>
{ Console.WriteLine("Token cancelled");
tcs.SetCanceled();
});
var innerT = Task.Factory.StartNew(() =>
{
Console.WriteLine("Doing work");
Thread.Sleep(3000);
Console.WriteLine("Completed.");
// When the work has complete, set the TaskCompletionSource so that the
// continuation will fire.
tcs.SetResult(null);
});
// Second chunk of work which, in the real world, would be identical to the
// first chunk of work.
// Note that we continue when the TaskCompletionSource's task finishes,
// not the above innerT task.
tcs.Task.ContinueWith((lastTask) =>
{
Console.WriteLine("Continuation started");
});
// Give the user 3s to cancel the first batch of work
Console.ReadKey();
if (innerT.Status == TaskStatus.Running)
{
Console.WriteLine("Cancel requested");
cts.Cancel();
Console.ReadKey();
}
}
Again this works, but now I have two problems:
a) It feels like I'm abusing TaskCompletionSource by never using it's result, and just setting null when I've finished my work.
b) In order to properly wire up continuations I need to keep a handle on the previous unit of work's unique TaskCompletionSource, and not the task that was created for it. This is technically possible, but again feels clunky and strange.
Where to go from here?
To reiterate, my question is: are either of these methods the "correct" way to tackle this problem, or is there a more correct/elegant solution that will allow me to prematurely abort a long running task and immediately starting a continuation? My preference is for a low-impact solution, but I'd be willing to undertake some huge refactoring if it's the right thing to do.
Alternately, is the TPL even the correct tool for the job, or am I missing a better task queuing mechanism. My target framework is .NET 4.0.
The real issue here is that the long-running call in DoWork is not cancellation-aware. If I understand correctly, what you're doing here is not really cancelling the long-running work, but merely allowing the continuation to execute and, when the work completes on the cancelled task, ignoring the result. For example, if you used the inner task pattern to call CrunchNumbers(), which takes several minutes, cancelling the outer task will allow continuation to occur, but CrunchNumbers() will continue to execute in the background until completion.
I don't think there's any real way around this other than making your long-running calls support cancellation. Often this isn't possible (they may be blocking API calls, with no API support for cancellation.) When this is the case, it's really a flaw in the API; you may check to see if there are alternate API calls that could be used to perform the operation in a way that can be cancelled. One hack approach to this is to capture a reference to the underlying Thread being used by the Task when the Task is started and then call Thread.Interrupt. This will wake up the thread from various sleep states and allow it to terminate, but in a potentially nasty way. Worst case, you can even call Thread.Abort, but that's even more problematic and not recommended.
Here is a stab at a delegate-based wrapper. It's untested, but I think it will do the trick; feel free to edit the answer if you make it work and have fixes/improvements.
public sealed class AbandonableTask
{
private readonly CancellationToken _token;
private readonly Action _beginWork;
private readonly Action _blockingWork;
private readonly Action<Task> _afterComplete;
private AbandonableTask(CancellationToken token,
Action beginWork,
Action blockingWork,
Action<Task> afterComplete)
{
if (blockingWork == null) throw new ArgumentNullException("blockingWork");
_token = token;
_beginWork = beginWork;
_blockingWork = blockingWork;
_afterComplete = afterComplete;
}
private void RunTask()
{
if (_beginWork != null)
_beginWork();
var innerTask = new Task(_blockingWork,
_token,
TaskCreationOptions.LongRunning);
innerTask.Start();
innerTask.Wait(_token);
if (innerTask.IsCompleted && _afterComplete != null)
{
_afterComplete(innerTask);
}
}
public static Task Start(CancellationToken token,
Action blockingWork,
Action beginWork = null,
Action<Task> afterComplete = null)
{
if (blockingWork == null) throw new ArgumentNullException("blockingWork");
var worker = new AbandonableTask(token, beginWork, blockingWork, afterComplete);
var outerTask = new Task(worker.RunTask, token);
outerTask.Start();
return outerTask;
}
}

Categories