我想用TPL包装以下数据报套接字操作来清理API,以便它与async
和await
很好地协作,就像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
的方法。对我而言,这似乎是一个矛盾。
答案 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
注册您也设置为已取消的回调。小心要经常丢弃计时器。
我建议您将计时器逻辑移动到可重用的辅助方法中。这使代码看起来像意大利面条,混合了许多不相关的东西。