Информация об изменениях

Сообщение Re[3]: Как вызвать метод формы из родительского потока? от 18.09.2014 8:58

Изменено 18.09.2014 9:03 fortnum

Здравствуйте, mDmitriy, Вы писали:

D>Здравствуйте, Sharov, Вы писали:

S>> тыц
D>Спасибо, но это, кажется, несколько не то, что мне нужно...
D>Про BackgroundWorker, Invoke и BeginInvoke я в курсе, но они вызываются из формы, а мне надо доступ к форме снаружи.

Абсолютно без разницы, откуда вызываются Invoke и BeginInvoke. В случае с формой, откуда бы эти методы ни были бы вызваны, всё что они сделают — поставят в очередь сообщений вызов заданного делегата. Разница лишь в том, что Invoke будет ждать, когда поставленная в очередь структура данных будет обработана — т.е. будет вызван делегат, и этот делегат вернет вызов обратно. А BeginInvoke не ждет — после постановки делегата в очередь немедленно возвращает вызов. Методы Invoke и BeginInvoke — это так называемые потоко-безопасные методы, т.е. их можно вызывать из неограниченного числа потоков одновременно — они на это и рассчитаны. Вот что по этому поводу написано в MSDN :

In addition to the InvokeRequired property, there are four methods on a control that are thread safe : Invoke, BeginInvoke, EndInvoke


Вообще, говорить, что метод вызывается "из формы" в данном случае некорректно. Вызов делает поток. Т.е. в чистом виде один и тот же код может параллельно исполняться неограниченным количеством потоков. Но код сам по себе — штука бесполезная. Код изменяет состояние, и вот тут нюанс. Если каждый из этих параллельных потоков работает со своим состоянием — проблем нет. Но если они в какой-то части работают над одним общим состоянием, для каждого отдельного потока могут возникнуть непредвиденные (недетерминированные) изменения этого общего состояния между последовательно исполняемыми им инструкциями, если этот отдельный поток не рассматривает это состояние как общее, либо не имеет возможности предотвратить такие изменения.

Состояние формы для многопоточной программы — это и есть то самое общее (разделяемое) состояние. Сделать разделяемое состояние детерминированным можно как минимум двумя путями:

1) перед началом исполнения ряда последовательных инструкций, между которыми изменение разделяемого состояния должно быть для данных инструкций детерминированным, заблокировать исполнение этих инструкций другими потоками;
2) назначить для работы с разделяемым состоянием один поток, которому все остальные потоки будут делегировать необходимость исполнить инструкции, между которыми изменение разделяемого состояния должно быть для данных инструкций детерминированным.

Поэтому в ThreadUIClass1.Dispose надо всего лишь написать:

_myForm.Invoke((Action)_myForm.Dispose);


Но у тебя в коде еще и не такая явная Проблема №2. В ThreadUIClass1.Dispose у тебя идет проверка "if (_myForm != null)", но поле _myForm — это разделяемое состояние между потоком "new Thread(RunApp)" и потоком, который вызовет ThreadUIClass1.Dispose. И хотя, в спецификации C# в 5.5 Atomicity of variable references говорится:

Reads and writes of the following data types are atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types.


То есть запись и чтение поля _myForm — атомарные операции, и само содержимое этого поля будет всегда прочитано корректно, однако, в ~0.001% может получиться так, что создание формы задержится — можно представить себе это так:

private void RunApp(object obj)
{
    var myForm = new Form();

    Thread.Sleep(10000);

    _myForm = myForm;

    Application.Run(_myForm);
}


, а клиент во время этой задержки, до назначения значения полю _myForm успеет задиспоузить твой COM+ компонент, и теоретически может получиться ситуация, когда метод ThreadUIClass1.Dispose выполнится до назначения значения полю _myForm

Более того, даже если форма успеет создаться, и значение полю _myForm будет назначено, нюанс в том, что Application.Run запускает прокачку очереди сообщений, и если ты задиспоузишь форму после ее создания, но до вызова Application.Run... ну, попробуй сделать так и сам все увидишь :

private void RunApp(object obj)
{
    _myForm = new Form();

    _myForm.Dispose();

    Application.Run(_myForm);
}


Тут еще один нюанс, который состоит в том, что вызвать Invoke или BeginInvoke до создания хэндла окна Windows Forms не даст, а кинет исключение. То есть исключение возникнет не в "new Thread" (не в RunApp), а в ThreadUIClass1.Dispose при вызове _myForm.Invoke. То есть компонент будет задиспоужен с исключением, а форма (если ты обработаешь это исключение) останется существовать. Попробуй еще так и увидишь:

private void RunApp(object obj)
{
    _myForm = new Form();

    _myForm.Invoke((Action)_myForm.Dispose);

    Application.Run(_myForm);
}


Короче, если интересно, можем обсудить, как правильно решать эту Проблему №2. К примеру, в COM специально для решения этой задачи существует single-threaded apartment — STA апартмент, который позволяет исполнять внешние вызовы последовательно. COM сам осуществляет синхронизацию внешних параллельных вызовов в последовательные. Конкретно эта модель апартмента аналогична окну, и даже представляет собой один поток и ту же самую очередь сообщений. И почему бы не использовать одну и ту же очередь сообщений, один и тот же рабочий поток для двух дел одновременно? Для обработки внешних вызовов, и для реакции на события окна? Этот апартмент специально для управления окнами и создавался. Минус в том, что ты потеряешь для своего компонента возможность вызова его методов в параллель, а обработка событий окна будет тормозить обработку внешних вызовов. Если у тебя стоит задача не формой управлять, а параллельно обслуживать большое число клиентов, а форма служит лишь для индикации работы, конечно решение с STA не подойдет, хотя...
Здравствуйте, mDmitriy, Вы писали:

D>Здравствуйте, Sharov, Вы писали:

S>> тыц
D>Спасибо, но это, кажется, несколько не то, что мне нужно...
D>Про BackgroundWorker, Invoke и BeginInvoke я в курсе, но они вызываются из формы, а мне надо доступ к форме снаружи.

Абсолютно без разницы, откуда вызываются Invoke и BeginInvoke. В случае с формой, откуда бы эти методы ни были бы вызваны, всё что они сделают — поставят в очередь сообщений вызов заданного делегата. Разница лишь в том, что Invoke будет ждать, когда поставленная в очередь структура данных будет обработана — т.е. будет вызван делегат, и этот делегат вернет вызов обратно. А BeginInvoke не ждет — после постановки делегата в очередь немедленно возвращает вызов. Методы Invoke и BeginInvoke — это так называемые потоко-безопасные методы, т.е. их можно вызывать из неограниченного числа потоков одновременно — они на это и рассчитаны. Вот что по этому поводу написано в MSDN :

In addition to the InvokeRequired property, there are four methods on a control that are thread safe : Invoke, BeginInvoke, EndInvoke


Вообще, говорить, что метод вызывается "из формы" в данном случае некорректно. Вызов делает поток. Т.е. в чистом виде один и тот же код может параллельно исполняться неограниченным количеством потоков. Но код сам по себе — штука бесполезная. Код изменяет состояние, и вот тут нюанс. Если каждый из этих параллельных потоков работает со своим состоянием — проблем нет. Но если они в какой-то части работают над одним общим состоянием, для каждого отдельного потока могут возникнуть непредвиденные (недетерминированные) изменения этого общего состояния между последовательно исполняемыми им инструкциями, если этот отдельный поток не рассматривает это состояние как общее, либо не имеет возможности предотвратить такие изменения.

Состояние формы для многопоточной программы — это и есть то самое общее (разделяемое) состояние. Сделать разделяемое состояние детерминированным можно как минимум двумя путями:

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

Поэтому в ThreadUIClass1.Dispose надо всего лишь написать:

_myForm.Invoke((Action)_myForm.Dispose);


Но у тебя в коде еще и не такая явная Проблема №2. В ThreadUIClass1.Dispose у тебя идет проверка "if (_myForm != null)", но поле _myForm — это разделяемое состояние между потоком "new Thread(RunApp)" и потоком, который вызовет ThreadUIClass1.Dispose. И хотя, в спецификации C# в 5.5 Atomicity of variable references говорится:

Reads and writes of the following data types are atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types.


То есть запись и чтение поля _myForm — атомарные операции, и само содержимое этого поля будет всегда прочитано корректно, однако, в ~0.001% может получиться так, что создание формы задержится — можно представить себе это так:

private void RunApp(object obj)
{
    var myForm = new Form();

    Thread.Sleep(10000);

    _myForm = myForm;

    Application.Run(_myForm);
}


, а клиент во время этой задержки, до назначения значения полю _myForm успеет задиспоузить твой COM+ компонент, и теоретически может получиться ситуация, когда метод ThreadUIClass1.Dispose выполнится до назначения значения полю _myForm

Более того, даже если форма успеет создаться, и значение полю _myForm будет назначено, нюанс в том, что Application.Run запускает прокачку очереди сообщений, и если ты задиспоузишь форму после ее создания, но до вызова Application.Run... ну, попробуй сделать так и сам все увидишь :

private void RunApp(object obj)
{
    _myForm = new Form();

    _myForm.Dispose();

    Application.Run(_myForm);
}


Тут еще один нюанс, который состоит в том, что вызвать Invoke или BeginInvoke до создания хэндла окна Windows Forms не даст, а кинет исключение. То есть исключение возникнет не в "new Thread" (не в RunApp), а в ThreadUIClass1.Dispose при вызове _myForm.Invoke. То есть компонент будет задиспоужен с исключением, а форма (если ты обработаешь это исключение) останется существовать. Попробуй еще так и увидишь:

private void RunApp(object obj)
{
    _myForm = new Form();

    _myForm.Invoke((Action)_myForm.Dispose);

    Application.Run(_myForm);
}


Короче, если интересно, можем обсудить, как правильно решать эту Проблему №2. К примеру, в COM специально для решения этой задачи существует single-threaded apartment — STA апартмент, который позволяет исполнять внешние вызовы последовательно. COM сам осуществляет синхронизацию внешних параллельных вызовов в последовательные. Конкретно эта модель апартмента аналогична окну, и даже представляет собой один поток и ту же самую очередь сообщений. И почему бы не использовать одну и ту же очередь сообщений, один и тот же рабочий поток для двух дел одновременно? Для обработки внешних вызовов, и для реакции на события окна? Этот апартмент специально для управления окнами и создавался. Минус в том, что ты потеряешь для своего компонента возможность вызова его методов в параллель, а обработка событий окна будет тормозить обработку внешних вызовов. Если у тебя стоит задача не формой управлять, а параллельно обслуживать большое число клиентов, а форма служит лишь для индикации работы, конечно решение с STA не подойдет, хотя...