Stathread c что это

STAThread Attribute Класс

Определение

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

Указывает, что потоковой моделью COM для приложения является однопотоковое подразделение (STA).

Комментарии

Примените этот атрибут к методу точки входа ( Main() метод в C# и Visual Basic). Он не влияет на другие методы. Чтобы задать состояние апартамента потоков, запускаемых в коде, используйте Thread.SetApartmentState Thread.TrySetApartmentState метод или перед запуском потока.

Общие сведения о потоковых моделях COM см. в разделе Основные сведения и использование потоковых моделей COM.

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

ЯзыкМодель подразделения COM
C#Многопоточное подразделение
C++Многопоточное подразделение
Visual BasicОднопотоковое подразделение

Чтобы изменить эти значения по умолчанию, используйте STAThreadAttribute атрибут, чтобы задать потоковую модель для приложения, или Thread.SetApartmentState Thread.TrySetApartmentState перед запуском потока для установки потоковой модели для конкретного потока необходимо вызвать метод или. В C++ можно также использовать параметр компоновщика /CLRTHREADATTRIBUTE для указания модели подразделения.

Ниже приведены некоторые случаи, в которых необходимо использовать STAThreadAttribute атрибут, чтобы явно установить потоковую модель в однопотоковое подразделение:

вы разрабатываете приложение Windows Forms. Windows приложения форм должны быть однопотоковыми, если они взаимодействуют с Windowsными компонентами системы, такими как буфер обмена или Windows общие диалоговые окна, или если они используют системные функции, такие как функции перетаскивания. шаблон приложения Windows Forms для c# автоматически добавляет STAThreadAttribute атрибут в проекты c#. так как модель апартамента с одним потоком используется по умолчанию для Visual Basic, атрибут не требуется.

вы разрабатываете приложение C#, которое вызывает библиотеку Visual Basic, которая, в свою очередь, использует COM-взаимодействие. поскольку модель однопотокового подразделения используется по умолчанию для Visual Basic, следует изменить модель потоков приложения на однопотоковое с помощью STAThreadAttribute атрибута.

Приложение выполняет вызовы COM-компонентов, использующих модель однопотокового подразделения.

Конструкторы

Инициализирует новый экземпляр класса STAThreadAttribute.

Свойства

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

Методы

Возвращает значение, показывающее, равен ли экземпляр указанному объекту.

Возвращает хэш-код данного экземпляра.

Возвращает объект Type для текущего экземпляра.

При переопределении в производном классе указывает, является ли значение этого экземпляра значением по умолчанию для производного класса.

При переопределении в производном классе возвращает значение, указывающее, является ли этот экземпляр равным заданному объекту.

Создает неполную копию текущего объекта Object.

Возвращает строку, представляющую текущий объект.

Явные реализации интерфейса

Сопоставляет набор имен соответствующему набору идентификаторов диспетчеризации.

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

Возвращает количество предоставляемых объектом интерфейсов для доступа к сведениям о типе (0 или 1).

Предоставляет доступ к открытым свойствам и методам объекта.

Источник

Stathread c что это. tick. Stathread c что это фото. Stathread c что это-tick. картинка Stathread c что это. картинка tickЧто такое атрибуты и зачем они нужны?
Нужен человек, который может объяснить, что такое атрибуты и зачем они нужны, на как можно более.

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

Stathread c что это. tick. Stathread c что это фото. Stathread c что это-tick. картинка Stathread c что это. картинка tickЧто такое абстрактные классы и зачем они нужны?
Добрый день, форум, решил немного расширить свои знания c# и начал с абстрактных классов(раньше.

Решение

Что-то вроде метаданных для типов и их членов. Дополнительная информация, которая не вписывается в модель, чтобы быть полноценным членом типа, но которая может помочь с принятием решения о типе или его члене/членах сторонним классам.

Примерно затем же, зачем аудио-файлам: полезная нагрузка в них — это байты, которые идут на аудио-плату и преобразуются в звук; но к файлу можно подцепить дополнительные атрибуты: исполнитель, год записи, альбом, номер трека и пр. Сами по себе эти данные никак на воспроизводимый звук не влияют, но какой-нибудь сторонний проигрыватель может их считать и показать пользователю эту информацию.
То же самое с атрибутами: они позволяют сторонним классам получать какую-нибудь дополнительную информацию о вашем типе. Если вы хотите, чтобы ваш тип хорошо «дружил» с такими классами, вы помечаете свой тип и его члены интересующими этот сторонний класс атрибутами.
Посмотрите, например, как работает XML-сериализация через класс XmlSerializer. Имеется куча атрибутов, о которых этот класс знает и с помощью которых можно управлять тем, как именно он будет сериализовать экземпляры вашего класса.
Если вы пишете свои компоненты под Windows Forms, то можете использовать специальные атрибуты, о которых знает код дизайнера форм в студии: к какой категории относится ваш компонент или его свойства, какой редактор использовать для его изменения в дизайнере, показывать ли свойство в дизайнере и т.д.
Есть атрибуты специально для компилятора, например Conditional. Если компилятор увидит этот атрибут на вашем методе, то все вызовы данного метода будут сгенерированы только тогда, когда определен указанный символ.

Никто, разумеется, не запрещает вам создавать свои атрибуты и потом использовать их в своем же коде.

Источник

Что делает [STAThread]?

Я изучаю C# 3.5 и я хочу знать, что [STAThread] есть ли в наших программах?

3 ответов:

на STAThreadAttribute по существу, это требование к насосу сообщений Windows для связи с компонентами COM. Хотя ядро Windows Forms не использует COM, многие компоненты ОС, такие как системные диалоги, используют эту технологию.

MSDN объясняет причину чуть подробнее:

STAThreadAttribute указывает, что Модель com продевая нитку для приложение является однопоточным квартира. Этот атрибут должен быть присутствует на точка входа любого приложение, которое использует Windows Forms; если он опущен, то окна компоненты могут работать неправильно. Если атрибут отсутствует, то приложение использует многопоточность модель квартиры, которой нет поддерживается для Windows Forms.

этот блог (почему STAThread требуется?) также объясняет требование довольно хорошо. Если вы хотите получить более глубокое представление о том, как работает потоковая модель в Уровень CLR, см. эта статья журнала MSDN с июня 2004 года (Архив, Апреля. 2009).

Читать подробнее здесь (Архивировано, Июнь 2009)

Он сообщает компилятору, что вы находитесь в одной потоковой модели квартиры. Это злая вещь COM, она обычно используется для Windows Forms (GUI), поскольку она использует Win32 для своего рисования, который реализован как STA. Если вы используете что-то, что является моделью STA из нескольких потоков, вы получаете поврежденные объекты.

вот почему вы должны вызвать на Gui из другого потока (если вы сделали какие-либо формы кодирования).

в основном, не беспокойтесь об этом, просто примите, что Потоки графического интерфейса Windows должны быть помечены как STA, иначе происходят странные вещи.

Источник

Работа с потоками в C#


Часть 2


Автор: Joseph Albahari
Перевод: Алексей Кирюшкин
The RSDN Group
Источники: Threading in C#
базируется на книге
Joseph Albahari Ben Albahari «C# 3.0 in a Nutshell»
Материал предоставил: RSDN Magazine #2-2007

Опубликовано: 27.06.2007
Исправлено: 15.04.2009
Версия текста: 1.0

Stathread c что это. Albahari book. Stathread c что это фото. Stathread c что это-Albahari book. картинка Stathread c что это. картинка Albahari book Stathread c что это. mag0207. Stathread c что это фото. Stathread c что это-mag0207. картинка Stathread c что это. картинка mag0207

3. Работа с потоками


Апартаменты и Windows Forms

Апартамент – логический “контейнер” для потоков. Апартаменты бывают двух видов – “single” (однопоточные) и “multi” (многопоточные). Однопоточный апартамент может содержать только один поток, многопоточный – любое количество потоков. Однопоточная модель используется чаще и имеет большие возможности для взаимодействия.

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

Библиотека-контекст синхронизации позволит войти любому человеку, но только одному одновременно. Если людей больше – перед библиотекой образуется очередь.

Библиотека-апартамент имеет свой штат – одного библиотекаря для однопоточной библиотеки или целую группу для многопоточной. Никто, кроме штатных библиотекарей, не может войти в библиотеку. Клиент, желающий провести исследование, должен посигналить библиотекарю, а затем попросить его выполнить задание! Вызов библиотекаря называется маршалингом – клиент выполняет маршалинг своего вызова штатному сотруднику. Маршалинг выполняется автоматически и реализуется через прокачку сообщений – в Windows Forms этот механизм постоянно следит за событиями клавиатуры и мыши от операционной системы. Если обработка не успевает за новыми сообщениями, создается очередь сообщений, обрабатываемая в порядке поступления.

Назначение типа апартамента

Можно также запросить, чтобы главный поток приложения вошел в однопоточный апартамент, используя атрибут STAThread для метода main :

Control.Invoke


BackgroundWorker

BackgroundWorker – класс-обертка из пространства имен System.ComponentModel для управления рабочими потоками. Он обеспечивает следующие возможности:

Последние две возможности особенно полезны – они означают, что можно не добавлять try/catch в рабочий метод и обновлять элементы управления без использования Control.Invoke.

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

Вот минимально необходимые действия для использования BackgroundWorker :

Чтобы добавить отображение выполнения операции:

Код в обработчике ProgressChanged может свободно обращаться к элементам управления UI так же, как и в RunWorkerCompleted. Обычно это нужно для обновления индикатора прогресса.

Чтобы иметь возможность отмены операции:

Вот пример, реализующий обе описанные возможности:

Консольный вывод (Enter нажат во время работы BackgroundWorker):

Консольный вывод (работа BackgroundWorker не прерывалась):

Наследование от BackgroundWorker

Можно реорганизовать его следующим образом:

Возможность использования BackgroundWorker хоронит старую “асинхронную модель, основанную на событиях”.

ReaderWriterLock

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

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

Поскольку добавление элементов в список происходит быстрее, чем удаление, в этом примере в метод AppendItem добавлен вызов SpinWait для сохранения баланса.

Пулы потоков

Для использования пула потоков нужно зарегистрировать WaitHandle и делегат, который должен быть исполнен, когда WaitHandle будет установлен. Это делается вызовом ThreadPool.RegisterWaitForSingleObject, как в следующем примере:

Консольный вывод (после пятисекундной задержки):

В дополнение к WaitHandle и делегату RegisterWaitForSingleObject принимает объект-“черный ящик”, который будет передан в ваш делегат (как это было для ParameterizedThreadStart ), таймаут в миллисекундах (-1, если таймаут не нужен) и флаг, указывающий, является запрос одноразовым или повторяющимся.

Вызов Abort для потоков пула – это тоже плохая идея. Потоки пула должны повторно использоваться в течение всей жизни приложения.

Если необходимо передать в целевой метод более одного объекта, можно или создать объект с нужными свойствами, или использовать анонимный метод. Например, если метод Go требует два целочисленных параметра, можно запустить его следующим образом:

Другой путь в пул потоков – через асинхронные делегаты.

Асинхронные делегаты

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

Цена, которую нужно заплатить – асинхронная модель. Чтобы понять, что это значит, сначала рассмотрим обычную, синхронную модель программирования. Скажем, нужно сравнить две web-страницы. Можно последовательно загрузить каждую страницу, а затем сравнить их примерно так:

Вот как можно использовать асинхронные делегаты для загрузки двух web-страниц, с одновременным выполнением других вычислений:

Мы начинаем с объявления и создания делегатов для методов, которые хотим исполнить асинхронно. В этом примере нам нужны два делегата – каждый для отдельного объекта WebClient ( WebClient не допускает параллельного доступа, если бы это было возможно, мы использовали бы один единственный делегат).

Асинхронные методы

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

Если вам просто нужно параллельное выполнение, лучше вызовите синхронную версию метода (например NetworkStream.Read ) через асинхронный делегат. Другая возможность – использовать ThreadPool.QueueUserWorkItem или BackgroundWorker — либо просто создать новый поток.

Асинхронные события


Таймеры

Таймер Windows Forms предназначен для заданий, которые могут привести к обновлению пользовательского интерфейса, и которые должны выполняться достаточно быстро. Быстрота выполнения важна, так как событие Tick вызывается в главном потоке, а значит, во время его выполнения интерфейс не будет отвечать на действия пользователя.

Локальные хранилища

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

Метод Thread.GetData читает из изолированной области данных потока, Thread.SetData пишет в нее. Оба метода требуют в качестве параметра объект LocalDataStoreSlot (на самом деле это только обертка для строки с именем слота) для идентификации слота. Один и тот же объект LocalDataStoreSlot может быть использован из любого потока для получения им своих локальных данных. Вот пример:

4. Дополнительные материалы


Неблокирующая синхронизация


Атомарность и Interlocked

Инструкция является атомарной, если она выполняется как единая, неделимая команда. Строгая атомарность препятствует любой попытке вытеснения. В C# простое чтение или присвоение значения полю в 32 бита или менее является атомарным (для 32-битных CPU). Операции с большими полями не атомарны, так как являются комбинацией более чем одной операции чтения/записи:

Чтение и запись 64-битных полей не атомарны на 32-битных CPU, так при этом используются два 32-битных участка памяти. Если поток A читает 64-битное значение, в то время как поток B обновляет его, поток A может получить битовую комбинацию из старого и нового значений.

Унарные операторы типа x++ сначала читают переменную, затем обрабатывают ее, а потом записывают новое значение. Рассмотрим следующий класс:

Один из путей решения таких проблем – обернуть неатомарные операции в блокировку. Блокировка фактически моделирует атомарность. Однако класс Interlocked предлагает более простое и быстрое решение для простых атомарных операций:

Барьеры в памяти и асинхронная изменчивость (volatility)

Рассмотрим следующий класс:

Внимание, вопрос: насколько существенная задержка может разделять «Понеслась. » от «Готово» – другими словами, может ли цикл в методе Wait продолжать крутиться после того, как флаг endIsNigh был установлен в true? И еще, может ли метод Wait напечатать «Готово, false»?

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

Это кэширование можно обойти, используя статические методы Thread.VolatileRead и Thread.VolatileWrite для чтения и записи полей. VolatileRead – это способ “читать последнее значение”; VolatileWrite означает “записать немедленно в память”. Того же эффекта можно достичь более изящно, объявлением полей с модификатором volatile :

Использование оператора lock было бы необходимо, если бы нужно было получить доступ к полям repented и endIsNigh атомарно, например, выполнить что-то типа такого:

Wait и Pulse

Ранее мы рассматривали EventWaitHandle – простой сигнальный механизм блокировки потока до получения уведомления от другого потока.

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

Определение Wait и Pulse

Назначение Wait и Pulse – обеспечить простой сигнальный механизм: Wait блокирует, пока не получено уведомление от другого потока, Pulse реализует это уведомление.

Например, если x объявлен следующим образом:

то следующий код заблокирует поток на вызове Monitor.Wait :

А этот код (если он выполнен позже в другом потоке) освободит блокированный поток:

Переключение блокировки

Чтобы выполнить эту работу, Monitor.Wait временно освобождает или отключает базовый lock на время ожидания, чтобы другой поток (который будет вызывать Pulse ) тоже мог получить блокировку. Метод Wait можно представить в виде следующего псевдокода:

Wait логически разворачивается следующим образом:

Согласно нормальной семантике блокировки, только первый вызов Monitor.Enter действительно может произвести блокировку.

Сигнализация и подтверждения

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

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

Полагаясь на своевременные действия потока, ожидающего сигнала, без специального механизма подтверждений вы проиграете!

Очередь ожидания и PulseAll

Вызвать Wait на одном и том же объекте могут сразу несколько потоков – в этом случае за объектом синхронизации образуется очередь ожидания, «waiting queue» (не путать c очередью ожидания на lock – «ready queue»). Каждый Pulse освобождает один поток из головы очереди ожидания, «waiting queue», после чего он переходит к «ready queue» для переустановки блокировки. Можно провести аналогию с автоматической парковкой для машин – сначала вы стоите в очереди к автомату для проверки билетов («waiting queue»), а потом ждете снова перед шлагбаумом на входе («ready queue»).

Stathread c что это. waitpulse. Stathread c что это фото. Stathread c что это-waitpulse. картинка Stathread c что это. картинка waitpulse
Рисунок 2: Waiting Queue и Ready Queue

Использование Pulse и Wait

Итак, начнем. И для начала условимся о следующем:

Имея в виду эти два правила, рассмотрим простой пример: рабочий поток, который приостанавливается, пока не получит уведомление от главного потока:

Вот метод Main, приводящий все это в движение:

Теперь запустим пример, чтобы убедиться, что он действительно работает. Вот что он выводит:

Вот модифицированный класс, с опущенными для краткости вызовами Console :

Обобщение модели использования Wait и Pulse

Теперь вставим в наш шаблон Wait и Pulse так же, как и в прошлый раз:

Вот модифицированный псевдокод:

Важно также, что Pulse не принуждает ожидающий поток к возобновлению исполнения. Скорее Pulse только уведомляет его, что кое-что изменилось, рекомендуя перепроверить условие блокировки. Не сигнализирующий, а сам ожидающий поток определяет (в следующей итерации цикла), нужно ли ему продолжать работу или остаться в заблокированном состоянии. Преимущество такого подхода – возможность использования сложных условий блокировки без замысловатой логики синхронизации.

Очередь поставщик/потребитель

В следующем примере для представления задачи будет использоваться строка. А очередь задач будет, соответственно, такой:

Поскольку очередь будет использоваться из нескольких потоков, необходимо обернуть в lock все операции чтения и записи в очередь. Так будет выглядеть постановка задачи в очередь:

Следующий шаг – рабочий поток удаляет задачу из очереди и исполняет её:

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

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

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

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

Про экономию сигналов

Посмотрим еще раз на добавление задачи в очередь:

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

Сэкономим мы, однако, немного, так как выдача сигнала занимает менее микросекунды и не влечет за собой накладных расходов для занятых потребителей, поскольку они этот сигнал все равно игнорируют. Правильной практикой для многопоточного кода будет удаление любой необязательной логики: плохо воспроизводимый дефект из-за глупой ошибки – слишком большая цена за экономию одной микросекунды! Вот пример внесения блуждающей ошибки типа “застрявший потребитель”, которая наверняка не будет выявлена при первоначальном тестировании (найди одно отличие):

Сигнализация без всяких условий защитит вас от этого типа ошибок.

СОВЕТ

Сомневаешься – сигналь! В рассмотренном шаблоне проектирования сигнализация редко может повредить.

Pulse или PulseAll?

Вспомним отличие: при использовании Pulse может пробудиться максимум один поток (и перепроверить условие блокирования в while ); в случае PulseAll пробудятся все ждущие потоки (и перепроверят условие блокирования). Если в очередь добавляется одна задача, для обработки нужен только один потребитель, так что нужно разбудить только одного вызовом Pulse. Это походит на класс спящих детей – если есть только одно мороженое, нет смысла будить их всех и ставить в очередь.

Цена одного невызванного Pulse – застрявший поток-потребитель. Эта ошибка будет блуждающей, так как вызов Pulse производит эффект, только когда потребитель находится в состоянии ожидания. Следовательно, можно расширить наш предыдущий лозунг “Сомневаешься – сигналь” до “Сомневаешься – сигналь всем!”

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

Использование таймаутов для Wait

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

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

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

Гонки и подтверждения

Допустим, мы хотим посигналить рабочему потоку пять раз подряд:

При тестировании этого примера мы получили «ожидаемый результат», запуская программу, скомпилированную в debug-режиме (не из под отладчика), и «реальный результат» (то есть, зависание) в release-версии – прим.ред.

Эта программа с дефектом: цикл for в главном потоке может прокрутить все пять своих итераций в то время, когда рабочий поток не блокирован. Возможно, даже до того, как он вообще стартует! Наш пример с очередью Поставщик/Потребитель не страдал от этой проблемы, так как если главный поток забегал вперёд рабочего, каждый запрос просто ставился в очередь. Но в данном случае требуется блокировка главного потока на каждой итерации, если рабочий все еще занят предыдущей задачей.

В качестве простого решения можно в каждой итерации цикла for ожидать, пока флаг go не будет сброшен рабочим потоком. Рабочий поток после сброса флага должен вызвать Pulse :

Важная особенность такой программы заключается в том, что рабочий поток освобождает блокировку перед выполнением длительного задания (которое должно быть на месте вызова Console.WriteLine ). Это гарантирует, что инициатор не будет заблокирован все время, пока рабочий поток исполняет задачу, о которой ему только что посигналили (и будет блокирован, только если рабочий поток занят предыдущей задачей).

В данном примере только один поток (главный) сигналит рабочему потоку о необходимости выполнить задачу. Если несколько потоков начнут сигналить рабочему – используя текущую логику из метода Main – у нас начнутся проблемы. Два сигналящих потока могли бы последовательно исполнить следующую строку кода:

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

Имитация Wait Handle

Возможно, вы заметили шаблонный код в предыдущем примере: оба цикла ожидания имеют следующую структуру:

Имитация статических методов, которые работают с несколькими WaitHandle, в большинстве случаев проста. Эквивалент вызова WaitAll с несколькими WaitHandle – не что иное, как условие блокировки, включающее все флаги, используемые вместо WaitHandle :

Это может быть особенно полезно, так как WaitAll в большинстве случаев не пригоден к использованию из-за проблем с унаследованным COM-кодом. Имитация WaitAny – просто вопрос замены оператора && оператором ||.

Wait и Pulse vs. Wait Handles

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

В смысле производительности Wait и Pulse имеют небольшой перевес, если следовать вот такому образцу дизайна:

Suspend и Resume

Можно, однако, безопасно вызывать Suspend для текущего потока, реализовав при этом простой механизм синхронизации для рабочего потока в цикле выполнение задачи/вызов Suspend для себя/ожидание вызова Resume (“побудки”) главным потоком, когда будет готова следующая задача. Сложность заключается в определении, действительно ли рабочий поток сейчас приостановлен. Посмотрите следующий код:

Нерекомендуемые методы Suspend и Resume имеют два режима – опасный и бесполезный.

Аварийное завершение потоков

Поток может быть аварийно завершен при помощи метода Abort :

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

Abort будет воздействовать на поток в любом состоянии – рабочем, заблокированном, приостановленном или остановленном. Если, однако, принудительно завершается приостановленный поток, генерируется ThreadStateException – на сей раз в вызывающем потоке – и принудительное завершение не может быть произведено, пока поток не возобновит работу. Вот как можно принудительно завершить приостановленый поток:

Сложности с Thread.Abort

C#-оператор using – на самом деле просто сокращенная запись для следующего кода:

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

Поскольку дизассемблирование каждого используемого вызова CLR, очевидно, непрактично, встает вопрос, как же писать методы, которые можно без проблем аварийно завершать. Самый очевидный выход из ситуации – не завершать аварийно другие потоки вообще, а использовать специальное булево поле, через которое сигнализировать потоку о необходимости завершения. Рабочий поток должен периодически проверять это поле, элегантно завершаясь, если оно установлено в true. Как ни странно, самым элегантным завершением для рабочего потока будет вызов Abort для себя самого, подойдет также явный выброс исключения. Это гарантирует правильную очистку потоков в процессе выполнения блоков catch/finally – аналогично вызову Abort из другого потока, за исключением того, что исключение генерируется в выбранном нами месте:

Завершение домена приложений

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

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

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

Завершение процессов

Еще один вариант, при котором поток может завершиться – завершение родительского процесса. Пример – рабочий поток с установленным в true свойством IsBackground, и главный поток, завершающийся, когда рабочий еще исполняется. Фоновый поток не способен продлить жизнь приложения, так что процесс завершается, унося с собой фоновый поток.

Когда поток завершается из-за родительского процесса, он останавливается намертво, никакие блоки finally не выполняются.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *