2
votes

I’m implementing a client protocol MyProtocol over TCP/IP. The protocol’s Connect() method should have a signature similar to that of TcpClient.ConnectAsync() – that is, it should return a Task:

Task MyProtocol.Connect (…);

This method (MyProtocol.Connect()) should make asynchronous TCP/IP connection (via TcpClient.ConnectAsync()), return a non-completed task T and then periodically send a certain message M to the server – again asynchronously (via NetworkStream.WriteAsync()). When a certain response R is received from the server – again asynchronously (via NetworkStream.ReadAsync()), MyProtocol.Connect() should complete the task T. I’m doing the following:

    // Client of the protocol:
var task = myProtocol.Connect(); // asynchronous call, we don’t want to wait until connected
task.ContinueWith(t =>
{
    // Connected – doing an OnConnected stuff
    …
});

// MyProtocol.Connect() implementation:
public class MyProtocol
{
    private Task connectTask;
    public Task Connect(…)
    {
        var tcpIpConnectTask = mTcpIpProtocol.Connect(…);
        tcpIpConnectTask.ContinueWith(t =>
        {
            connectTask = new Task();
        }
        return connectTask;
    }
}

Periodical sending of message M to the server obviously has to be done through a timer. Once the response R is received from the server asynchronously, connectTask must be marked as completed, and I don’t see ways of doing that. Well, strictly speaking, I have managed to mark connectTask as completed; I’m wrapping it in TaskCompletionSource<bool> and use TaskCompletionSource.SetResult(true). However I’m wondering if this is the only, let alone the best way of accomplishing what I need? I especially don’t like the fact that TaskCompletionSource<> has to have a non-void task result type (I used bool), i.e. there’s no non-generic version.

Until TPL arrived, we had our own similar framework and a method Task.NotifyCompleted(), so we could create a Task in one place, and mark it as completed in another place – all this was working asynchronously. But all I’ve read about tasks in TPL seems to imply that a Task only completes if its delegate runs to the last line… Or am I missing something simple?

2
Why don't you just return tcpIpConnectTask? That one is complete once the connection is complete. Also if you are using .net 4.5 you can use async/await which simplifies the usageNeddySpaghetti
tcpIpConnectTask is essentially what TcpClient.ConnectAsync() returns. As per above, MyProtocol adds its own functionality to TCP/IP, so it can only use tcpIpConnectTask as an intermediate result, it can't return it to the client, because the connection won't be complete when tcpIpConnectTask is complete, as far as MyProtocol is concerned.alexk

2 Answers

3
votes

Since you're already using .Net 4.5, you should use C# 5.0 async-await, which is meant exactly for this kind of situations. The code could look something like (somewhat pseudo-codish):

public Task ConnectAsync()
{
    await ClientConnectAsync();

    while (true)
    {
        await SendMessageAsync();

        var response = await ReceiveMessageAsync();

        if (response == R)
            return;

        await Task.Delay(period);
    }
}

To actually answer your questions:

However I’m wondering if this is the only, let alone the best way of accomplishing what I need?

If you don't want to use async-await, then yes, TaskCompletionSource is the only general-purpose way of doing this.

I especially don’t like the fact that TaskCompletionSource<> has to have a non-void task result type (I used bool), i.e. there’s no non-generic version.

That is somewhat annoying, but it doesn't really matter, since Task<T> inherits from Task.

But all I’ve read about tasks in TPL seems to imply that a Task only completes if its delegate runs to the last line […]

That's true for Tasks that do have a delegate to run (those can be created using Task.Run(), Task.Factory.StartNew() or new Task()). But it doesn't apply to Tasks with no delegate, those can be created using async-await or TaskCompletionSource.

1
votes

I haven't tested this but it might be possible to achieve what you want with several task continuations, and once you receive the desired response set the connectedTask to the task received from the last ReadAsync.

public class MyProtocol
{
    private Task connectTask;
    public Task Connect()
    {
        var tcpIpConnectTask = mTcpIpProtocol.Connect();
        tcpIpConnectTask.ContinueWith(t =>
        {
            bool finished = false;
            Task readAsyncTask = null;

            while(!finsihed)
            {              
                NetworkStream.WriteASync().
                ContinueWith(t1 =>
                {
                     if(t1.Exception == null)
                     {                  
                        readAsyncTask = NetworkStream.ReadASync().
                        ContinueWiht(t2 => 
                        {
                            if(t2.Exception == null)
                            {
                                if(t2.Result == /*Desired Response*/)
                                { 
                                    finished = true;
                                    connectedTask = t2;
                                }
                            }
                            else
                            {   
                                // Handle ReadAsync error
                            }
                        }, null, TaskScheduler.Default);
                    }
                    else
                    {
                       // handle WriteAsync error
                    }
                }, null, TaskScheduler.Default);    

                // Wait for the read operation to complete. This can cause a deadlock
                // when called from a UI thread as Wait() is a blocking call. Ensure we are waiting
                // on a background thread by specifying TaskScheduler.Default
                readAsyncTask.Wait();
            }
        }, null, TaskScheduler.Default);

        return connectTask;
}

Alternatively if you wanted the task to have a particular result you can return Task.FromResult(..) which is also a completed task.