为什么在使用 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);
}
问题:
- 为什么 UI 被
while
循环阻塞,为什么没有它就不会被阻塞?
- 我如何修改我的代码以仍然能够在不阻塞 UI 的情况下使用 Tasks 进行 ping?
- 有没有更好的方法可以同时向多个 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
。
我花了几天时间在 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);
}
问题:
- 为什么 UI 被
while
循环阻塞,为什么没有它就不会被阻塞? - 我如何修改我的代码以仍然能够在不阻塞 UI 的情况下使用 Tasks 进行 ping?
- 有没有更好的方法可以同时向多个 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
。