睡眠任务(System.Threading.Tasks)

本文关键字:Threading Tasks System 任务 | 更新日期: 2023-09-27 18:30:52

我需要创建线程来替换Windows窗体窗口中的照片,而不是等待~1秒并恢复上一张照片。

我认为以下代码:

TaskScheduler ui = TaskScheduler.FromCurrentSynchronizationContext();
var task = Task.Factory.StartNew(() =>
{
    pic.Image = Properties.Resources.NEXT;
    Thread.Sleep(1000);
    pic.Image = Properties.Resources.PREV;
}, CancellationToken.None, TaskCreationOptions.LongRunning, ui)

做这项工作,但不幸的是没有。它冻结主 UI 线程。

这是因为不能保证每个任务有一个线程。一个线程可用于处理多个任务。甚至 TaskCreationOptions.LongRun 选项也无济于事。

我该如何解决?

睡眠任务(System.Threading.Tasks)

Thread.Sleep 是一个同步延迟。如果需要异步延迟,请使用 Task.Delay

在目前处于测试版的 C# 5 中,您可以简单地说

await Task.Delay(whatever);

在异步方法中,该方法将自动从中断的位置继续。

如果您没有使用 C# 5,那么您可以"手动"设置您想要的任何代码作为延迟的延续。

当您传递来自当前同步上下文的新任务计划程序时,您实际上告诉该任务在 UI 线程上运行。 您实际上想要这样做,因此您可以更新 UI 组件,但是您不想在该线程上睡觉,因为它会阻塞。

这是.ContinueWith理想的一个很好的例子:

TaskScheduler ui = TaskScheduler.FromCurrentSynchronizationContext();
var task = Task.Factory.StartNew(() =>
                                     {
                                         pic.Image = Properties.Resources.NEXT;
                                     },
                                 CancellationToken.None,
                                 TaskCreationOptions.None,
                                 ui);
task.ContinueWith(t => Thread.Sleep(1000), TaskScheduler.Default)
    .ContinueWith(t =>
                      {
                          pic.Image = Properties.Resources.Prev;
                      }, ui);

编辑(删除了一些东西并添加了这个):

发生的情况是,我们阻止 UI 线程的时间只够更新pic.Image。 通过指定TaskScheduler,您可以告诉它要在哪个线程上运行任务。 重要的是要知道任务和线程之间的关系不是 1:1。 事实上,你可以在相对较少的线程上运行 1000 个任务,甚至 10 个或更少,这完全取决于每个任务的工作量。 不要假设您创建的每个任务都将在单独的线程上运行。 CLR 在自动平衡性能方面做得很好。

现在,您不必使用 默认TaskScheduler ,如您所见。 当您传递 UI TaskScheduler 时,即TaskScheduler.FromCurrentSynchronizationContext() ,它使用 UI 线程而不是线程池,就像TaskScheduler.Default一样。

记住这一点,让我们再次查看代码:

var task = Task.Factory.StartNew(() =>
                                     {
                                         pic.Image = Properties.Resources.NEXT;
                                     },
                                 CancellationToken.None,
                                 TaskCreationOptions.None,
                                 ui);

在这里,我们将创建并启动一个将在 UI 线程上运行的任务,该任务将使用资源更新 picImage 属性。 执行此操作时,UI 将无响应。 幸运的是,这可能是一个非常快速的操作,用户甚至不会注意到。

task.ContinueWith(t => Thread.Sleep(1000), TaskScheduler.Default)
    .ContinueWith(t =>
                      {
                          pic.Image = Properties.Resources.Prev;
                      }, ui);

使用此代码,我们调用 ContinueWith 方法。 它完全符合它的声音。 它返回一个新的 Task 对象,该对象将在运行时执行 lambda 参数。 当任务完成、出错或被取消时,它将启动。 您可以通过传入 TaskContinuationOptions 来控制它何时运行。 但是,我们也像以前一样传递了不同的任务计划程序。 这是默认任务计划程序,它将在线程池线程上执行任务,因此不会阻止 UI。 此任务可能会运行数小时,并且您的 UI 将保持响应(不要让它),因为它是与您与之交互的 UI 线程不同的线程。

我们还对设置为在默认任务计划程序上运行的任务调用了ContinueWith。 这是将再次更新 UI 线程上的图像的任务,因为我们已将相同的 UI 任务计划程序传递给正在执行的任务。 线程池任务完成后,它将在 UI 线程上调用此任务,在更新映像时将其阻止很短的时间。

您应该在将来的某个时候使用 Timer 来执行 UI 任务。 只需将其设置为运行一次,间隔为 1 秒即可。 将 UI 代码放在 tick 事件中,然后将其关闭。

如果您确实想使用任务,则希望另一个任务不在 UI 线程中运行,而是在后台威胁中运行(即只是一个常规StartNew任务),然后在任务内部使用 Control.Invoke 在 UI 线程上运行命令。 这里的问题是,"创可贴"启动任务只是为了让它睡觉的根本问题。 最好只是让代码甚至不首先执行完整的第二个。