Многопоточность в Windows Forms

Я хочу парализовать редактор 3D-вокселей, построенный поверх Windows Forms, он использует raycaster для рендеринга, поэтому разделение экрана и получение каждого потока в пуле для его рендеринга должно быть тривиальным.

Проблема возникает в том, что поток Windows Forms должен работать как STA - я могу заставить другие потоки запускаться и выполнять работу, но блокирование основного потока в ожидании его завершения вызывает странные случайные взаимоблокировки, как и ожидалось.

Сохранение основного потока разблокированным также было бы проблемой - если, например, пользователь использует инструмент заливки, ввод будет обработан во время процесса рендеринга, что приведет к появлению промежуточных изображений (например, частично окрашенного объекта). Копирование всего изображения перед каждым кадром также невозможно, поскольку тома достаточно велики, чтобы компенсировать любой выигрыш в производительности, если его нужно копировать каждый кадр.

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

Если это невозможно, есть ли лучший дизайн для решения этой проблемы?

РЕДАКТИРОВАТЬ: Читая anwsers Я думаю, я не был уверен, что raycaster работает в режиме реального времени, поэтому показ диалогов прогресса не будет работать вообще. К сожалению, FPS достаточно низок (5-40 в зависимости от различных факторов) для ввода между кадрами для получения нежелательных результатов.

Я уже пытался реализовать его, блокируя поток пользовательского интерфейса и используя некоторые потоки ThreadPool для обработки, и он работает нормально, за исключением этой проблемы с STA.

13.10.2009 01:19:35
Случайные входные события. Обычно это происходит, если щелкнуть свернутое приложение на панели задач - иногда Windows издает звуковой сигнал (сигнал об ошибке), а иногда происходит сбой.
John Doe 13.10.2009 03:17:01
5 ОТВЕТОВ
РЕШЕНИЕ

Я что-то упустил или вы можете просто отключить свой элемент управления рендерингом (и любые другие элементы управления, которые генерируют входные события) во время рендеринга кадра? Это предотвратит нежелательные входы.

Если вы по-прежнему хотите принимать события во время рендеринга, но не хотите применять их до следующего кадра, вы должны оставить свои элементы управления включенными и отправить подробные сведения о событии во входную очередь. Затем эта очередь должна обрабатываться в начале каждого кадра.

Это влияет на то, что пользователь все еще может нажимать кнопки и взаимодействовать с пользовательским интерфейсом (поток графического интерфейса не блокируется), и эти события не видны для средства визуализации до начала следующего кадра. При скорости 5 кадров в секунду пользователь должен видеть, что его события обрабатываются в наихудшем случае 400 мс (2 кадра), что не достаточно быстро, но лучше, чем многопоточность.

Возможно, что-то вроде этого:

Public InputQueue<InputEvent> = new Queue<InputEvent>();

// An input event handler.
private void btnDoSomething_Click(object sender, EventArgs e)
{ 
    lock(InputQueue)
    {
         InputQueue.Enqueue(new DoSomethingInputEvent());
    }
}

// Your render method (executing in a background thread). 
private void RenderNextFrame()
{
    Queue<InputEvent> inputEvents = new Queue<InputEvent>();

    lock(InputQueue)
    {
         inputEvents.Enqueue(InputQueue.Dequeue());
    }

    // Process your input events from the local inputEvents queue.
    ....

    // Now do your render based on those events.
    ....
}

Да, и сделайте рендеринг в фоновом потоке. Ваш поток пользовательского интерфейса драгоценен, он должен выполнять только тривиальную работу. Предложение Мэтта Бранделла о BackgroundWorker имеет много достоинств. Если он не делает то, что вы хотите, ThreadPool также полезен (и проще). Более мощными (и сложными) альтернативами являются CCR или Task Parallel Library .

1
13.10.2009 03:50:22
Это кажется хорошей идеей, я попробую и дам вам знать, если это работает.
John Doe 13.10.2009 14:03:23

Покажите модальное диалоговое окно «Пожалуйста, подождите» ShowDialog, затем закройте его после завершения рендеринга.

Это предотвратит взаимодействие пользователя с формой, и в то же время позволит вам вызывать поток пользовательского интерфейса (что, вероятно, является вашей проблемой).

0
13.10.2009 01:35:18
На самом деле это ShowDialog, а не ShowModal;)
Thomas Levesque 13.10.2009 01:33:25
Разве ShowDialog не останавливает всю обработку родительской формы, пока дочерняя форма не будет закрыта?
Ryan Alford 13.10.2009 01:36:08
К сожалению, это рендерер в реальном времени, поэтому я не могу этого сделать.
John Doe 13.10.2009 03:08:07

Это распространенная проблема. С окнами формы вы можете иметь только один поток пользовательского интерфейса. Не запускайте свой алгоритм в потоке пользовательского интерфейса, потому что тогда пользовательский интерфейс будет казаться замороженным.

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

Редактировать:

Еще один факт, касающийся потока пользовательского интерфейса, заключается в том, что он обрабатывает все события мыши и клавиатуры, а также системные сообщения, отправляемые в окно. (Winforms на самом деле просто Win32, окруженный хорошим API.) Вы не можете иметь стабильное приложение, если поток пользовательского интерфейса насыщен.

С другой стороны, если вы запустите несколько других потоков и попытаетесь нарисовать их прямо на экране, у вас могут возникнуть две проблемы:

  1. Вы не должны рисовать на интерфейсе с каким-либо потоком, кроме потока пользовательского интерфейса. Элементы управления Windows не являются потокобезопасными.
  2. Если у вас много потоков, переключение контекста между ними может снизить производительность.

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

Честно говоря, я не знаю, возможно ли это с вашей целевой частотой кадров, или вы должны рассмотреть более ориентированную на графику библиотеку, такую ​​как OpenGL.

6
13.10.2009 04:39:38
Я думаю, что я не очень ясно, намерение состоит в том, чтобы пользовательский интерфейс казался замороженным, пока не нарисован кадр (5-40 FPS, в зависимости от различных параметров). Если он продолжает обновляться, изображение может быть частично изменено между кадрами, и это будет выглядеть странно.
John Doe 13.10.2009 03:07:33

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

Тогда не копируйте внеэкранный буфер на каждый кадр.

-1
13.10.2009 01:40:25

Если вам не нужны все функции, предлагаемые BackgroundWorker, вы можете просто использовать ThreadPool.QueueUserWorkItem, чтобы добавить что-то в пул потоков и использовать фоновый поток. Было бы легко показать некоторый прогресс, когда фоновый поток выполнял свои операции, поскольку вы можете предоставить обратный вызов делегата, чтобы уведомлять вас, когда выполняется конкретный фоновый поток. Взгляните на метод ThreadPool.QueueUserWorkItem (WaitCallback, Object), чтобы увидеть, на что я вас ссылаюсь . Если вам нужно что-то более сложное, вы всегда можете использовать асинхронный метод APM для выполнения ваших операций.

В любом случае, надеюсь, это поможет.

РЕДАКТИРОВАТЬ:

  1. Как-то уведомить пользователя о внесении изменений в пользовательский интерфейс.
  2. В (многих) фоновых потоках, использующих ThreadPool, выполните операции, которые вам необходимо выполнить для пользовательского интерфейса.
  3. Для каждой операции сохраняйте ссылку на состояние операции, чтобы вы знали, когда она завершена, в WaitCallback. Возможно, поместите их в какой-нибудь тип хэша / коллекции, чтобы сохранить ссылку на них.
  4. Когда операция завершается, удаляйте ее из коллекции, содержащей ссылку на выполненные операции.
  5. После того, как все операции завершены (хеш / коллекция), больше нет ссылок в нем, визуализируем пользовательский интерфейс с примененными изменениями. Или, возможно, постепенно обновлять интерфейс

Я думаю, что если вы делаете так много обновлений пользовательского интерфейса во время выполнения своих операций, это является причиной ваших проблем. Вот почему я рекомендовал использовать SuspendLayout, PerformLayout, поскольку вы, возможно, выполняли так много обновлений пользовательского интерфейса, что основной поток был перегружен.

Хотя я не специалист по потокам, просто пытаюсь обдумать это сам. Надеюсь это поможет.

0
13.10.2009 03:47:47
Я уже пытался использовать ThreadPool, но я не могу продолжать работу цикла и отображать индикатор выполнения, потому что это средство визуализации в реальном времени (недостаточно быстрое, чтобы избежать артефактов, вызванных частичным изменением изображения между кадрами). Если я блокирую поток пользовательского интерфейса во время ожидания завершения рендеринга, взаимоблокировки случаются случайно.
John Doe 13.10.2009 03:13:20
Вы можете использовать APM, чтобы сообщить о прогрессе для каждого элемента. Я думаю, что вы, вероятно, столкнетесь с отчетом о значительном детерминистическом прогрессе. Я не думаю, что вы хотите выполнить какие-либо блокировки в потоке пользовательского интерфейса для достижения этой цели. Выполняете ли вы SuspendLayout и ResumeLayout для соответствующих элементов управления при обновлении пользовательского интерфейса, чтобы отразить изменения? Мне кажется, что вы хотели бы полностью приостановить SuspendLayout, пока не закончите вносить изменения, но я не уверен, что полностью понимаю вашу проблему.
Wil P 13.10.2009 03:29:51