如何使DatagramSocket.MessageReceived与async / await一起使用?

时间:2013-02-08 05:41:21

标签: c# network-programming windows-runtime task-parallel-library async-await

我想用TPL包装以下数据报套接字操作来清理API,以便它与asyncawait很好地协作,就像StreamSocket类一样。< / p>

public static async Task<bool> TestAsync(HostName hostName, string serviceName, byte[] data)
{
    var tcs = new TaskCompletionSource<bool>();
    var socket = new DatagramSocket();
    socket.MessageReceived += (sender, e) =>
    {
        var status = false; // Status value somehow derived from e etc.
        tcs.SetResult(status);
    };
    await socket.ConnectAsync(hostName, serviceName);
    var stream = await socket.GetOutputStreamAsync();
    var writer = new DataWriter(stream);
    writer.WriteBytes(data);
    await writer.StoreAsync();
    return tcs.Task;
}

关键点是MessageReceived事件,它将DatagramSocket类转换为事件异步模式和新async模式的奇怪混搭。无论如何,TaskCompletionSource<T>允许我调整处理程序以符合后者,所以这不是太可怕。

除非端点永远不返回任何数据,否则这似乎工作得很好。与MessageReceived处理程序关联的任务永远不会完成,因此从TestAsync返回的任务永远不会完成。

正确包装此操作以包含超时和取消的正确方法是什么?我想扩展这个函数,为后者提供一个CancellationToken参数,但是我该怎么做呢?我唯一想到的就是使用Task.Delay创建一个额外的“监控”任务,我将超时值和取消令牌传递给它,以支持以下几行中的这两种行为:

public static async Task<bool> CancellableTimeoutableTestAsync(HostName hostName, string serviceName, byte[] data, CancellationToken userToken, int timeout)
{
    var tcs = new TaskCompletionSource<bool>();
    var socket = new DatagramSocket();
    socket.MessageReceived += (sender, e) =>
    {
        var status = false; // Status value somehow derived from e etc.
        tcs.SetResult(status);
    };
    await socket.ConnectAsync(hostName, serviceName);
    var stream = await socket.GetOutputStreamAsync();
    var writer = new DataWriter(stream);
    writer.WriteBytes(data);
    await writer.StoreAsync();

    var delayTask = Task.Delay(timeout, userToken);
    var t1 = delayTask.ContinueWith(t => { /* Do something to tcs to indicate timeout */ }, TaskContinuationOptions.OnlyOnRanToCompletion);
    var t2 = delayTask.ContinueWith(t => { tcs.SetCanceled(); }, TaskContinuationOptions.OnlyOnCanceled);

    return tcs.Task;
}

然而,这有各种各样的问题,包括延迟任务和MessageReceived处理程序之间的潜在竞争条件。我从来没有能够使这种方法可靠地工作,而且它看起来非常复杂以及线程池的低效使用。它很狡猾,容易出错并且伤到我的头脑。

旁注:我是唯一一个对DatagramSocket API感到困惑的人吗?它不仅似乎是IAsyncAction WinRT模型和TPL的丑陋集合,还带有一些棘手的EAP,我对用于表示基本无连接协议(如UDP)的API并不十分满意其中名为ConnectAsync的方法。对我而言,这似乎是一个矛盾。

2 个答案:

答案 0 :(得分:2)

首先,我认为DatagramSocket的接口恰恰是因为UDP的本质。如果您有数据报流,那么事件是表示该数据报的适当方式。 WinRT IAsyncAction(或.Net Task)只能代表拉模型,您可以在其中明确请求每个数据(例如,可能有方法ReadNextDatagramAsync())。这对TCP有意义,因为它有流量控制,所以如果你慢慢读取数据,发送者也会慢慢发送它们。但对于UDP,推送模型(由WinRT和.Net中的事件表示)更有意义。

我同意名称Connect没有100%的意义,但我认为主要是确实有意义,特别是为了使其与StreamSocket更加一致。你需要这样的方法,以便系统可以解析域名并为你的套接字分配一个端口。

对于您的方法,我同意@usr您应该创建一个单独的方法来接收数据报。如果你想将一个异步模型转换为另一个异步模型,同时添加原始模型本身不支持的功能,那将是繁琐的,我认为你无能为力。

如果你正确实现它也不会效率低下:你应确保在Task完成后,MessageReceived事件被取消订阅,与{{1}相关联的计时器处置(你通过取消传递给Delay()的令牌来执行此操作)并且注册了传入的Delay()的委托是未注册的(我认为你应该直接使用Register()而不是(ab)使用CancellationToken

关于比赛条件,当然你必须考虑它们。但有一种相对简单的方法可以解决这个问题:使用Delay()的{​​{1}}方法(例如TrySetResult())。

答案 1 :(得分:0)

超时:启动计时器并使用tcs.TrySetCancelled()完成任务。要取消,请使用cancellationToken.Register注册您也设置为已取消的回调。小心要经常丢弃计时器。

我建议您将计时器逻辑移动到可重用的辅助方法中。这使代码看起来像意大利面条,混合了许多不相关的东西。