Home > Mobile >  TaskCompletionSource deadlock while using SocketAsyncEventArgs
TaskCompletionSource deadlock while using SocketAsyncEventArgs

Time:08-24

I have simple console TCP server app, and I am trying to make it async. For some reason I am getting deadlock. I run Test-NetConnection localhost -Port 12345 from PowerShell to trigger incoming connection.

I have an assumption: OnAcceptCompleted method is getting called inside Thread spawned by OS, and it is not part of ThreadPool. But I don't know how to fix it.

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    private static CancellationTokenSource _shutdownSource = new CancellationTokenSource();

    static async Task Main()
    {
        using var listener = new Socket(SocketType.Stream, ProtocolType.Tcp);
        listener.Bind(new IPEndPoint(IPAddress.Any, 12345));
        listener.Listen(10);

        while (!_shutdownSource.IsCancellationRequested)
        {
            var socket = await AcceptClient(listener, _shutdownSource.Token).ConfigureAwait(false);
            // execution never go this line
            Console.WriteLine($"Client accepted {socket.RemoteEndPoint}");
        }
    }

    private static async Task<Socket> AcceptClient(Socket listener, CancellationToken token)
    {
        var source = new TaskCompletionSource<Socket>();
        var args = new SocketAsyncEventArgs();
        args.UserToken = new TaskCompletionSource<Socket>();
        args.Completed  = OnAcceptCompleted;
        if (!listener.AcceptAsync(args))
        {
            OnAcceptCompleted(listener, args);
        }

        using (token.Register(() => source.TrySetCanceled()))
        {
            // we don't need ConfigureAwait(false) here, but just to be sure
            return await source.Task.ConfigureAwait(false);
        }
    }

    private static void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
    {
        var source = (TaskCompletionSource<Socket>)args.UserToken;
        if (args.SocketError == SocketError.Success)
        {
            // checked in debugger, TaskCompletionSource goes to RanToCompletion status here
            source.TrySetResult(args.AcceptSocket);
        }
        else if (args.SocketError == SocketError.OperationAborted)
        {
            source.TrySetCanceled();
        }
        else
        {
            source.TrySetException(new InvalidOperationException("Socket error = "   args.SocketError));
        }
    }
}

CodePudding user response:

I don't understand why you need the bottom two methods. Why is this not acceptable?

static async Task Main()
    {
        using var listener = new Socket(SocketType.Stream, ProtocolType.Tcp);
        listener.Bind(new IPEndPoint(IPAddress.Any, 12345));
        listener.Listen(10);

        while (!_shutdownSource.IsCancellationRequested)
        {
            var socket = await listener.AcceptAsync(_shutdownSource.Token);
            Console.WriteLine($"Client accepted {socket.RemoteEndPoint}");
        }
    }


CodePudding user response:

As you rightly noted, you are using two different TaskCompletionSource objects, when they should be the same. So the source object was never completing.

However, working with raw sockets is fraught with difficulties, and you should instead use TcpListener, which handles all this for you

class Program
{
    private static CancellationTokenSource _shutdownSource = new CancellationTokenSource();

    static async Task Main()
    {
        var listener = new TcpListener(IPAddress.Any, 12345);
        try
        {
            listener.Start(10);

            while (!_shutdownSource.IsCancellationRequested)
            {
                var client = await listener.AcceptTcpClientAsync(_shutdownSource.Token).ConfigureAwait(false);
                Console.WriteLine($"Client accepted {client.RemoteEndPoint}");
                // hand off client to another function
                // Task.Run(() => HandleYourClient(client, _shutdownSource.Token));
                // otherwise `client` needs `using` above to dispose
            }
        }
        catch (OperationCanceledException)
        { //
        }
        finally
        {
            if (listener.Active)
                listener.Stop()
        }
    }

    private async Task HandleClient(TcpClient client, CancellationToken token)
    {
        using client;
        using var stream = client.GetStream();
        // do stuff
    }
}

CodePudding user response:

Sorry, my bad. I am creating 2 TaskCompletionSource<Socket> in AcceptClient method. Fixed code:

private static async Task<Socket> AcceptClient(Socket listener, CancellationToken token)
{
    var source = new TaskCompletionSource<Socket>();
    var args = new SocketAsyncEventArgs();
    args.UserToken = source;
    args.Completed  = OnAcceptCompleted;
    if (!listener.AcceptAsync(args))
    {
        OnAcceptCompleted(listener, args);
    }

    using (token.Register(() => source.TrySetCanceled()))
    {
        return await source.Task.ConfigureAwait(false);
    }
}
  • Related