10

I need to create thread which will replace photo in Windows Forms window, than waits for ~1second and restore the previous photo.

I thought that the following code:

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)

do the job, but unfortunately doesn't. It freezes main UI thread.

That's because it's not guaranteed that there is one thread per one task. One thread can be used for processing several tasks. Even TaskCreationOptions.LongRunning option can't help.

How I can fix it?

patryk.beza
  • 4,876
  • 5
  • 37
  • 56
  • 8
    You create the task scheduler by using `FromCurrentSynchronizationContext()`, which for WinForms will be the UI thread. So your task eventually runs on the UI thread, which you then proceed to put to sleep. *Don't put the UI thread to sleep. Ever.* – dlev Apr 10 '12 at 23:03
  • Your code updates the UI directly, so it must run on the UI thread. Your code sleeps, so it cannot run on the UI thread. Conclusion: Your code is broken. Fix it by removing one of the two conflicting requirements. – David Schwartz Apr 10 '12 at 23:11

3 Answers3

29

Thread.Sleep is a synchronous delay. If you want an asynchronous delay then use Task.Delay.

In C# 5, which is at present in beta release, you can simply say

await Task.Delay(whatever);

in an asynchronous method, and the method will automatically pick up where it left off.

If you are not using C# 5 then you can "manually" set whatever code you want to be the continuation of the delay yourself.

svick
  • 236,525
  • 50
  • 385
  • 514
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
7

When you pass a new TaskScheduler that is from the current synchronization context, you actually telling the task to run on the UI thread. You actually want to do that, so you can update the UI component, however you don't want to sleep on that thread, since it will block.

This is a good example of when .ContinueWith is ideal:

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);

EDIT (Removed some stuff and added this):

What happens is that we're blocking the UI thread for only enough time to update pic.Image. By specifying the TaskScheduler, you're telling it what thread to run the task on. It's important to know that the relationship between Tasks and Threads is not 1:1. In fact, you can have 1000 tasks running on relatively few threads, 10 or less even, it all depends on the amount of work each task has. Do not assume each task you create will run on a separate thread. The CLR does a great job of balancing performance automatically for you.

Now, you don't have to use the default TaskScheduler, as you've seen. When you pass the UI TaskScheduler, that is TaskScheduler.FromCurrentSynchronizationContext(), it uses the UI thread instead of the thread pool, as TaskScheduler.Default does.

Keeping this in mind, let's review the code again:

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

Here, we're creating and starting a task that will run on the UI thread, that will update the Image property of pic with your resource. While it does this, the UI will be unresponsive. Fortunately, this is a likely a very fast operation, and the user won't even notice.

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

With this code, we're calling the ContinueWith method. It does exactly what it sounds like. It returns a new Task object that will execute the lambda parameter when it runs. It will be started when the task has either completed, faulted or been cancelled. You can control when it will run by passing in TaskContinuationOptions. However, we're also passing a different task scheduler as we did before. This is the default task scheduler that will execute a task on a thread pool thread, thus, NOT blocking the UI. This task could run for hours and your UI will stay responsive (don't let it), because it's a separate thread from the UI thread that you are interacting with.

We've also called ContinueWith on the tasks we've set to run on the default task scheduler. This is the task that will update the image on the UI thread again, since we've passed that same UI task scheduler to the executing task. Once the threadpool task has finished, it will call this one on the UI thread, blocking it for a very short period of time while the image is updated.

Christopher Currens
  • 29,917
  • 5
  • 57
  • 77
  • I don't use timer because in fact I have big `TableLayoutPanel` with `PictureBox`es in it (I do not want to have as many timers as `PictureBox`es). I placed this task's code in OnClick event handler, because I want to swap pictues for 1sec. after clicking clicked picture. – patryk.beza Apr 11 '12 at 00:07
  • Your code works great but I don't understand why. The key to achieve your solution is continuing task with different `TaskScheduler` (`.Default` one). What does it mean? Changing context to threadpool thread context? So we stop rest of the threads? That's not clear for me. – patryk.beza Apr 11 '12 at 00:27
  • @patryk.beza - Maybe my edits will make it more clear for you. – Christopher Currens Apr 11 '12 at 01:18
  • Thanks, I got it. But one more question: is there any way I can update UI from worker thread not interrupting UI thread? In this particular problem that's not a problem (:]) because we assume that `pic.Image = Properties.Resources.XYZ` is fast operation (and it is fast enough). – patryk.beza Apr 11 '12 at 11:50
  • Well, you can [explicitly do it from another thread](http://stackoverflow.com/questions/724972/updating-ui-from-a-different-thread), but the code you invoke will still have to run on the UI thread, so you'll want to keep it to a minimal amount of code that does the actual invoking. In my experience, tasks look cleaner, though either way will work. – Christopher Currens Apr 11 '12 at 16:44
5

You should be using a Timer to perform a UI task at some point in the future. Just set it to run once, and with a 1 second interval. Put the UI code in the tick event and then set it off.

If you really wanted to use tasks, you'd want to have the other task not run in the UI thread but rather in a background threat (i.e. just a regular StartNew task) and then use the Control.Invoke inside of the task to run a command on the UI thread. The problem here is that is' band-aid-ing the underlying problem of starting a task just to have it sleep. Better to just have the code not even execute in the first place for the full second.

Servy
  • 202,030
  • 26
  • 332
  • 449