Task.Factory.StartNew + TaskCreationOptions.LongRunning explanation - c#

I'm trying to understand what David Fowler said about Task.Factory.StartNew + TaskCreationOptions.LongRunning here.
💡 NOTE: Don't use TaskCreationOptions.LongRunning with async code as this will create a new thread which will be destroyed after first await.
I know that there is no point of having Task.Run or Task.Factory.StartNew in this case because SendLoopAsync and ReceiveLoopAsync are completely async. I also know that if there is a time-consuming synchronous part inside either one of these methods, the Task.Run/Task.Factory.StartNew should be inside that method.
What does David Fowler mean in his statement? That there shouldn't be TaskCreationOptions.LongRunning from within an async task? Or he meant that SendLoopAsync/ReceiveLoopAsync should not be async? I also know that TaskCreationOptions.LongRunning means that the task will start immediately, which isn't the case with just a normal task which gets scheduled by the scheduler, and might take some time to wind up. You can notice this behavior when starting multiple connections concurrently, which caused the Send and Receive loop to start with a significant delay.
public async Task StartAsync(CancellationToken cancellationToken)
{
_ = Task.Factory.StartNew(_ => SendLoopAsync(cancellationToken), TaskCreationOptions.LongRunning, cancellationToken);
_ = Task.Factory.StartNew(_ => ReceiveLoopAsync(cancellationToken), TaskCreationOptions.LongRunning, cancellationToken);
}
private async Task SendLoopAsync()
{
await foreach (var message in _outputChannel.Reader.ReadAllAsync(_cancellationSource?.Token))
{
if (_clientWebSocket.State == WebSocketState.Open)
{
await _clientWebSocket.SendAsync(message.Data.AsMemory(), message.MessageType, true, CancellationToken.None).ConfigureAwait(false);
}
}
}

David Fowler means that the SendLoopAsync/ReceiveLoopAsync should not be async. There is no point at starting a task as LongRunning, if this task is going to use the starting thread for a duration measured in nanoseconds. The ThreadPool was invented in order to handle exactly these types of situations. In case the ThreadPool is not responsive enough because it has become saturated, then it's more logical to try to find the cause of the saturation and fix it, instead of bypassing the ThreadPool, and creating new threads every time you have some microseconds-worth of work to do.
Here is a demonstration of what happens when LongRunning is combined with async:
var stopwatch = Stopwatch.StartNew();
Thread workerThread = null;
List<(string, long, System.Threading.ThreadState)> entries = new();
Task<Task> taskTask = Task.Factory.StartNew(async () =>
{
workerThread = Thread.CurrentThread;
entries.Add(("A", stopwatch.ElapsedMilliseconds, workerThread.ThreadState));
await Task.Delay(500);
entries.Add(("D", stopwatch.ElapsedMilliseconds, workerThread.ThreadState));
}, default, TaskCreationOptions.LongRunning, TaskScheduler.Default);
taskTask.Wait();
entries.Add(("B", stopwatch.ElapsedMilliseconds, workerThread.ThreadState));
workerThread.Join();
entries.Add(("C", stopwatch.ElapsedMilliseconds, workerThread.ThreadState));
await taskTask.Unwrap();
entries.Add(("E", stopwatch.ElapsedMilliseconds, workerThread.ThreadState));
foreach (var (title, elapsed, state) in entries)
Console.WriteLine($"{title } after {elapsed,3} msec worker thread is {state}");
Output:
A after 2 msec worker thread is Background
B after 6 msec worker thread is Background, Stopped
C after 6 msec worker thread is Stopped
D after 507 msec worker thread is Stopped
E after 507 msec worker thread is Stopped
Try it on Fiddle.
The lifetime of the worker thread was at most 6 milliseconds. All it really had to do was to instantiate an async state machine, and schedule a callback using a System.Threading.Timer component. 6 milliseconds look to me like an eon for such a minuscule workload. Most probably these 6 milliseconds were spent for inter-thread communication, and for the thread's creation and destruction.

Related

Calling UI Thread from Parallel Loop using Dispacher.BeginInvoke [duplicate]

This question already has answers here:
Parallel foreach with asynchronous lambda
(10 answers)
Closed 1 year ago.
I have a simple asynchronous code below : this is a WPF with one button and one textBox.
I used some list with five integers to mimic 5 different tasks.
My intention was to achieve that when I run all five tasks in parallel and asynchronously ,
I can observe that the numbers are one by one added to the textbox.
And I achieved it. Method "DoSomething" run all five tasks in parallel and each of the task has different execution time (simulated by Task.Delay) so all results in the numbers appearing in the textbox one by one.
The only problem that I cannot figure out is: why in the textbox I have the string text "This is end text" displayed at first ?! If I await method DoSomething then it should be accomplished first and then the rest of the code should be executed.Even though in my case is a repainting of the GUI.
I guess that this might be caused by the use of Dispacher.BeginInvoke which may "cause some disturbance " to async/await mechanism. But I would appreciate small clue and how to avoid this behawior.
I know that I could use the Progress event to achieve similar effect but is there any other way that I can use Parallel loop and update results progressively in WPF avoiding such a unexpected behaviour which I described?
private async void Button_Click(object sender, RoutedEventArgs e)
{
await DoSomething();
tbResults.Text += "This is end text";
}
private async Task DoSomething()
{
List<int> numbers = new List<int>(Enumerable.Range(1, 5));
await Task.Run(()=> Parallel.ForEach(numbers,async i =>
{
await Task.Delay(i * 300);
await Dispatcher.BeginInvoke(() => tbResults.Text += i.ToString() + Environment.NewLine);
}));
}
// output is:
//This is end text 1 2 3 4 5 (all in separatÄ™ lines).
My questions:
Why the text is displayed prior the method DoSomething .
How to solve it/ avoid it , any alternative way to solve it ( except using Progress event ).
Any info will be highly appreciate.
The threads of Parallel.Foreach are "real" background threads. They are created and the application continues execution. The point is that Parallel.Foreach is not awaitable, therefore the execution continues while the threads of the Parallel.Foreach are suspended using await.
private async Task DoSomething()
{
List<int> numbers = new List<int>(Enumerable.Range(1, 5));
// Create the threads of Parallel.Foreach
await Task.Run(() =>
{
// Create the threads of Parallel.Foreach and continue
Parallel.ForEach(numbers,async i =>
{
// await suspends the thread and forces to return.
// Because Parallel.ForEach is not awaitable,
// execution leaves the scope of the Parallel.Foreach to continue.
await Task.Delay(i * 300);
await Dispatcher.BeginInvoke(() => tbResults.Text += i.ToString() + Environment.NewLine);
});
// After the threads are created the internal await of the Parallel.Foreach suspends background threads and
// forces to the execution to return from the Parallel.Foreach.
// The Task.Run thread continues.
Dispatcher.InvokeAsync(() => tbResults.Text += "Text while Parallel.Foreach threads are suspended");
// Since the background threads of the Parallel.Foreach are not attached
// to the parent Task.Run, the Task.Run completes now and returns
// i.e. Task.run does not wait for child background threads to complete.
// ==> Leave Task.Run as there is no work.
});
// Leave DoSomething() and continue to execute the remaining code in the Button_Click().
// Parallel.Foreach threads is still suspended until the await chain, in this case Button_Click(), is completed.
}
The solution is to implement the pattern suggested by Clemens' comment or an async implementation of the Producer Consumer pattern using e.g., BlockingCollection or Channel to gain more control over the fixed number of threads while distributing the "unlimited" number of jobs.
private async Task DoSomething(int number)
{
await Task.Delay(number * 300);
Dispatcher.Invoke(() => tbResults.Text += number + Environment.NewLine);
}
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
List<int> numbers = new List<int>(Enumerable.Range(1, 5));
List<Task> tasks = new List<Task>();
// Alternatively use LINQ Select
foreach (int number in numbers)
{
Task task = DoSomething(number);
tasks.Add(task);
}
await Task.WhenAll(tasks);
tbResults.Text += "This is end text" + Environment.NewLine;
}
Discussing the comments
"my intention was to run tasks in parallel and "report" once they are
completed i.e. the taks which takes the shortest would "report" first
and so on."
This is exactly what is happening in the above solution. The Task with the shortest delay appends text to the TextBox first.
"But implementing your suggested await Task.WhenAll(tasks) causes
that we need to wait for all tasks to complete and then report all at
once."
To process Task objects in their order of completion, you would replace Task.WhenAll with Task.WhenAny. In case you are not only interested in the first completed Task, you would have to use Task.WhenAny in an iterative manner until all Task instances have been completed:
Process all Task objects in their order of completion
private async Task DoSomething(int number)
{
await Task.Delay(number * 300);
Dispatcher.Invoke(() => tbResults.Text += number + Environment.NewLine);
}
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
List<int> numbers = new List<int>(Enumerable.Range(1, 5));
List<Task> tasks = new List<Task>();
// Alternatively use LINQ Select
foreach (int number in numbers)
{
Task task = DoSomething(number);
tasks.Add(task);
}
// Until all Tasks have completed
while (tasks.Any())
{
Task<int> nextCompletedTask = await Task.WhenAny(tasks);
// Remove the completed Task so that
// we can await the next uncompleted Task that completes first
tasks.Remove(nextCompletedTask);
// Get the result of the completed Task
int taskId = await nextCompletedTask;
tbResults.Text += $"Task {taskId} has completed." + Environment.NewLine;
}
tbResults.Text += "This is end text" + Environment.NewLine;
}
"Parallel.ForEach is not awaitable so I thought that wrapping it up in
Task.Run allows me to await it but this is because as you said "Since
the background threads of the Parallel.Foreach are not attached to the
parent Task.Run""
No that's not exactly what I have said. The key point is the third sentence of my answer: "The point is that Parallel.Foreach is not awaitable, therefore the execution continues while the threads of the Parallel.Foreach are suspended using await.".
This means: normally Parallel.Foreach executes synchronously: the calling context continues execution when all threads of Parallel.Foreach have completed. But since you called await inside those threads, you suspend them in an async/await manner.
Since Parallel.Foreach is not awaitable, it can't handle the await calls and acts like the suspended threads have completed naturally. Parallel.Foreach does not understand that the threads are just suspended by await and will continue later. In other words, the await chain is broken as Parallel.Foreach is not able to return the Task to the parent awaited Task.Run context to signal its suspension.
That's what I meant when saying that the threads of Parallel.Foreach are not attached to the Task.Run. They run in total isolation from the async/await infrastructure.
"async lambdas should be "only use with events""
No, that's not correct. When you pass an async lambda to a void delegate like Action<T> you are correct: the async lambda can't be awaited in this case. But when passing an async lambda to a Func<T> delegate where T is of type Task, your lamda can be awaited:
private void NoAsyncDelegateSupportedMethod(Action lambda)
{
// Since Action does not return a Task (return type is always void),
// the async lambda can't be awaited
lambda.Invoke();
}
private async Task AsyncDelegateSupportedMethod(Func<Task> asyncLambda)
{
// Since Func returns a Task, the async lambda can be awaited
await asyncLambda.Invoke();
}
public voi DoSoemthing()
{
// Not a good idea as NoAsyncDelegateSupportedMethod can't handle async lamdas: it defines a void delegate
NoAsyncDelegateSupportedMethod(async () => await Task.Delay(1));
// A good idea as AsyncDelegateSupportedMethod can handle async lamdas: it defines a Func<Task> delegate
AsyncDelegateSupportedMethod(async () => await Task.Delay(1));
}
As you can see your statement is not correct. You must always check the signature of the called method and its overloads. If it accepts a Func<Task> type delegate you are good to go.
That's how async support is added to Parallel.ForeachAsync: the API supports a Func<ValueTask> type delegate. For example Task.Run accepts a Func<Task> and therefore the following call is perfectly fine:
Task.Run(async () => await Task.Delay(1));
" I guess that you admit that .Net 6.0 brought the best solution :
which is Parallel.ForEachASYNC! [...] We can spawn a couple of threads
which deal with our tasks in parallel and we can await the whole loop
and we do not need to wait for all tasks to complte- they "report" as
they finish "
That's wrong. Parallel.ForeachAsync supports threads thatr use async/await, that's true. Indeed, your original example would no longer break the intended flow: because Parallel.ForeachAsync supports await in its threads, it can handle suspended threads and propagate the Task object properly from its threads to the caller context e.g., to the wrapping await Task.Run.
It now knows how to wait for and resume suspended threads.
Important: Parallel.ForeachAsync still completes AFTER ALL its threads have completed. You assumption "they "report" as they finish" is wrong. That's the most intuitive concurrent implementation of a foreach. foreach also completes after all items are enumerated.
The solution to process Task objects as they complete is still using the Task.WhenAny pattern from above.
In general, if you you don't need the extra features like partitioning etc. of the the Parallel.Foreach and Parallel.ForeachAsync, you can always use Task.WhenAll instead. Task.WhenAll and especially Parallel.ForeachAsync are equivalent, except for Parallel.ForeachAsync provides greater customization by default: it suppors techniques like throttling and partitioning without the extra code.
Good links from Clemens see in comment. Answering your questions:
In Parallel.ForEach you start/fire for each entry of numbera an async task, which you don't await. So you do only await, that Parallel.ForEach does finish and it does finish before the async tasks of it.
What you could do e.g. remove async inside of Parallel.ForEach and use Dispatcher.Invoke instead of Dispatcher.BeginInvoke, Thread.Sleep is an anti-pattern ;) , so depending on your task may be take another solution(edited: BionicCode has a nice one):
private async Task DoSomething()
{
var numbers = new List<int>(Enumerable.Range(1, 5));
await Task.Run(()=> Parallel.ForEach(numbers, i =>
{
Thread.Sleep(i * 300);
Dispatcher.Invoke(() => tbResults.Text += i.ToString() + Environment.NewLine);
}));
}

Is this good practice use of an async task with monitor and sleep?

I am currently running an async task on polling (meaning this async task is called every 1s). All other items need to update that fast, with exception of my async task below. This code works, but I wonder if it's good practice?
Note: _monitor only gets used by DoAsync()
private readonly object _monitor = new object();
private void PolledEverySecond()
{
_ = DoAsync(); // do this every 5 seconds
// Other stuff
GetNetworkState();
GetCurrentVelocity();
GetCurrentPosition();
Etc;
}
private async Task DoAsync()
{
await Task.Run(() =>
{
if (!Monitor.IsEntered(_monitor))
{
try
{
Monitor.Enter(_monitor);
DoStuff();
}
finally
{
Thread.Sleep(5000);
Monitor.Exit(_monitor);
}
}
});
}
The intention behind the Monitor.Enter/Monitor.Exit and the Thread.Sleep(5000). Is that DoAsync() does not get called every 1 second. I have a polling service that works great to update things and it's used by many of my ViewModels. But, in the case of DoAsync(), it is overkill to poll every second. Therefore, by making it async and using monitors, DoStuff() only gets called approximately every 5-6 seconds.
There are two problems with your current code, a race condition between the Monitor.IsEntered and Monitor.Enter calls, and the blocking of a ThreadPool thread with the Thread.Sleep(5000) call. Blocking ThreadPool threads is not consider a good practice, because it may result to the saturation of the pool. A saturated ThreadPool cannot respond immediately to requests for work, so the program becomes less responsive. Also in this case the ThreadPool has to inject more threads in the pool, resulting to increased memory consumption (each thread requires at least 1 MB of memory).
My suggestion is to switch from the blocking Thread.Sleep to the asynchronous (non-blocking) Task.Delay method, and also to
switch from the thread-affine Monitor to the thread-agnostic SemaphoreSlim. In order to check the availability and acquire the semaphore as an atomic operation, you could use the Wait method, passing zero milliseconds as argument:
private readonly SemaphoreSlim _semaphore = new(1, 1);
private async Task DoAsync()
{
await Task.Run(async () =>
{
bool acquired = _semaphore.Wait(0);
if (acquired)
{
var delayTask = Task.Delay(5000);
try
{
DoStuff();
}
finally
{
await delayTask;
_semaphore.Release();
}
}
});
}
This way the ThreadPool thread will be utilized only during the DoStuff execution, and then it will be freed, and it will be available for other work.
Creating the Task.Delay(5000) task before starting the DoStuff and awaiting it afterwards, has the generally desirable effect of including the duration of the DoStuff into the 5 seconds delay. If you don't want this behavior, you can just create and await the task in the same line: await Task.Delay(5000);

How start a Task and define a timeout to finish it

We have a project where we run some tasks in parallel. We are starting Tasks to do this. Every Task runs a code inside a CodeDom (written by customer to customize part of the process). It works fine.
I would like to know how to limitate the time of the execution of a Task. I am afraid of the user write a bad code and it result in something like infinity loop or a bad code which takes a lot of time to perform. I would like to give him 10 seconds to perform its code and after that, kill the thread that is running on the Task and finish the Task.
For sample (pseudo code):
Task[] tasks = GetListOfTasks();
foreach (var task in tasks)
{
task.Start();
}
I tried to do this:
Task.WaitAll(tasks, TimeSpan.FromSeconds(10));
I already have the cancelation token on all these tasks, but the user can't use it.
How can I stop the task execution and kill the tasks if it do not finish on the specified TimeSpan ?
Thank you.
var threadTask = Task.Run(() =>
{
// start thread
thread.Start();
// force to wait the thread
thread.Join();
});
// run a new task to wait the task/thread to finish in a timeout.
// on the continueWith, abort the thread.
Task.Run(() => Task.WaitAll(new[] { threadTask }, TimeSpan.FromSeconds(10)))
.ContinueWith(t =>
{
if (thread.IsAlive)
{
thread.Abort();
}
});

Parallel.ForEach using Thread.Sleep equivalent

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.

How to determine when all task is completed

here is sample code for starting multiple task
Task.Factory.StartNew(() =>
{
//foreach (KeyValuePair<string, string> entry in dicList)
Parallel.ForEach(dicList,
entry =>
{
//create and add the Progress in UI thread
var ucProgress = (Progress)fpPanel.Invoke(createProgress, entry);
//execute ucProgress.Process(); in non-UI thread in parallel.
//the .Process(); must update UI by using *Invoke
ucProgress.Process();
System.Threading.Thread.SpinWait(5000000);
});
});
.ContinueWith(task =>
{
//to handle exceptions use task.Exception member
var progressBar = (ProgressBar)task.AsyncState;
if (!task.IsCancelled)
{
//hide progress bar here and reset pb.Value = 0
}
},
TaskScheduler.FromCurrentSynchronizationContext() //update UI from UI thread
);
when we start multiple task using Task.Factory.StartNew() then we can use .ContinueWith() block to determine when each task finish. i mean ContinueWith block fire once for each task completion. so i just want to know is there any mechanism in TPL library. if i start 10 task using Task.Factory.StartNew() so how do i notify after when 10 task will be finish. please give some insight with sample code.
if i start 10 task using Task.Factory.StartNew() so how do i notify after when 10 task will be finish
Three options:
The blocking Task.WaitAll call, which only returns when all the given tasks have completed
The async Task.WhenAll call, which returns a task which completes when all the given tasks have completed. (Introduced in .NET 4.5.)
TaskFactory.ContinueWhenAll, which adds a continuation task which will run when all the given tasks have completed.
if i start 10 task using Task.Factory.StartNew() so how do i notify after when 10 task will be finish
You can use Task.WaitAll. This call will block current thread until all tasks are finished.
Side note: you seem to be using Task, Parallel and Thread.SpinWait, which makes your code complex. I would spend a bit of time analysing if that complexity is really necessary.
You can use the WaitAll(). Example :
Func<bool> DummyMethod = () =>{
// When ready, send back complete!
return true;
};
// Create list of tasks
System.Threading.Tasks.Task<bool>[] tasks = new System.Threading.Tasks.Task<bool>[2];
// First task
var firstTask = System.Threading.Tasks.Task.Factory.StartNew(() => DummyMethod(), TaskCreationOptions.LongRunning);
tasks[0] = firstTask;
// Second task
var secondTask = System.Threading.Tasks.Task.Factory.StartNew(() => DummyMethod(), TaskCreationOptions.LongRunning);
tasks[1] = secondTask;
// Launch all
System.Threading.Tasks.Task.WaitAll(tasks);
Another solution:
After the completion of all the operation inside Parallel.For(...) it return an onject of ParallelLoopResult, Documentation:
For returns a System.Threading.Tasks.ParallelLoopResult object when
all threads have completed. This return value is useful when you are
stopping or breaking loop iteration manually, because the
ParallelLoopResult stores information such as the last iteration that
ran to completion. If one or more exceptions occur on one of the
threads, a System.AggregateException will be thrown.
The ParallelLoopResult class has a IsCompleted property that is set to false when a Stop() of Break() method has been executed.
Example:
ParallelLoopResult result = Parallel.For(...);
if (result.IsCompleted)
{
//Start another task
}
Note that it advised to use it only when breaking or stoping the loop manually (otherwise just use WaitAll, WhenAll etc).

Categories

Resources