0

I am starting a Thread where an await Task.Run can be invoked.

After starting a Thread with the ThreadStart.Start method, why does the await Task.Run terminate the Thread and Task.Run does not?

Here is some code as an example:

public async Task Task1()
{
    if (awaitRunTask)
    {
        await Task.Run(async () =>
        {
            await Test();
        }
        );
    }
    else
    {
        Task.Run(async () =>
        {
            await Test();
        }
        );
    }
}

In the above example, if a Thread invokes the Task1 method, and awaitRunTask = true, the Thread terminates. If awaitRunTask = false, the Thread does not terminate.

When I say terminate, the Thread does not complete correctly and the method where the ThreadStart.Start is invoked returns. This happens at the await Test() code.

Why is this and if I want to await a Task.Run on a Thread, is there a way to do this?

EDIT

Here is some code to show a more detailed example:

public class ThreadExample
{
    public bool awaitRunTask;
    public string value;
    private async Task StartThreadAsync()
    {
        var method = this.GetType().GetMethod("RunTasksAsync");
        ThreadStart threadStart;
        threadStart = async () =>
        {
            await InvokeAsync(method, this, null);
        };
        var thread = new Thread(threadStart);
        thread.Start();
        thread.Join();
    }
    public async Task RunTasksAsync()
    {
        await Task1Async();
        Task2();
    }
    private async Task Task1Async()
    {
        if (awaitRunTask)
        {
            await Task.Run(async () =>
            {
                await TestAsync();
            }
            );
        }
        else
        {
            Task.Run(async () =>
            {
                await TestAsync();
            }
            );
        }
    }
    private void Task2()
    {
        value = "valid";
    }
    private async Task TestAsync()
    {
        await Task.Delay(1000);
    }
    private async Task InvokeAsync(MethodInfo method, object instance, object[] parameters)
    {
        dynamic awaitable = method.Invoke(instance, parameters);
        await awaitable;
    }
    public async Task ValueIsCorrectAsync()
    {
        value = "not valid";
        awaitRunTask = false;
        await StartThreadAsync();
        var isCorrect = (value == "valid");
    }
    public async Task ValueIsNotCorrectAsync()
    {
        value = "not valid";
        awaitRunTask = true;
        await StartThreadAsync();
        var isCorrect = (value == "valid");
    }
}

The ValueIsCorrectAsync method works correctly as the Task2 method sets the value field.

The ValueIsNotCorrectAsync method does not work correctly as the await Task.Run in the Task1Async method interferes with the Thread. The StartThreadAsync method returns before the Task2 method sets the value field.

The only difference between the two methods, is the value of awaitRunTask.

How should I change my code such that the value field is set correctly when awaitRunTask = true?

EDIT3

If the await Task.Delay(1000); is commented out in the TestAsync method, the code works for both awaitRunTask = true and awaitRunTask = false;

Can someone please explain to me why? I need to know why because the TestAsync method needs to be able to run asynchronous code.

Simon
  • 7,991
  • 21
  • 83
  • 163
  • It would be super useful to know how to run the sample code. – Enigmativity Aug 22 '22 at 06:16
  • The `StartThreadAsync` method is probably giving you a compilation warning about an `async` method that lacks an `await` keyword, correct? – Theodor Zoulias Aug 22 '22 at 08:02
  • Related: [Is it ok to use "async" with a ThreadStart method?](https://stackoverflow.com/questions/44364092/is-it-ok-to-use-async-with-a-threadstart-method) – Theodor Zoulias Aug 22 '22 at 08:03
  • @Theodor Zoulias That is correct. Even though I have async code for the ThreadStart. – Simon Aug 22 '22 at 08:12
  • Can the code be adapted such that the compilation warning does not happen, and thus resolve the problem? – Simon Aug 22 '22 at 10:04
  • Simon I mentioned the compilation warning because it should give you a clue that you are doing something wrong. To be honest there are multiple things wrong in the code, there are too many questions asked inside the same question, the questions are not presented in an easily comprehensible manner, which makes it overall a not good question IMHO. I wish I had some idea about how the question could be improved, but I don't. – Theodor Zoulias Aug 22 '22 at 12:32
  • @Theodor Zoulias When you see the StartThreadAsync method, there is async code in it, I am not sure why the compilation warning is shown. Maybe the compiler cannot see the async code in the threadStart variable. – Simon Aug 22 '22 at 12:58
  • The code inside the `threadStart` lambda is not part of the execution flow of the `StartThreadAsync` method. Adding an `await` inside the general area of a method (from a spatial perspective) is not enough to make it a proper asynchronous method. The `await` must also be part of the method's execution flow. – Theodor Zoulias Aug 22 '22 at 13:17
  • Are you able to explain to me why the await Task.Delay(1000) stops the flow of execution when awaitRunTask = true? – Simon Aug 22 '22 at 13:25
  • @Simon - The answers currently explain why `await Task.Delay(1000)` stops the flow of execution. Do you understand that `async` state machine let's the calling thread return and the execution then continues after the `Task.Delay` on a thread from the thread pool? – Enigmativity Aug 23 '22 at 03:50
  • @Simon - You really should answer my question as to who you need to do this? What's the underlying need? – Enigmativity Aug 23 '22 at 03:51
  • @Enigmativity Can any changes be made in the `StartThreadAsync` method such that the `await Task.Delay(1000)` does not return to the calling thread? Or can the `Task1Async` or `TestAsync` methods be written differently for this to not happen? – Simon Aug 25 '22 at 09:44
  • @Simon - Can you please answer my question? Then I might be able to give you alternatives. – Enigmativity Aug 25 '22 at 12:17

2 Answers2

3

Why is this?

As I explain on my blog, await here is actually returning to its caller. So the Task1 method returns an incomplete task to its caller, presumably the thread's main method, which presumably is async void. When the thread's main method returns (due to it's await), the thread exits.

The core of the problem is that the Thread type doesn't understand or work naturally with asynchronous code. Thread is really a very low-level building block at this point and is best avoided in modern code. There are very few scenarios where it can't be replaced with Task.Run.

if I want to await a Task.Run on a Thread, is there a way to do this?

The easiest solution is to get rid of the legacy thread completely; replace it with Task.Run.

Otherwise, you need the thread to block. If the continuations can run on thread pool threads, then you can just block directly (e.g., GetAwaiter().GetResult()). If the continuations need to run on that thread, then use AsyncContext from my AsyncEx library.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Can you have a look at my edit where I have added a more detailed example? The Join method joins the Thread, yet the await Task.Run still does not return the correct result. – Simon Aug 22 '22 at 05:59
  • Did you read the link I gave? As described there, `await` will continue synchronously if the task is already completed. So all the thread code is synchronous when `awaitRunTask` is `false`. – Stephen Cleary Aug 22 '22 at 11:26
  • I have read the link you gave. In the code I have posted, the problem appears to be at the code await Task.Delay(1000). Can you explain what I should do such that the flow of execution does not stop when getting to the await Task.Delay(1000)? – Simon Aug 22 '22 at 13:53
  • 1
    @Simon: The best solution is to replace the `Thread` with `Task.Run`. Is there some reason you can't do that? – Stephen Cleary Aug 22 '22 at 14:52
  • The reason that I am using the Thread is because it can be Aborted rather than having to code any additional CancellationToken code. – Simon Aug 23 '22 at 01:14
  • 1
    Please don't take this the wrong way, but `Abort` is one of the most serious *problems* with `Thread`! It is dangerous and will eventually lead to application instability. Please use the properly-designed modern cancellation system instead. – Stephen Cleary Aug 23 '22 at 03:21
  • 1
    `Can someone please explain to me why?` As [described on my blog](https://blog.stephencleary.com/2012/02/async-and-await.html), it's because it runs synchronously. All your "working" examples are ones that run synchronously and are not asynchronous at all. – Stephen Cleary Aug 23 '22 at 03:24
  • @Simon be aware that the `Thread.Abort` [is not supported](https://stackoverflow.com/questions/53465551/net-core-equivalent-to-thread-abort) on .NET Core and later, and [never will](https://github.com/dotnet/runtime/issues/11369#issuecomment-692155140). – Theodor Zoulias Aug 23 '22 at 03:54
0

Here's a minimal version of your sample code:

async Task Main()
{
    var te = new ThreadExample();
    await te.StartThreadAsync(false);
    await te.StartThreadAsync(true);
}

public class ThreadExample
{
    public string value;
    public async Task StartThreadAsync(bool awaitRunTask)
    {
        value = "not valid";
        var thread = new Thread(() => Task1Async(awaitRunTask));
        thread.Start();
        thread.Join();
        var isCorrect = (value == "valid");
        Console.WriteLine(isCorrect);
    }

    private async Task Task1Async(bool awaitRunTask)
    {
        if (awaitRunTask)
        {
            await Task.Run(async () => await Task.Delay(1000));
        }
        value = "valid";
    }
}

This outputs:

True
False

The thread that enters Task1Async executes the line value = "valid" when awaitRunTask == false, but when it's true it hits the await Task.Run and, because of the async state machine, the thread returns to the caller at this point and executes the thread.Join().

Effectively you've created an extreme race condition.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Will the async state machine always do this, such that there is no work around? – Simon Aug 22 '22 at 06:54
  • @Simon - What are you actually trying to do? – Enigmativity Aug 22 '22 at 07:08
  • I am wanting to be able run a Task asynchronously from a Thread without the Thread returning to the caller. – Simon Aug 22 '22 at 09:19
  • The Task could run anything, including an asynchronous method via reflection. The Task may or may not be awaited, hence having the await in a Boolean. – Simon Aug 22 '22 at 09:51
  • Also, the Thread I added is joined such that the code can be adapted to have a return value from the ThreadStart InvokeAsync method at a later date. – Simon Aug 22 '22 at 09:55
  • 2
    @Simon - That's a description of the solution you've presented here. What is the underlying business need for this? What problem is this solution trying to solve? – Enigmativity Aug 22 '22 at 10:25
  • @Simon - I have tested the code (now twice) and it produces the output I included in my answer. What's exactly not working when you run my code? – Enigmativity Aug 22 '22 at 11:10
  • I am after isCorrect == true for both StartThreadAsync(true) and StartThreadAsync(false). – Simon Aug 22 '22 at 11:44
  • @Simon - Then don't use a thread. But if you explain what your actual problem is, then maybe there's a solution. – Enigmativity Aug 22 '22 at 12:19
  • I seem to have got the await Task Run sorted. Can you have a look at my Edit3? – Simon Aug 22 '22 at 12:52