【问题标题】:How to use Task.WhenAny with ReadLineAsync to get data from any TcpClient如何使用 Task.WhenAny 和 ReadLineAsync 从任何 TcpClient 获取数据
【发布时间】:2017-02-08 14:49:21
【问题描述】:

通过所有异步/等待(来自线程池),我遇到了一个有趣的挑战。

我有一个在 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";
}

此函数遍历客户端列表并创建所有ReadLineAsync() 任务的List&lt;Tast&lt;string&gt;&gt;

在任何给定时间,我可能只有 1 或 2 个客户端实际发送数据,所以我不能 WhenAll() 并且我尝试了 WhenAny()WaitAny() 没有成功。

未来的 Google 员工注意事项:WaitAny()Wait() 类似,并且处于阻塞状态。不要在 UI 线程上执行此操作。而是使用 WhenAny() 并等待它。

所以我目前的实现是可行的,但我无法弄清楚如果其他客户端不发送数据,消息将从 1 个客户端建立的这个错误。

TL;DR:我是否正确使用了WhenAny(),还是有更好的方法让我等待 ReadLineAsync 并将结果传递给文本框?

编辑:这是我看到的行为 我按以下顺序输入:左、右、左 2、右 2、左 3、右 3 似乎有些消息正在被丢弃?

编辑2:我在MSDN博客上找到了我复制的代码sn-p的来源:https://blogs.msdn.microsoft.com/pfxteam/2012/08/02/processing-tasks-as-they-complete/

此代码 sn-p 专门用于遍历任务列表以确保它们全部完成。我不在乎任务是否重复,所以我需要修改代码以始终检查整个任务列表,而不是删除任何任务。

【问题讨论】:

  • 您没有删除运行完成的任务..?
  • 天哪,因为 return 语句绕过了 remove 语句......哇。让我试一试。
  • @stuartd 好吧,这并没有修复问题,但它确实告诉我,有时 2 个或更多 ReadLineAsync 被组合在一起。但是,似乎某些消息从一个客户端延迟到另一个客户端发送消息。
  • 我认为这可能是因为tasklist 可以减少到只有 1 个任务,此时它的功能就好像它只是在等待那个任务

标签: c# wpf asynchronous task-parallel-library


【解决方案1】:

好像有些消息被丢弃了?

是的。因为异步工作在您调用他们的方法时开始(例如,ReadLineAsync)。当其中一个任务完成时 (Task.WhenAny),您的代码将放弃其他任务。但它们继续运行——它们仍在从它们的套接字中读取,而它们读取的任何内容都将被丢弃。

AFAIK,当您开始从同一个套接字读取时,行为未定义再次 - 它可能会读取接下来的内容,或者可能会排队。我知道你不应该同时从一个套接字(或任何流)发出多个读取。

套接字与async 并不完美匹配,因为它们可以随时发送数据。您应该改用 Rx 或事件。可以使async 工作,但它非常复杂。

【讨论】:

  • 首先,您的网站对我帮助很大!谢谢你。现在我实际上想出了如何在没有WhenAny() 或访问流阅读器的情况下做到这一点。相反,我使用 TcpClient 的 NetworkStream 并简单地检查stream.DataAvailable,如果没有数据则提前返回。更简单,更清洁。就行为而言,它实际上会引发异常,前提是您没有触发并忘记任务。我将发布我如何实现它的答案。感谢您的建议和帮助!
【解决方案2】:

好的,所以我弄清楚了哪里出了问题以及为什么我以前的代码不起作用。

首先,让我们谈谈这段代码的作用,以及为什么它不起作用:

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 时,我们等待 await 任何 ReadLineAsync 函数。

3) 当我们点击一​​个已经完成的任务时,我们返回它的字符串,然后退出函数

4) 任何未完成的剩余 ReadLineAsync 函数仍在运行,但我们丢失了对其实例的引用。

5) 这个函数循环,完成后立即调用AllReadAsync()

6) 这导致我们在第 4 步仍在等待 StreamReady 时尝试访问它 - 从而引发异常。


使用异步 TCP 服务器处理多个 TcpClients

由于这种结构,我无法想出在我的应用程序中使用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 应用程序中处理多个 TcpClients 的方式。

【讨论】:

  • 当socket中没有数据时为什么不立即返回呢?您可以改用 Task.FromResult() 。还有——这样一来,你不是一一检查而不是一一检查吗?
  • 当我立即返回时,我锁定了 UI,因此添加延迟可以防止这种情况发生。我还没有看到Task.FromResult(),所以我必须调查一下!而且我还没有并行检查,因为我试图保持简单并在使代码复杂化之前工作。如果您想创建一个伪代码示例,那将很有帮助!否则,我将阅读更多内容并发布另一个答案!谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-09
  • 1970-01-01
  • 2015-08-04
  • 1970-01-01
  • 2021-12-19
相关资源
最近更新 更多