.net MAUI c# Background Task ContinueWith and notification Event - c#

[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.

Related

Chain tasks using continuation task

I'm trying to chain tasks, so as soon as the one finishes the next starts, but the UI doesn't update. I did a course in react and the one lesson is where you update the UI based on state changes in the application, and that is what I'm trying to replicate. Change the state of the application (basically I'll be running methods that run return a bool for validation), and then update the UI accordingly, I'm also using binding, but for some reason its not running as intended, I don't know if I follow the documentation incorrectly. What can I change or fix to make this work and is it practically correct to use more than one task in a single async Task<T> method
public async Task<string> Connect_To_Ip()
{
await Task.Run(() =>
{
details.State = "Connection To IP 127.0.01.258.....";
Task.Delay(5000).Wait();
}).ContinueWith(result => new Task(async () =>
{
await Task.Run(() =>
{
if (result.Status == TaskStatus.RanToCompletion)
{
details.State = "Validating Card Number......";
}
});
}), TaskContinuationOptions.OnlyOnRanToCompletion);
return details.State;
}
How I'm calling the original task
Task connect = Connect_To_Ip();
await connect;
When you use await then you don't need Task.ContinueWith. Everything that follows the awaited operation is a continuation. Since you want to validate on a background thread, you must post the changes back to the UI thread in order to update the UI elements, otherwise you will produce cross-thread exceptions.
This is because UI elements can't be updated from a background thread, except the update occurs via INotifyPropertyChanged and data binding.
One way to do this is to use the Dispatcher to invoke UI manipulations on the UI thread or use the Progress<T> class, which will always execute the registered callback on the UI thread.
Your fixed and simplified code could look like this example:
public async Task ValidateAsync()
{
// Register the callback that updates the UI with the 'progressReporter'.
// Progress<T> must be instantiated on the UI thread it is associated with
var progressReporter = new Progress<string>(message => details.State = message);
// Execute the operation on a background thread
await Task.Run(() => ConnectToIp(progressReporter));
// Continuation starts here, after await
}
public async Task ConnectToIp(IProgress<string> progressReporter)
{
progressReporter.Report("Connection To IP 127.0.01.258.....");
await Task.Delay(TimeSpan.FromSeconds(5));
// Continuation starts here, after await
progressReporter.Report("Validating Card Number......");
}
It is recommended to use async APIs when possible instead of using background threads. For example, to connect to a server without blocking the UI you can use
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync("http://www.contoso.com/");
Many IO classes provide an async API.
Furthermore, I recommend to take a look at the INotifyDataErrorInfo interface. It is the recommended way to implement property validation and allows to provide UI error feedback in a very easy way.
I did this in Windows Forms (I had a test Windows Forms project open), but it should be about the same in WPF. I dropped a button, a label and a text box on the form. Then I wrote this code:
private async void button1_Click(object sender, EventArgs e)
{
var result = await ValidateTextBox();
if (result != null)
{
label1.Text = result;
return;
}
var intResult = await ReadTextBox();
label1.Text = intResult.ToString();
await IncrementTextBox();
intResult = await ReadTextBox();
label1.Text = intResult.ToString();
}
private async Task<string> ValidateTextBox()
{
await Task.Delay(2000);
if (!int.TryParse(textBox1.Text, out _)) {
return "Not Valid";
}
//otherwise
return null;
}
private async Task<int> ReadTextBox()
{
await Task.Delay(3000);
if (!int.TryParse(textBox1.Text, out var result))
{
throw new Exception("Don't do that");
}
return result;
}
private async Task IncrementTextBox()
{
await Task.Delay(3000);
if (!int.TryParse(textBox1.Text, out var result))
{
throw new Exception("Don't do that");
}
textBox1.Text = (result + 1).ToString();
}
If you type something that's not an int into the text box and press the button, a few seconds go by, and then Not Valid shows up in the label.
If there is a number there, then there is a pause and the number shows up in the label. Then another pause and the text box number will increment by 1. Finally after another pause, the label will show the incremented value.
Note that this all runs on a single thread. But, in spite of all the delays, the UI remains responsive the whole time.
Put breakpoints at the start of each function and on the lines after each of the awaits in the button click handler. Step through (not into) the whole thing and you'll see how the awaits create continuations

Trying to call Start() on a continuation task

I am writing an console application where I want to offload some CPU-bound work onto a new thread in order to keep the main thread responsive. However, I only want to create one new thread at a time; if more CPU-bound work is requested while some is still ongoing, that task should get queued. Here's my implementation:
readonly ConcurrentQueue<Task> _searchQueue = new ConcurrentQueue<Task>();
volatile bool _searchInProgress = false;
var searchTask = new Task(() =>
{
// ... do some cpu-bound work ...
// run the next task if one is queued
if (_searchQueue.TryDequeue(out var nextTask))
{
nextTask.Start();
}
else
{
_searchInProgress = false;
}
});
// *** attempt to propogate exceptions to the main thread ***
searchTask = searchTask.ContinueWith(t =>
{
if (t.IsFaulted) throw t.Exception;
});
if (!_searchInProgress)
{
_searchInProgress = true;
searchTask.Start();
}
else _searchQueue.Enqueue(searchTask);
This code worked fine before I added the ContinueWith clause. After I added it, I got the error:
Start may not be called on a continuation task.
A related SO question says to use Task.Factory.StartNew instead of the Task constructor, but this doesn't work for my use case since I want to create the Task but may not want to immediately start it.

async await execution order - code only actually works when stepping through/debugging

I'm hoping there is a simple answer here, and this is probably due to my misunderstanding of asynchronous operations...
I have a method that can be started manually or can autostart when the program loads. The async method works perfectly when invoked manually (on button press). However, when autoloaded the method just seems to skip the main "await" part of the method without performing any work, and skips straight to the end.
The whole process starts in this method:
private void StartBatch()
{
var batchSize = (int)BatchSizeSlider.Value;
if (_config.AutoStart)
{
ExecutionLogAddItem(string.Format("Auto batch processing started (batch size: {0})", batchSize.ToString()));
Task.Factory.StartNew(async () =>
{
await BatchTransfer(batchSize);
CompleteBatch();
});
}
else
{
var start = ConfirmStartBatch();
var doBatch = start.ContinueWith(async (continuation) =>
{
//start task
if (start.Result == true)
{
ExecutionLogAddItem("Batch processing started.");
ExecutionLogAddItem(string.Format("Batch size set at {0}", batchSize.ToString()));
await BatchTransfer(batchSize).ContinueWith((x) => CompleteBatch());
}
else
{
ExecutionLogAddItem("Batch processing aborted.");
}
});
}
}
If _config.AutoStart is true, the BatchTransfer method doesn't seem to do anything, instead the program skips straight to the CompleteBatch() method. If invoked manually everything works as expected.
The strange thing is, if I set a breakpoint on await BatchTransfer(batchSize) in the autostarted method, I can step through the code and the batch transfers take place. So when debugging it works, when not debugging it doesn't. Please help!
It is because -
Task.Factory.StartNew(async () =>
{
await BatchTransfer(batchSize);
CompleteBatch();
});
You are waiting for the inner task to complete with await but Task.Factory.StartNew(async () => itself is an asynchronous task and is not awaited. You should also wait for Task.Factory.StartNew(async () => like this -
await Task.Factory.StartNew(async () =>
When you are debugging, the separate thread that is calling inner task is held and you can see the execution but when running normally the background is still working, but you cannot see it since you didn't wait for the Task.Factory.StartNew(async () =>.
If you check the thread pool and thread id, I am sure you will see that they are different when debugging.
This blog might help you understand the situation - http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
In order to use await you have to make your method async and call it without Task.Factory.StartNew. One more thing, instead of void make return type as Task of your method because void async are fire and forget. You will not able to track them.
private async Task StartBatch()
{
await BatchTransfer(batchSize);
CompleteBatch();
}
Check this link. It have very basic demonstration of async and it is very helpful in understanding how asynchrony works. Six Essential Tips For Async - Introduction. It includes six tips which are very essential. I recommend you go through all of them but to understand current question situation you can go through Tip 1 whose title is Async void is for top-level event-handlers only.

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