0

In situation 1, taska and taskb are in the same scope. taska bites the dust and we leave this scope prematurely. What becomes of taskb? To put it another way: mechanically, how does the Framework/OS know how to kill taskb or otherwise do something with it?

In situation 2, we don't leave scope prematurely, so taskb completes its mission as expected. No surprises here.

(The real problem at hand is working out the cleanest way to handle ctrl+c in a console app that spins off several tasks using a couple daisy-chained TPL dataflow ActionBlocks with MaxDegreeOfParallelism set to processor count on the second one. This was a minimal repro of the concept. What's the wisdom (or folly) of waiting (or not waiting) for all completions if the user wants to abort?)

int a = 0;
int b = 0;

int counta = 0;
int countb = 0;

Task taska = Task.Run(() =>
{
    Random rng = new Random((int)DateTime.UtcNow.Ticks);
    for (int i = 0; i < 100000000; i++)
    {
        a += rng.Next(0, 1000000) - 500000;
        counta++;
        if (counta > 100000) { throw new NotImplementedException(); }
    }
});

Task taskb = Task.Run(() =>
{
    Random rng = new Random((int)DateTime.UtcNow.Ticks);
    for (int i = 0; i < 100000000; i++)
    {
        b += rng.Next(0, 1000000) - 500000;
        countb++;
    }
});

try
{
    Task.WaitAll(new Task[] { taska });        // SITUATION 1
  //Task.WaitAll(new Task[] { taska, taskb }); // SITUATION 2
}
catch (Exception)
{
    Console.WriteLine(a);
    Console.WriteLine(b);
    Console.WriteLine(counta);
    Console.WriteLine(countb);
}

Output:

-30873531
-477919899
100001
3678224

EDIT: This question is similar to the linked questions, but I feel like it would still be helpful for future searchers, because it is focused on (plausible but incorrect) perceptions about how scope affects (or doesn't affect) the outcome.

amonroejj
  • 573
  • 4
  • 16
  • To make matters worse, my tasks may be starting external .exes with Process.Start. – amonroejj May 10 '21 at 20:03
  • 2
    Tasks are not killed, they just stop running (unless the process exits, at which point all tasks do end unceremoniously). An unhandled exception terminates the regular task flow. There is no "scope" associated with tasks, nor is there any kind of relation between `taska` and `taskb` here. If you don't wait for `taskb` to complete, it will just continue to merrily run on its own. All of the above applies more or less unchanged if you replace "task" with "thread that has `IsBackground` set to `true`", incidentally. – Jeroen Mostert May 10 '21 at 20:05
  • 1
    Note that neither of the following things affect the running of `taskb`: that the variables `taskb` uses go out of scope (these have been captured in a closure that remains alive) or that the variable `taskb` itself goes out of scope (this does not free up the thread that is running the code, and the `Task` remains alive for as long as the delegate associated with it is running). This is why it's a good thing that `Task`s have a built-in mechanism for cooperative cancellation (`CancellationToken`), which ideally you should be using if you're not going to exit the process outright. – Jeroen Mostert May 10 '21 at 20:10
  • 1
    _"how does the Framework/OS know how to kill taskb or otherwise do something with it"_ -- it doesn't. As noted above, tasks are independent of each other, unless you do something explicit to make it otherwise. For example, you cancel the second task if the first throws an exception. See duplicates. – Peter Duniho May 10 '21 at 20:54
  • 1
    Tasks aren't threads, they represent a job that needs to run on a threadpool thread. In other languages you'd call them a `promise` or a `future`. Nothing ties the job named `taska` to the job named `taskb`, so when job `taska` fails, `taskb` will keep running until the application terminates – Panagiotis Kanavos May 11 '21 at 06:48
  • `The real problem at hand is working out the cleanest way to handle ctrl+c in a console app that spins off several tasks using a couple daisy-chained TPL dataflow ActionBlocks with MaxDegreeOfParallelism set to processor count on the second one.` that has **nothing at all** to do with what you asked. Never mind that `ActionBlocks` aren't daisy-chained, TransformBlocks are. Post your code. You're doing something unusual. – Panagiotis Kanavos May 11 '21 at 06:50
  • Normally, to gracefully shut down a dataflow mesh/pipeline composed of chained TransformBlocks, you only need to complete the head block and let completion propagate. As long as you specified `PropagateCompletion` to the `LinkTo` call, completion will flow from one block to the next when it finishes processing any queued messages. ActionBlocks can only appear at the *end* of a pipeline, they can't be linked to other blocks. If you manually post messages between blocks, you'll have to manually propagate completion as well. You'll have to await all blocks to complete too, to ensure they finish – Panagiotis Kanavos May 11 '21 at 06:53
  • If you create a pipeline of TransformBlocks on the other hand, all you have to do is call `Complete` on the head block, and await the final block's `Completion` task. If you want too abruptly cancel the entire pipeline and discard messages, you need to [pass a CancellationToken](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/how-to-cancel-a-dataflow-block) to all blocks, and signal it when needed – Panagiotis Kanavos May 11 '21 at 06:58
  • Check [Walkthrough: Creating a Dataflow Pipeline](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/walkthrough-creating-a-dataflow-pipeline) for an example of a dataflow pipeline. It shows how to create blocks, link them, and gracefully stop the pipeline by calling `downloadString.Complete();` on the head block, and await until the tail completes with `await printReversedWords.Completion` – Panagiotis Kanavos May 11 '21 at 07:01
  • To react to `Ctrl+C` you can handle the [Console.CancelKeyPress](https://learn.microsoft.com/en-us/dotnet/api/system.console.cancelkeypress?view=net-5.0) event and signal a CancellationTokenSource that either cancels the pipeline, or simply interrupts the loop that pumps messages to your pipeline – Panagiotis Kanavos May 11 '21 at 07:07
  • _"You're doing something unusual."_ -- I'm implementing a DAG of independent jobs per https://medium.com/@pavloosadchyi/parallel-running-dag-of-tasks-in-pythons-celery-4ea73c88c915 with one ActionBlock as a queue and a second ActionBlock that works nodes and then posts all downstream neighbors of the worked node to the first block. This is all working fine. _"that has nothing at all to do with what you asked"_ -- It does, in the sense that if I get _different outputs_ depending on whether I wait on one or both of the ActionBlocks' completions. That was the genesis of the question. – amonroejj May 11 '21 at 13:49
  • @JeroenMostert Upgrade your comment to an answer and I can accept it. – amonroejj May 13 '21 at 14:55
  • Questions successfully closed as duplicates cannot get answers. – Jeroen Mostert May 14 '21 at 06:23

0 Answers0