Search code examples
c#wpfasync-awaitdeadlocksemaphore

Semaphore causing deadlock on WPF


DoAsync, ConnectFtpAsync, ConnectDbAsync all works on Console project when the number of tasks exceeds the semaphore's limit.
However ConnectFtpAsync and ConnectDbAsync except DoAsync cause WPF project to freeze when the number of tasks exceeds the semaphore's limit.

ButtonPressedAsync() which is the outermost call doesn't use the ConfigureAwait(false) and ConfigureAwait(false) used at inner call shouldn't matter.

Removing the ConfigureAwait(false) from inner calls didn't solve the problem.
Removing semaphore or not exceeding the limit of semaphore solved the problem.

FluentFTP and Oracle is used for the given code.
3 examples are tested separately.

  1. Why do ConnectFtpAsync and ConnectDbAsync freeze WPF project?
  2. Why DoAsync doesn't freeze WPF project?

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        await ButtonPressedAsync();
    }
    
    private static async Task ButtonPressedAsync()
    {
        try
        {
            var connectionLimit = 4;
            var smph = new Semaphore(connectionLimit, connectionLimit);
            var tasks = new List<Task>();
    
            for (var i = 0; i < connectionLimit + 1; ++i)
                tasks.Add(DoAsync(smph));
    
            //for (var i = 0; i < connectionLimit + 1; ++i)
            //    tasks.Add(ConnectFtpAsync(smph));
    
            //for (var i = 0; i < connectionLimit + 1; ++i)
            //    tasks.Add(ConnectDbAsync(smph));
    
            await Task.WhenAll(tasks).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            ;
        }
    }
    
    private static async Task DoAsync(Semaphore smph)
    {
        smph.WaitOne();
    
        await Task.Delay(500).ConfigureAwait(false);
    
        smph.Release();
    }
    
    private static async Task ConnectFtpAsync(Semaphore smph)
    {
        smph.WaitOne();
    
        var ftpConnection = new AsyncFtpClient(
            host: "ip",
            port: 21,
            user: "id",
            pass: "pswd");
    
        await ftpConnection.Connect().ConfigureAwait(false);
    
        smph.Release();
    }
    
    private static async Task ConnectDbAsync(Semaphore smph)
    {
        smph.WaitOne();
    
        var credential = "credential";
        using var dbConnection = new OracleConnection(credential);
        await dbConnection.OpenAsync().ConfigureAwait(false);
        await dbConnection.CloseAsync().ConfigureAwait(false);
    
        smph.Release();
    }


Solution

  • Semaphore and other kernel events are not really compatible with async because they completely block execution.

    So what you are getting is a classic Async Deadlock, because the code is being run on the UI thread and locking up waiting for the semaphore. This happens primarily in GUI apps such as WPF, rather than console apps where there is normally no synchronization context.

    This can be avoided using ConfigureAwait(false), but you need to ensure that that is used all the way down the stack, which in the case of external libraries is hard to ensure.

    The real answer is to never block on async code. You need a wait event that can suspend execution via async, such as SempahoreSlim.

    Note also that Release should be called in a finally to ensure it always gets called even in the event of an exception.

    private static async Task ButtonPressedAsync()
    {
        try
        {
            var connectionLimit = 4;
            using var smph = new SemaphoreSlim(connectionLimit, connectionLimit);
            var tasks = new List<Task>();
    
            for (var i = 0; i < connectionLimit + 1; ++i)
                tasks.Add(DoAsync(smph));
    
            //for (var i = 0; i < connectionLimit + 1; ++i)
            //    tasks.Add(ConnectFtpAsync(smph));
    
            //for (var i = 0; i < connectionLimit + 1; ++i)
            //    tasks.Add(ConnectDbAsync(smph));
    
            await Task.WhenAll(tasks).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            ;
        }
    }
    
    private static async Task DoAsync(SemaphoreSlim smph)
    {
        await smph.WaitAsync().ConfigureAwait(false);
        try
        {
            await Task.Delay(500).ConfigureAwait(false);
            // do other stuff
        }
        finally
        {
            smph.Release();
        }
    }
    
    private static async Task ConnectFtpAsync(Semaphore smph)
    {
        await smph.WaitAsync().ConfigureAwait(false);
        try
        {
            // make sure to dispose your connection
            using var ftpConnection = new AsyncFtpClient(
                host: "ip",
                port: 21,
                user: "id",
                pass: "pswd");
    
            await ftpConnection.Connect().ConfigureAwait(false);
            // do stuff with FTP connection
        }
        finally
        {
            smph.Release();
        }
    }
    
    private static async Task ConnectDbAsync(Semaphore smph)
    {
        await smph.WaitAsync().ConfigureAwait(false);
        try
        {
            var credential = "credential";
            await using var dbConnection = new OracleConnection(credential);
            await dbConnection.OpenAsync().ConfigureAwait(false);
        }
        finally
        {
            smph.Release();
        }
    }