Skip to main content
Доступ к элементам управления Windows Forms через потоки
Рейтинг 

Автор: Майкл Анфрид (Michael Unfried)

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

Общие сведения о многопоточности

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

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

Принципы работы многопоточности

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

Большая часть операторов кода, выполняющих некоторые виды манипуляций с данными, является "блокирующей". Это означает, что они препятствуют выполнению следующей строки кода до тех пор, пока не завершат свое выполнение. Это называется "синхронным выполнением", поскольку каждая строка кода выполняется в последовательности.

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

Давайте рассмотрим метафору. Представим процессор нашего компьютера в виде автодороги. Если на ней только одна полоса движения, то по ней в какой-либо момент времени может проходить только один автомобиль. Если автомобиль двигается медленно, то все остальные автомобили за ним вынуждены двигаться медленно и ждать первый автомобиль. Теперь представим каждый поток в приложении в виде дополнительной полосы движения. Теперь автомобили могут двигаться рядом друг с другом, и некоторые могут даже обгонять остальные автомобили. Теперь представим скоростную автомагистраль, построенную вне строк кода, и автомобили, которые представляют выполнение этого кода.

Если приложение выполняет несколько потоков, необходимо учитывать многое, в том числе синхронизацию и стабильность данных. Иначе могут возникнуть такие неприемлемые ситуации, в которых поток А назначает значение переменной, а затем поток Б изменяет это значение прежде, чем поток А сможет им воспользоваться. Это то, что называется "состоянием гонки" (не только потому, что я использовал аналогию с автомобилями). Классы с кодом, предотвращающим такие состояния гонки, считаются "потокобезопасными". Многие классы в платформе .NET Framework являются потокобезопасными, но далеко не все.

Наиболее часто неправильно используемый с многопоточностью набор типов .NET — это элементы управления System.Windows.Forms. Эти классы не являются потокобезопасными, и то и дело обнаруживается код с неправильной обработкой таких объектов через границы потоков. Если вернуться к нашей аналогии с автодорогой, то границы потоков можно представить в виде линий, разделяющих встречные полосы движения. Если один автомобиль перейдет на встречную полосу движения, не включив сигнал поворота, не убедившись, что полоса свободна, не ... АВАРИЯ!

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

Смена полос движения

Теперь давайте подробнее рассмотрим элементы управления Windows Forms. Все элементы Windows Forms наследуют от базового класса с именем "System.Windows.Forms.Control". Это довольно очевидно. Этот "управляющий" класс предоставляет общее логическое свойство с именем "InvokeRequired". Если это свойство возвращает значение false, то это значит, что все в порядке с доступом к данному элементу управления из текущего потока.

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

Не надо бояться

"Вызов" — это термин, используемый для описания, когда один поток запрашивает у другого потока выполнение фрагмента кода. Это выполняется путем использования "делегатов".

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

delegate void ChangeMyTextDelegate(Control ctrl, string text);

Этот оператор задает ссылку на метод с типом возврата "void" и принимает два параметра: Control и String. Взгляните на следующий метод, который пытается обновить текст конкретного элемента управления.

publicstatic void ChangeMyText(Control ctrl, string text);
{
    ctrl.Text = text;
}

Это все будет хорошо и правильно в том случае, если поток, вызывающий метод, является тем потоком, который создал этот элемент управления. Если это не так, мы рискуем тем, что наши автомобили столкнутся.

Чтобы обойти это, мы используем свойство "InvokeRequired" элемента управления и делегата, заданного ранее. Итоговый код выглядит следующим образом:

delegate void ChangeMyTextDelegate(Control ctrl, string text);
public static void ChangeMyText(Control ctrl, string text)
{
    if (ctrl.InvokeRequired)
    {
        ChangeMyTextDelegate del = new ChangeMyTextDelegate(ChangeMyText);
        ctrl.Invoke(del, ctrl, text)
    }
    else
    {
        ctrl.Text = text;
    }
}

Здесь в первой строке объявляется определение для нашего делегата. Как и ранее, мы определяем его как метод, возвращающий void и принимающий два аргумента. Затем наступает время нашего метода. Первое, что делает этот метод, это проверку, чтобы увидеть, находимся ли мы в правильном потоке для доступа к параметру "ctrl" объекта Control, передаваемому в качестве первого аргумента. Если требуется вызов, то создается новый экземпляр делегата, который затем направляется в метод "ChangeMyText". Затем мы просим элемент управления выполнить код, передавая делегата и необходимые ему аргументы. В этот момент может появиться уведомление, что подпись делегата соответствует подписи метода, который он представляет. Это условие всегда должно быть истинно для делегата, чтобы он мог ссылаться на метод.

После того как элемент управления выполнит этот метод (в собственном потоке), будет оцениваться значение свойства "InvokeRequired", поскольку его значение false позволяет выполнить блок "else" и установить в качестве значения свойства "Text" элемента управления строку, указанную в аргументе "text".

"Блокирующие" операторы

Как правило, практически каждый оператор в коде будет блокирующим. Исключением является случай, когда вызывается метод, начинающийся со слова "Begin". Это обычный стандарт кодирования, идентифицирующий вызовы неблокирующих методов. Это хорошее синтаксическое правило, которого следует придерживаться при написании собственного многопоточного кода, особенно при создании библиотеки многократно используемых классов, поскольку это помогает обслуживать стабильность при передаче библиотек DLL для использования кем-нибудь еще. Кроме метода "Invoke", который использовался выше, класс Control также предоставляет метод "BeginInvoke", который делает то же самое, но является неблокирующим оператором.

Что мы будем делать дальше?

Извлечем присоединенный файл "CrossThreadUI.cs", один файл из созданной мной большой библиотеки классов. Он не только содержит метод, почти идентичный приведенному выше примеру (с именем "SetText" в CS-файле), но также предоставляет множество других статических методов, позволяющих устанавливать почти любое свойство в любом типе объектов Control и иногда даже использовать отражение для проверки типов объектов и аргументов. Это полезный вспомогательный класс и прекрасный способ приобрести опыт работы с многопоточным доступом к элементам управления Windows Forms на практике.

Материалы по платформе

Материалы по темам