为什么在使用 ContinueWith 执行任务时 Windows 表单 UI 被阻塞?

Why is the Windows Forms UI blocked when executing Task with ContinueWith?

我花了几天时间在 Google 中搜索并试图理解为什么在我的情况下 Windows 表单 UI 被阻止 Tasks 中执行 ping。 我看到了很多类似的案例,但其中 none 解释了我的具体案例。

问题描述:

我有一个异步发送 ping 的应用程序。每个 ping 都在任务内部发送。我使用 .ContinueWith 接收 ping 的结果并将其打印到文本框而不阻塞 UI 线程。 如果我启动所有 ping 一次就可以正常工作。 如果我添加一个 while {run} 循环使它们永远 运行 我的 UI 变得无响应并被阻止, none 的结果打印到文本框。

有问题的代码:

Action action2 = () => {
        for (int i = 0; i < ipquantity; i++)
        {
            int temp1 = i;
            string ip = listView1.Items[temp1].SubItems[1].Text;

            if (finished[temp1] == true) // Variable helps to check if ping reply was received and printed
                continutask[temp1] = Task<string>.Run(() => PingStart(ip, temp1)).ContinueWith(antecedent => PrintResult(antecedent.Result, temp1));
        }
};

while (run)
{
    action2();
    Thread.Sleep(1000);
}

问题:

  1. 为什么 UI 被 while 循环阻塞,为什么没有它就不会被阻塞?
  2. 我如何修改我的代码以仍然能够在不阻塞 UI 的情况下使用 Tasks 进行 ping?
  3. 有没有更好的方法可以同时向多个 IP 地址发起无限 ping?

完整代码:

private async void buttonStart_Click(object sender, EventArgs e)
{
    run = true;

    int count = listView1.Items.Count;
    task = new Task<string>[count];
    result1 = new string[count];
    finished = new bool[count];
    continutask = new Task[count];
    for (int i = 0; i < count; i++)
    {
        finished[i] = true;
    }

        Action action2 = () =>
    {
        for (int i = 0; i < count; i++)
        {
            int temp1 = i;
            string ip = listView1.Items[temp1].SubItems[1].Text;


            if (finished[temp1] == true)
                continutask[temp1] = Task<string>.Run(() => PingStart(ip, temp1)).ContinueWith(antecedent => PrintResult(antecedent.Result, temp1));

        }
    };
    while (run)
    {
        action2();
        //await Task.Delay;
        //Thread.Sleep(1000);
    }
}

public void PrintResult(string message, int seqnum)
{
    Action action = () =>
    {
        textBox1.AppendText(message);
        textBox1.AppendText(Environment.NewLine);
        textBox1.AppendText("");
        textBox1.AppendText(Environment.NewLine);
    };
    if (InvokeRequired)
        Invoke(action);
    else
        action();
    finished[seqnum] = true;
}

public string PingStart(string ip, int seqnum)
{
    finished[seqnum] = false;

    Ping isPing = new Ping();
    PingReply reply;
    const int timeout = 2000;
    const string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    var buffer = Encoding.ASCII.GetBytes(data);
    PingOptions options = new PingOptions();
    // Use the default Ttl value which is 128,
    options.DontFragment = false;

    reply = isPing.Send(ip, timeout, buffer, options);
    string rtt = (reply.RoundtripTime.ToString());

    string success = "N/A";

    if (reply.Status == IPStatus.Success)
    {
        success = $"{ip}" + " Success!" + $" rtt: [{rtt}]" + $"Thread: {Thread.CurrentThread.GetHashCode()} Is pool thread: {Thread.CurrentThread.IsThreadPoolThread}";
    }
    else if (reply.Status != IPStatus.Success)
    {
        success = $"{ip}" + $" Not Successful! Status: {reply.Status}" + $"Thread: {Thread.CurrentThread.GetHashCode()} Is pool thread: {Thread.CurrentThread.IsThreadPoolThread}";
    }

    return success;
}

Thread.Sleep(n) 阻塞当前线程 n 毫秒。如果我正确理解代码,它会执行 action2 然后暂停 calling 线程一秒钟。如果该线程 主 (UI) 线程,您的 UI 将被阻塞。

也许将 while 循环移动到另一个线程可以解决问题。

由于您已经创建(并保存)了任务,最简单的解决方法是在 while 循环的每次迭代中等待它们:

while (run)
{
    action2();

    foreach (Task t in continutask)
        await t;
}

这样,当所有 ping 完成(成功与否)后,您将立即重新开始整个过程​​。

还有一件事:您可以将 textBox1.ScrollToEnd(); 添加到 PrintResult


由于还有很大的改进空间,下面是一个重写和简化的例子。我删除了许多未使用的变量(例如 seqnum)并使 PingStart 方法完全异步。我还将您的 ListBox 替换为 TextBox 以便于测试,因此您可能希望在代码中还原它。

这仍然不是所有可能实现中最干净(主要是因为全局run)但它应该向您展示如何做事"more async" :)

private async void buttonStart_Click(object sender, EventArgs e)
{
    // If the ping loops are already running, don't start them again
    if (run)
        return;

    run = true;

    // Get all IPs (in my case from a TextBox instead of a ListBox
    string[] ips = txtIPs.Text.Split(new[] {"\r\n"}, StringSplitOptions.RemoveEmptyEntries);

    // Create an array to store all Tasks
    Task[] pingTasks = new Task[ips.Length];

    // Loop through all IPs
    for(int i = 0; i < ips.Length; i++)
    {
        string ip = ips[i];

        // Launch and store a task for each IP
        pingTasks[i] = Task.Run(async () =>
            {
                // while run is true, ping over and over again
                while (run)
                {
                    // Ping IP and wait for result (instead of storing it an a global array)
                    var result = await PingStart(ip);

                    // Print the result (here I removed seqnum)
                    PrintResult(result.Item2);

                    // This line is optional. 
                    // If you want to blast pings without delay, 
                    // you can remove it
                    await Task.Delay(1000);
                }
            }
        );
    }

    // Wait for all loops to end after setting run = false.
    // You could add a mechanism to call isPing.SendAsyncCancel() instead of waiting after setting run = false
    foreach (Task pingTask in pingTasks)
        await pingTask;
}

// (very) simplified explanation of changes:
// async = this method is async (and therefore awaitable)
// Task<> = This async method returns a result of type ...
// Tuple<bool, string> = A generic combination of a bool and a string
// (-)int seqnum = wasn't used so I removed it
private async Task<Tuple<bool, string>> PingStart(string ip)
{
    Ping isPing = new Ping();
    const int timeout = 2000;
    const string data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    var buffer = Encoding.ASCII.GetBytes(data);
    PingOptions options = new PingOptions {DontFragment = false};

    // await SendPingAsync = Ping and wait without blocking
    PingReply reply = await isPing.SendPingAsync(ip, timeout, buffer, options);
    string rtt = reply.RoundtripTime.ToString();

    bool success = reply.Status == IPStatus.Success;
    string text;

    if (success)
    {
        text = $"{ip}" + " Success!" + $" rtt: [{rtt}]" + $"Thread: {Thread.CurrentThread.GetHashCode()} Is pool thread: {Thread.CurrentThread.IsThreadPoolThread}";
    }
    else
    {
        text = $"{ip}" + $" Not Successful! Status: {reply.Status}" + $"Thread: {Thread.CurrentThread.GetHashCode()} Is pool thread: {Thread.CurrentThread.IsThreadPoolThread}";
    }

    // return if the ping was successful and the status message
    return new Tuple<bool, string>(success, text);
}

这样,您将为每个 IP 建立一个循环,该循环将彼此独立地继续,直到 run 设置为 false