通过async / await(来自线程池)完成所有工作并且我遇到了一个有趣的挑战。
我有一个在WPF应用程序中运行的TCP服务器,它接受客户端并将它们存储在List<>
中,如下所示:
private List<Client> clients = new List<Client>();
while (running && clientCount <= maxClients)
{
Client client = new Client(await server.AcceptTcpClientAsync());
await client.WriteLineAsync("Connected to the Server!");
clients.Add(client);
clientCount++;
}
所以我想要做的是遍历我的客户端列表,如果收到任何数据,我想将它附加到文本框中。我意识到这可能不是最好的方式来实现这一目标,而且我愿意接受建议,但这就是我目前的结构。
按钮启动循环并不断调用并等待AllReadLineAsync()
private async void btnStartReadLoopClick(object sender, RoutedEventArgs e)
{
btnStartReadLoop.IsEnabled = false;
while(server.clientCount > 0)
{
string text = await server.AllReadLineAsync();
txtOutputLog.AppendText("[client] " + text + "\n");
}
}
这是这个功能:
public async Task<string> AllReadLineAsync()
{
var tasklist = new List<Task<string>>();
foreach (var client in clients)
tasklist.Add(client.ReadLineAsync());
while (tasklist.Count > 0)
{
Task<string> finishedTask = await Task.WhenAny(tasklist);
if (finishedTask.Status == TaskStatus.RanToCompletion)
return await finishedTask;
tasklist.Remove(finishedTask);
}
return "Error: No task finished";
}
此函数遍历客户端列表并创建所有List<Tast<string>>
任务的ReadLineAsync()
。
在任何时候,我可能只有1或2个客户实际发送数据,所以我不能WhenAll()
我已尝试WhenAny()
和WaitAny()
没有成功。
请注意未来的google: WaitAny()
与Wait()
类似,并且正在阻止。不要在UI线程上执行此操作。而是使用WhenAny()
并等待它。
所以我目前的实现方式很有效,但我无法弄清楚如果其他客户端不发送数据,那么从1个客户端建立消息的错误。
TL; DR:我是否正确使用WhenAny()
,还是有更好的方法让我等待ReadLineAsync并将结果传递给文本框?
编辑:以下是我所看到的行为 我输入的顺序是:左,右,左2,右2,左3,右3 好像有些消息被删除了?
编辑2 :我找到了我在MSDN博客上复制的代码段的来源:https://blogs.msdn.microsoft.com/pfxteam/2012/08/02/processing-tasks-as-they-complete/
此代码段专门用于遍历任务列表,确保完成所有任务。我不在乎任务是否重复,所以我需要修改代码以始终检查整个任务列表而不是删除任何任务。
答案 0 :(得分:2)
看起来好像有些邮件被删除了?
是。因为当您调用其方法时,异步工作已启动(例如,ReadLineAsync
)。当其中一个完成(Task.WhenAny
)时,您的代码将放弃其他任务。但是他们继续跑步 - 他们仍在从他们的插座中读书,而他们所阅读的一切都将被扔掉。
AFAIK,当您再次从同一个套接字开始阅读时,行为未定义 - 它可能会读取接下来的内容,或者它可能是可能会排队。我知道你不应该同时从套接字(或任何流)发出多次读取。
套接字与async
不完全匹配,因为可以随时发送数据。您应该使用Rx或事件。可以使async
工作,但它非常复杂。
答案 1 :(得分:1)
好吧,所以我弄清楚我哪里出错了,为什么我以前的代码不起作用。
首先,让我们谈谈这段代码的作用,以及它为什么不起作用:
public async Task<string> AllReadLineAsync()
{
var tasklist = new List<Task<string>>();
foreach (var client in clients)
tasklist.Add(client.ReadLineAsync());
while (tasklist.Count > 0)
{
Task<string> finishedTask = await Task.WhenAny(tasklist);
if (finishedTask.Status == TaskStatus.RanToCompletion)
return await finishedTask;
tasklist.Remove(finishedTask);
}
return "Error: No task finished";
}
1)创建await ReadLineAsync()
2)当该列表的大小大于0时,我们等待等待任何ReadLineAsync
函数。
3)当我们点击已完成的任务时,我们返回它的字符串,然后退出函数
4)任何未完成的剩余ReadLineAsync
函数仍在运行,但我们丢失了对其实例的引用。
5)此函数循环,在完成后立即调用AllReadAsync()
。
6)这导致我们尝试访问StreamReady,同时仍在等待第4步 - 因此抛出一个激活。
由于这种结构,我无法想出在我的应用程序中使用WhenAny()
的方法。相反,我将此功能添加到我的客户端类:
public async Task<string> CheckForDataAsync()
{
if (!stream.DataAvailable)
{
await Task.Delay(5);
return "";
}
return await reader.ReadLineAsync();
}
我们不是等待ReadLineAsync()
,而是从TcpClient访问NetworkStream,我们检查是否有可用的数据if(!stream.DataAvailable)
,如果没有,我们会提前返回一个空字符串,否则我们await ReadLineAsync()
因为我们知道我们有传入的数据,我们希望收到整行。
然后我们替换我所谈到的第一个函数,AllReadLineAsync()
使用以下内容:
public async Task<string> AllReadLineAsync()
{
string data = "", packet = "";
foreach (var client in clients)
{
data = await client.CheckForDataAsync();
if (data != string.Empty)
packet += string.Format($"[client] {data}\n");
}
return packet;
}
这比我之前尝试的方式更简单。现在,它在for循环中遍历所有客户端,并在每个客户端上调用CheckForDataAsync()
函数。由于此函数提前返回而不是无限等待完整ReadLineAsync()
,因此在AllReadLineAysnc()
函数结束后它不会继续在后台运行。
在我们完成所有客户端的循环后,我们获取字符串数据包并将其返回到UI上下文,然后我们可以将数据添加到文本框中:
private async void RecvData(object sender, RoutedEventArgs e)
{
while(server.hasClient)
{
string text = await server.AllReadLineAsync();
txtOutputLog.AppendText(text);
}
}
就是这样。这就是我在WPF应用程序中处理多个TcpClient的方式。