5

我想用 TPL 包装以下数据报套接字操作以清理 API,以便它可以很好地与async和一起使用await,就像StreamSocket类一样。

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;
}

症结在于MessageReceivedDatagramSocket类变成事件异步模式和新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处理程序之间的潜在竞争条件。我从来没有能够让这种方法可靠地工作,而且它看起来非常复杂,而且线程池的使用效率低下。这很繁琐,容易出错,而且让我头疼。

旁注:我是唯一对DatagramSocketAPI 感到困惑的人吗?它不仅看起来是IAsyncActionWinRT 模型和 TPL 与一些棘手的 EAP 的丑陋组合,而且我对旨在表示基本无连接协议(例如包含ConnectAsync在其中命名的方法的 UDP)的 API 不太满意。这对我来说似乎是一个矛盾。

4

2 回答 2

2

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

我同意这个名字Connect没有 100% 的意义,但我认为它主要是有道理的,尤其是让它更符合StreamSocket. 而且您确实需要这样的方法,以便系统可以解析域名并将端口分配给您的套接字。

对于您的方法,我同意@usr 的观点,即您应该创建一个单独的方法来接收数据报。而且,如果您想将一个异步模型转换为另一个,同时添加原始模型本身不支持的功能,那将会很麻烦,我认为您对此无能为力。

如果您正确实施它,它也不会效率低下:您应该确保在Task完成后,MessageReceived取消订阅事件,处理关联的计时器Delay()(您可以通过取消传递给的令牌来实现Delay())和委托用传入的CancellationToken注册是未注册的(我认为你应该Register()直接使用而不是(ab)使用Delay())。

关于比赛条件,您当然必须考虑它们。但是这里有一个相对简单的方法来处理它:使用(eg )的Try方法。TaskCompletionSourceTrySetResult()

于 2013-02-08T12:54:21.710 回答
0

超时:启动一个计时器并用于tcs.TrySetCancelled()完成任务。对于取消cancellationToken.Register,用于注册您也设置为取消的回调。小心处理计时器。

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

于 2013-02-08T11:32:50.477 回答