Какой подход использовать с этой проблемой многопоточности?
Краткий вопрос:
Я хотел бы создать один фоновый поток, который обрабатывал бы рабочие элементы, отправленные в очередь (например, пул потоков с одним потоком). Некоторые из рабочих элементов способны сообщать о прогрессе, некоторые нет. Какой из множества подходов многопоточности.NET я должен использовать?
Длинное объяснение (чтобы не спрашивать о половине, которая не имеет никакого смысла):
Главное окно моего приложения winforms разделено по вертикали на две половины. Левая половина содержит дерево с элементами. Когда пользователь дважды щелкает элемент в древовидном меню, элемент открывается в правой половине. Почти все объекты имеют множество свойств, разбитых на несколько разделов (представлены вкладками). Загрузка этих свойств занимает довольно много времени, обычно около 10 секунд, иногда больше. И больше свойств добавляются время от времени, поэтому время увеличивается.
В настоящее время мой однопоточный дизайн делает пользовательский интерфейс невосприимчивым к этому времени. Естественно, это нежелательно. Я хотел бы загружать вещи по частям в фоновом режиме и, как только часть загружается, сделать ее доступной для использования. Для других частей я бы отобразил вкладку-заполнитель с анимацией загрузки или чем-то еще. Кроме того, в то время как некоторые части загружаются в одной длительной монолитной операции, другие состоят из множества меньших вызовов функций и вычислений и, таким образом, могут отображать ход загрузки. Для этих частей было бы хорошо видеть прогресс (особенно, если они где-то зависают, что происходит).
Обратите внимание, что источник данных не является потокобезопасным, поэтому я не могу загрузить две части одновременно.
Какой подход будет лучше всего для реализации этого поведения? Есть ли какой-то класс.NET, который снял бы с моих плеч какую-то работу, или я должен просто сойти с ума от Thread
?
ThreadPool
выполняет управление очередью рабочих элементов, но нет средств для отчетов о ходе работ. BackgroundWorker
с другой стороны, поддерживает отчеты о прогрессе, но он предназначен для одного рабочего элемента. Возможно ли сочетание обоих?
3 ответа
.NET 4.0 вносит много улучшений в многопоточность, представляя Task
тип, представляющий одну возможно асинхронную операцию.
Для вашего сценария я бы рекомендовал разделить загрузку каждого свойства (или группы свойств) на отдельные задачи. Задачи включают понятие "родитель", поэтому загрузка каждого объекта может быть родительской задачей, владеющей задачами загрузки свойств.
Для обработки отмены используйте новую унифицированную структуру отмены. Создать CancellationTokenSource
для каждого объекта и передать его CancellationToken
родительской задаче (которая передает ее каждой дочерней задаче). Это позволяет отменить объект, который может вступить в силу после завершения загрузки текущего свойства (вместо ожидания, пока весь объект не будет завершен).
Для обработки параллелизма (или, точнее, не- параллелизма) используйте OrderedTaskScheduler
из библиотеки примеров ParallelExtensionsExtras. каждый Task
представляет только единицу работы, которая должна быть запланирована, и с помощью OrderedTaskScheduler
Вы гарантируете последовательное выполнение (в потоке ThreadPool).
Обновления прогресса пользовательского интерфейса могут быть сделаны путем создания обновления пользовательского интерфейса Task
и планирование его в потоке пользовательского интерфейса. У меня есть пример этого в моем блоге, где я обертываю некоторые из более неловких методов в ProgressReporter
вспомогательный тип.
Одна хорошая вещь о Task
тип в том, что он распространяет исключения и отмены естественным образом; это часто самые трудные части разработки системы для решения проблемы, подобной вашей.
Используйте поток, поместите свою работу в потокобезопасную коллекцию и используйте invoke, когда вы обновляете свой интерфейс, чтобы сделать это в нужном потоке.
Звучит сложно!
Вы говорите, что ваш источник данных не является потокобезопасным. Итак, что это значит для пользователя. Если они щелкают повсюду, но не ждут загрузки свойств, прежде чем щелкнуть где-то еще, они могут щелкнуть по 10 узлам, которые долго загружаются, а затем сидеть в ожидании 10-го. Загрузка должна выполняться одна за другой, так как доступ к источнику данных не является потокобезопасным. Это указывает на то, что ThreadPool не будет хорошим выбором, поскольку он будет запускать нагрузки параллельно и нарушать безопасность потоков. Было бы хорошо, если бы загрузка могла быть прервана на полпути, чтобы пользователю не пришлось ждать загрузки последних 9 узлов, прежде чем начнется загрузка страницы, которую он хочет видеть.
Если загрузка может быть прервана, я бы посоветовал использовать BackgroundWorker. Если пользователь переключает узел, и BackgroundWorker уже занят, установите событие или что-то, сигнализирующее о том, что он должен прервать существующую работу, а затем поставьте в очередь новую работу, чтобы загрузить текущую страницу.
Кроме того, учтите, что не так сложно заставить поток, работающий в пуле потоков, сообщать о прогрессе. Для этого передайте объект прогресса в вызов QueueUserWorkItem типа, подобного следующему:
class Progress
{
object _lock = new Object();
int _current;
bool _abort;
public int Current
{
get { lock(_lock) { return _current; } }
set { lock(_lock) { _current = value; } }
}
public bool Abort
{
get { lock(_lock) { return _abort; } }
set { lock(_lock) { _abort = value; } }
}
}
Поток может писать в это, а поток пользовательского интерфейса может опрашивать (из события System.Windows.Forms.Timer), чтобы прочитать прогресс и обновить индикатор выполнения или анимацию.
Также, если вы включите свойство Abort. Пользовательский интерфейс может установить его, если пользователь меняет узел. Метод load может в различных точках на протяжении всей своей работы проверять значение сброса и, если он установлен, возвращаться без завершения загрузки.
Если честно, то, что вы выбираете, не имеет никакого значения. Все три варианта делают вещи в фоновом потоке. На вашем месте я бы начал с BackgroundWorker, так как он довольно прост в настройке, и если вы решите, что вам нужно что-то большее, подумайте о том, чтобы потом переключиться на ThreadPool или обычный поток.
BackgroundWorker также имеет то преимущество, что вы можете использовать его завершенное событие (которое выполняется в главном потоке пользовательского интерфейса), чтобы обновить пользовательский интерфейс загруженными данными.