我需要延迟向用户显示信息。例如,每秒更改文本标签的内容。(或输出长时间计算的中间结果。)在命令行程序中,我这样做了:
Console.WriteLine("значение 1");
Thread.Sleep(1000);
Console.WriteLine("значение 2");
Thread.Sleep(1000);
Console.WriteLine("значение 3");
有效。现在我需要在图形程序中做同样的事情。我写了一个方法
void OnClick(object sender, EventArgs args)
{
label.Text = "значение 1";
Thread.Sleep(1000);
label.Text = "значение 2";
Thread.Sleep(1000);
label.Text = "значение 3";
}
但它不是那样工作的。中间值不显示,程序长时间停止响应。当它挂起时,它会立即显示最后一个值。
发生了什么?为什么程序运行不正确,如何正确运行?
图形程序与控制台程序的不同之处在于主线程做很多事情。在控制台程序中,我们有完全的控制权,我们完全控制它的里程数。在图形程序中,我们启动应用程序,框架为我们创建一个消息循环。在这个循环中,框架处理鼠标移动、按键、窗口大小调整、定时器回调等,并且还调用我们的事件处理程序,每次循环迭代一次(好吧,这是一个简化的图片,但为了这个演示文稿它会做)。循环迭代完成后,执行进入下一次迭代。
所有这些都在同一个线程上运行,该线程称为 UI 线程。
现在,如果我们在 UI 线程上执行会发生什么
Thread.Sleep(1000)?事情是这样的:线程被阻塞并且整整一秒什么都不做。就在这一秒,我们的消息循环处于空闲状态,因为执行线程被我们阻塞了!这一刻,窗口消息没有被处理,没有鼠标响应发生,没有回调被调用,甚至没有重绘窗口内容——毕竟,所有这些都是在我们阻止的同一个消息循环中完成的!为使程序正常运行,我们的事件处理程序(如
OnClick)、对象构造函数以及通常在 UI 线程上运行的所有代码都必须尽可能快地运行,没有延迟。如何暂停一秒钟?幸运的是,在该语言的现代版本中(自 C# 5 起)有一个简单的解决方案。这是异步/等待。让我们的处理程序异步(关键字
async),并将其替换Thread.Sleep为await Task.Delay:此方法工作正常!¹
发生了什么?问题是
await Task.Delay等待时间不会阻塞流程。在等待的过程中,方法似乎停止执行,消息循环不再阻塞。[小心,它可能在其他地方被阻塞。]当等待结束时,消息循环从它停止的地方恢复方法,到下一个,await或者到方法的末尾。²因此,我们的代码不再阻塞 UI 线程,框架可以继续绘制窗口并执行其他内务处理任务。
但是,如果您需要执行一些计算而不是延迟怎么办?它们不是那么容易从函数流中切出的,它们仍然必须被执行。出于这些目的,可以将它们卸载到另一个流中。不要害怕,这很简单。而不是代码
你这样写:
Task.Run在后台线程上执行您的代码,并且该函数在此执行期间不会再次阻塞 UI 线程。³获利!请注意,您不能从后台线程读取控件的值,因此必须提前读取它们:它是:
它变成了:
在旧版本的语言中,没有 async/await,你必须以更复杂的方式实现同样的事情。例如,启动一个计时器,订阅它的滴答声,并更改它们的控件中的值。同时,必须将局部变量移至类字段(或移至特殊的结构上下文)。或者你可以用
DoEvents. 幸运的是,那些糟糕的过去早已一去不复返了。相关问题:
¹ 但是对于其他异步方法,不是事件处理程序,我们需要返回 not
void,而是返回Task一些Task<string>,以便调用代码可以等待它们完成并获得结果。² 陈述过于简单化,所以不要把它当作最终的真理。这是一个大概的图,如果你想知道确切的图,最好看书或者文档。或者问一个问题,如果有什么东西表现得难以理解。
³ 如果您需要在后台线程上执行大量长时间运行的工作,那么完全卸载这些工作并通过
Progress<T>.作为答案的例证
VladD。任务: 组织具有以下条件的方法的执行:
解决方案:
为了实现第1点,我们将使用
async/await和Task.Run()。要执行第2点,我们将使用一个实例
Progress<T>为完成第3步,我们将使用所谓的取消令牌,该令牌将由 提供给我们
CancellationTokenSource。我们应该这样:
让我们创建一个实用程序类,它将有一个以 100 毫秒的延迟发送的方法。从 0 到 1000 的连续数字。
这是我们进一步的解决方案
PS 在 WPF 和 MVVM 的情况下相同的例子
这是 XAML
这是视图模型
Thread.Sleep 阻塞整个线程。包括处理 UI 的整个线程(如果在那里调用)。
Лично я бы сказал бы что на это есть несколько решений.
Решение №1: async await (советую использовать именно его)
Его расписал Влад, но я повторюсь и распишу его немного короче чисто что бы люди не мотались туда-назад:
Решение №2: использование делегатов (не лучшее решение даной проблемы, но лучше с ним ознакомится)
Суть в чем: в UI потоке ты создаешь делегат который будет делать что-то нужное. Например, обновлять в UI потоке некий progressBar который будет показывать на сколько процентов скачался большой файл. Собственно, сама скачка должна воспроизводится в другом потоке (создай новый тред) или в таске.
На что нужно обратить внимание: используй
BeginInvoke(делегат);(запускает метод по ссылке делегата в главном потоке) а неделегат.Invoke();(запускает метод по ссылке делегата в этом же потоке).也可以在不声明特定委托实例的情况下完成。KG在他的回答中展示了旧版本语言如何处理任务的相同示例。事实上,他准确地描述了路径的这种变体,只是他没有声明任何具体的委托:
解决方案 #3:实施 MVC 或 MVP 模式之一或任何其他类似模式。(一个非常正确的解决方案,但更难实施和繁琐)
思路是:UI对程序的实现一无所知。一切都与 UI 分开完成。而实际上,逻辑的实现是通过一层事件与UI进行通信的。