10

我需要创建线程来替换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.LongRunning选项也无济于事。

我该如何解决?

4

3 回答 3

28

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

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

await Task.Delay(whatever);

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

如果您不使用 C# 5,那么您可以“手动”设置您希望自己继续延迟的任何代码。

于 2012-04-11T04:31:11.997 回答
7

当您传递来自当前同步上下文的新 TaskScheduler 时,实际上是在告诉任务在 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,您是在告诉它在哪个线程上运行任务。重要的是要知道 Tasks 和 Threads 之间的关系不是 1:1。实际上,您可以在相对较少的线程上运行 1000 个任务,甚至 10 个或更少,这完全取决于每个任务的工作量。不要假设您创建的每个任务都将在单独的线程上运行。CLR 在自动为您平衡性能方面做得很好。

现在,您不必使用 default TaskScheduler,如您所见。当你传递 UITaskScheduler时,也就是TaskScheduler.FromCurrentSynchronizationContext()它使用 UI 线程而不是线程池TaskScheduler.Default

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

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

在这里,我们正在创建并启动一个将在UI线程上运行的任务,该任务将使用您的资源更新 的Image属性。pic执行此操作时,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 线程上调用它,在更新图像时将其阻塞很短的时间。

于 2012-04-10T23:07:36.813 回答
5

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

如果你真的想使用任务,你会希望其他任务不在 UI 线程中运行,而是在后台威胁中运行(即只是一个常规StartNew任务),然后使用任务内部的 Control.Invoke 来运行UI线程上的命令。这里的问题是'创可贴'启动任务只是为了让它睡觉的潜在问题。最好只让代码在第一时间不执行整整一秒。

于 2012-04-10T23:03:35.530 回答