Skip to content

Unexpected behaviour where code execution suddenly stops #115960

Closed
@Moe-Baker

Description

@Moe-Baker

Description

Code execution stops unexpectedly under some async code.

Reproduction Steps

Taking this code into consideration

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    async static Task Main()
    {
        var bugged = true;

        if (bugged)
            await Use();
        else
            Use();

        while (true) Console.ReadKey();
    }

    public static async Task Use()
    {
        var coordinator = new TaskCoordinator();

        coordinator.Register(Task1);
        async Task Task1(TaskCoordinator.Handle handle)
        {
            await Task.Delay(200);

            throw new NotImplementedException();

            Program.Print("Task 1 Finished");
        }

        coordinator.Register(Task2);
        async Task Task2(TaskCoordinator.Handle handle)
        {
            await Task.Delay(400);
            handle.ThrowIfCancellationRequested();

            Program.Print("Task 2 Finished");
        }

        coordinator.Register(Task3);
        async Task Task3(TaskCoordinator.Handle handle)
        {
            await Task.Delay(400);
            handle.ThrowIfCancellationRequested();

            Program.Print("Task 3 Finished");
        }

        try
        {
            await coordinator.Await();
        }
        catch (Exception ex)
        {
            Program.Print(ex);
            return;
        }

        Program.Print("Done");
    }

    public static void Print(object target) => Console.WriteLine(target);
}

class TaskCoordinator
{
    CancellationTokenSource CancellationSource;
    public CancellationToken CancellationToken => CancellationSource.Token;

    TaskCompletionSource<bool> StartSource;
    TaskCompletionSource<bool> ProcessingSource;

    volatile int HandleCount;
    public Handle CreateHandle()
    {
        Interlocked.Increment(ref HandleCount);

        return new Handle(this);
    }

    public async Task Await()
    {
        StartSource.SetResult(true);

        await ProcessingSource.Task;
    }

    void CompleteHandle()
    {
        if (Interlocked.Decrement(ref HandleCount) is not 0)
            return;

        ProcessingSource.TrySetResult(true);
    }
    void FailHandle(Exception exception)
    {
        try
        {
            Program.Print("Previous Line");

            var output = ProcessingSource.TrySetException(exception);

            //Anything bellow here doesn't execute

            Program.Print("Next Line");

            if (output is false)
                return;

            CancellationSource.Cancel();
        }
        catch (Exception)
        {
            Program.Print("Catch Exception");
        }
        finally
        {
            Program.Print("Finally Block");
        }
    }

    public void Register(Func<Handle, Task> function)
    {
        var handle = CreateHandle();
        var task = function(handle);

        Register(task);
    }
    public async void Register(Task task)
    {
        try
        {
            await StartSource.Task;

            await task;

            CompleteHandle();
        }
        catch (OperationCanceledException) when (CancellationToken.IsCancellationRequested)
        {
            //Do Nothing
        }
        catch (Exception ex)
        {
            FailHandle(ex);
        }
    }

    public TaskCoordinator()
    {
        CancellationSource = new CancellationTokenSource();

        StartSource = new();
        ProcessingSource = new();
    }

    public struct Handle
    {
        readonly TaskCoordinator Coordinator;
        public CancellationToken CancellationToken => Coordinator.CancellationToken;

        public bool IsCancellationRequested => CancellationToken.IsCancellationRequested;
        public void ThrowIfCancellationRequested() => CancellationToken.ThrowIfCancellationRequested();

        public void Fail()
        {
            var exception = new Exception($"Handle Failed");

            Fail(exception);
        }
        public void Fail(Exception exception)
        {
            Coordinator.FailHandle(exception);
        }

        public Handle(TaskCoordinator Coordinator)
        {
            this.Coordinator = Coordinator;
        }
    }
}

The problem occurs inside the method TaskCoordinator.FailHandle, the issue is rather strange, simply put; any code after the ProcessingSource.TrySetException(exception) line doesn't work, including code inside the catch and finally blocks.
This only occurs when the bugged flag is set to true inside the Program.Main function.
The bugged flag will only change the behaviour of using the Program.Use method; the bug only occurs when the returned task is awaited, and it doesn't occur when the task is forgotten. I'm not sure if this is tied directly to the bug, but it's just an observation I made.
Even breakpoints on any line after ProcessingSource.TrySetException(exception) do not work.

Expected behavior

Code inside TaskCoordinator.FailHandle runs normally, respecting the try-catch-finally blocks.

Actual behavior

Execution of TaskCoordinator.FailHandle stops at ProcessingSource.TrySetException(exception) when the bugged flag is set to true inside of 'Program.Main'

Regression?

The code was originally made to be used in the Unity game engine, but I was first prototyping it in an empty .NET project.
The code is bugged using .NET 9 & .NET 8.
But it works as expected under Unity, which uses some version of Mono, of course.
I also tested the code on dotnetfiddle.net to ensure that it isn't just my dotnet installation that is broken, and yes, it's still bugged.

Known Workarounds

None.

Configuration

.NET: 9.0.203
OS: Windows 11 23H2
Arch: x64
I'm unsure if it's specific to this configuration

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.Threading.TasksquestionAnswer questions and provide assistance, not an issue with source code or documentation.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions