Задача сна (System.Threading.Tasks)

Мне нужно создать поток, который заменит фото в окне Windows Forms, а не будет ждать ~ 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)

сделать работу, но, к сожалению, не делает. Он замораживает основной поток пользовательского интерфейса.

Это потому, что не гарантируется наличие одного потока для каждой задачи. Один поток может использоваться для обработки нескольких задач. Даже TaskCreationOptions.LongRunning< Параметр /a> не может помочь.

Как я могу это исправить?


person patryk.beza    schedule 10.04.2012    source источник
comment
Вы создаете планировщик задач, используя FromCurrentSynchronizationContext(), который для WinForms будет потоком пользовательского интерфейса. Таким образом, ваша задача в конечном итоге выполняется в потоке пользовательского интерфейса, который вы затем переводите в спящий режим. Не переводите поток пользовательского интерфейса в спящий режим. Никогда.   -  person dlev    schedule 11.04.2012
comment
Ваш код обновляет пользовательский интерфейс напрямую, поэтому он должен выполняться в потоке пользовательского интерфейса. Ваш код спит, поэтому он не может работать в потоке пользовательского интерфейса. Вывод: Ваш код неисправен. Исправьте это, удалив одно из двух конфликтующих требований.   -  person David Schwartz    schedule 11.04.2012


Ответы (3)


Thread.Sleep — это синхронная задержка. Если вам нужна асинхронная задержка, используйте < em>Task.Delay.

В C# 5, который в настоящее время находится в бета-версии, вы можете просто сказать

await Task.Delay(whatever);

в асинхронном методе, и метод автоматически продолжит работу с того места, где остановился.

Если вы не используете С# 5, вы можете «вручную» установить любой код, который вы хотите, чтобы он был продолжением задержки самостоятельно.

person Eric Lippert    schedule 11.04.2012
comment
@JosephLennox C# 5 == .NET 4.5 - person Rudey; 06.02.2015

Когда вы передаете новый TaskScheduler из текущего контекста синхронизации, вы фактически указываете задаче выполняться в потоке пользовательского интерфейса. Вы на самом деле хотите это сделать, поэтому вы можете обновить компонент пользовательского интерфейса, однако вы не хотите спать в этом потоке, так как он будет заблокирован.

Это хороший пример идеального случая .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);

РЕДАКТИРОВАТЬ (удалил кое-что и добавил это):

Что происходит, так это то, что мы блокируем поток пользовательского интерфейса только на время, достаточное для обновления pic.Image. Указав TaskScheduler, вы указываете, в каком потоке выполнять задачу. Важно знать, что отношения между задачами и потоками не равны 1:1. На самом деле, вы можете иметь 1000 задач, работающих в относительно небольшом количестве потоков, даже 10 или меньше, все зависит от объема работы, которую выполняет каждая задача. Не думайте, что каждая создаваемая вами задача будет выполняться в отдельном потоке. CLR отлично справляется с автоматической балансировкой производительности.

Теперь вам не нужно использовать TaskScheduler по умолчанию, как вы видели. Когда вы передаете пользовательский интерфейс TaskScheduler, то есть TaskScheduler.FromCurrentSynchronizationContext(), он использует поток пользовательского интерфейса вместо пула потоков, как это делает 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, который будет выполнять лямбда-параметр при запуске. Он будет запущен, когда задача будет завершена, неисправна или отменена. Вы можете контролировать, когда он будет запущен, передав TaskContinuationOptions. Однако мы также передаем другой планировщик задач, как и раньше. Это планировщик задач по умолчанию, который будет выполнять задачу в потоке пула потоков, таким образом, НЕ блокируя пользовательский интерфейс. Эта задача может выполняться часами, и ваш пользовательский интерфейс будет оставаться отзывчивым (не позволяйте этому), потому что это отдельный поток от потока пользовательского интерфейса, с которым вы взаимодействуете.

Мы также вызвали ContinueWith для задач, которые мы установили для запуска в планировщике задач по умолчанию. Это задача, которая снова обновит изображение в потоке пользовательского интерфейса, поскольку мы передали тот же планировщик задач пользовательского интерфейса в выполняемую задачу. Как только задача пула потоков завершится, она вызовет эту задачу в потоке пользовательского интерфейса, блокируя ее на очень короткий период времени, пока изображение обновляется.

person Christopher Currens    schedule 10.04.2012
comment
Я не использую таймер, потому что на самом деле у меня есть большой TableLayoutPanel с PictureBoxes в нем (я не хочу иметь столько таймеров, сколько PictureBoxes). Я поместил код этой задачи в обработчик события OnClick, потому что я хочу поменять местами картинки на 1 секунду. после клика по картинке. - person patryk.beza; 11.04.2012
comment
Ваш код отлично работает, но я не понимаю, почему. Ключом к достижению вашего решения является продолжение задачи с разными TaskScheduler (.Default одним). Что это означает? Изменение контекста на контекст потока пула потоков? Итак, мы останавливаем остальные потоки? Это не ясно для меня. - person patryk.beza; 11.04.2012
comment
@patryk.beza - Может быть, мои правки сделают его более понятным для вас. - person Christopher Currens; 11.04.2012
comment
Спасибо, я понял. Но еще один вопрос: есть ли способ обновить пользовательский интерфейс из рабочего потока, не прерывая поток пользовательского интерфейса? В данной конкретной задаче это не проблема (:]), потому что мы предполагаем, что pic.Image = Properties.Resources.XYZ является быстрой операцией (и достаточно быстрой). - person patryk.beza; 11.04.2012
comment
Что ж, вы можете явно сделать это из другого потока, но код, который вы вызываете, все равно придется работать в потоке пользовательского интерфейса, поэтому вам нужно свести к минимуму объем кода, который выполняет фактический вызов. По моему опыту, задачи выглядят чище, хотя любой способ будет работать. - person Christopher Currens; 11.04.2012

Вы должны использовать Timer для выполнения задачи пользовательского интерфейса в какой-то момент в будущем. Просто запустите его один раз и с интервалом в 1 секунду. Поместите код пользовательского интерфейса в событие тика, а затем установите его.

Если вы действительно хотите использовать задачи, вам нужно, чтобы другая задача выполнялась не в потоке пользовательского интерфейса, а в фоновой угрозе (т. е. просто в обычной задаче StartNew), а затем использовалась функция Control. Вызов внутри задачи для запуска команды в потоке пользовательского интерфейса. Проблема здесь в том, что это «помощь» основной проблемы запуска задачи только для того, чтобы она заснула. Лучше просто не запускать код в течение полной секунды.

person Servy    schedule 10.04.2012