Разработка многопоточных приложений на .Net под Windows:...

73
Разработка многопоточных приложений на .Net под Windows: основы Михаил Дутиков CUSTIS ноябрь 2011 года

description

Разработка многопоточных приложений на .Net под Windows: основы. Михаил Дутиков CUSTIS ноябрь 2011 года. Почему именно я об этом рассказываю ?. Речь пойдет о следующем :  - Потоки в Windows, кванты , приоритеты , переключения контекста , планировщик . - PowerPoint PPT Presentation

Transcript of Разработка многопоточных приложений на .Net под Windows:...

Page 1: Разработка  многопоточных приложений на .Net под Windows: основы

Разработка многопоточных приложений на .Net под

Windows: основы

Михаил Дутиков

CUSTISноябрь 2011 года

Page 2: Разработка  многопоточных приложений на .Net под Windows: основы

Почему именно я об этом рассказываю?

Речь пойдет о следующем: - Потоки в Windows, кванты, приоритеты, переключения контекста, планировщик. - Потоки в .Net. Явное создание и взаимодействие. - Архитектура многопоточных приложений (уровень классов). Управление состоянием. Изоляция, неизменяемость, синхронизация. - Синхронизация: объекты ядра, пользовательского режима, гибриды. Сценарии использования. - Потокобезопасный код. Пример агента и потокобезопасной обертки. - Пул потоков. - Таймеры; - Исключения;

Page 3: Разработка  многопоточных приложений на .Net под Windows: основы

Процессы и потоки в Windows -- Капитан ОчевидностьWindows многозадачна -- в ней работает несколько процессов одновременно. Грубо говоря, процесс -- экземпляр программы. Процессы изолированы, не имеют доступа к адресным пространствам друг друга.

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

Page 4: Разработка  многопоточных приложений на .Net под Windows: основы

Процессорное время: планировщик

- Как Windows распределяет процессорное время между потоками?- Использует вытесняющую многопоточность.

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

Page 5: Разработка  многопоточных приложений на .Net под Windows: основы

Процессорное время: планировщик

• Планировщик игнорирует процессы, раздает ядра только потокам.

• Планировщик гарантирует: поток с самым высоким приоритетом, готовый к работе, сейчас выполняется.

• Потокам с одинаковым приоритетом при прочих равных достается примерно одинаково процессорного времени.

Page 6: Разработка  многопоточных приложений на .Net под Windows: основы

Приоритет

Число от 0 до 31. Диапазон от 1 до 15 -- динамический. От 16 до 31 -- реального времени.

У процесса есть "класс приоритета": Idle (4), Below Normal (6), Normal (8), Above Normal (10), High (13), Realtime (24).

Приоритет потока задается относительно класса того процесса, которому поток принадлежит. Относительный приоритет потока, где P - приоритет процесса: Idle (нет в .Net), Lowest (P-2), Below Normal (P-1), Normal (P), Above Normal (P+1), Highest (P+2), Time-critical (нет в .Net).

Page 7: Разработка  многопоточных приложений на .Net под Windows: основы

Приоритеты

Page 8: Разработка  многопоточных приложений на .Net под Windows: основы

Приоритеты: использовать?

Если ими вообще пользоваться, то лучше избегать классов High и Realtime.

Высокоприоритетным потокам лучше всего давать короткие задачи.

Лучше снижать приоритеты потокам, чем повышать.

Page 9: Разработка  многопоточных приложений на .Net под Windows: основы

Когда Windows повышает приоритеты потокам (самые важные случаи)

- всем потокам в системе, которые не работали 4 секунды или больше, приоритет временно повышается до 15.

- поток, ждавший и дождавшийся события через объект ядра (о них --дальше), получает +1 к приоритету.

Бонусы Windows дает на время. Как она их отбирает?

Page 10: Разработка  многопоточных приложений на .Net под Windows: основы

Переключение контекста

При переключении контекста Windows:

• сохраняет содержимое регистров и другие данные в контекст потока (в объекте ядра ОС);

• переключает виртуальное адресное пространство, если новый поток из другого процесса;

• загружает контекст того потока, который вот-вот получит процессорное время (наполняет регистры процессора и проч.);

Page 11: Разработка  многопоточных приложений на .Net под Windows: основы

Переключение контекста: чем плохо?

 - само по себе переключение -- это чистый оверхед; - остывают процессорные кэши, падает производительность;

Для борьбы с проблемой №2 Windows назначает потоку "идеальный процессор" при создании и запоминает, на каком процессоре поток выполнялся в последний раз. У этих двух процессоров больше шансов получить этот же поток снова, чем у остальных.

Page 12: Разработка  многопоточных приложений на .Net под Windows: основы

Кванты

Чему равен квант?

На клиентских версиях Windows (XP, 2000, Vista, 7) по умолчанию -- примерно 2 интервала между срабатываниями системного таймера. На серверных версиях ОС -- 12 интервалов. На большей части железа этот интервал составляет 10-15 мс.

Почему такая разница для клиентских и серверных систем?

- 2 способа поменять квант-

- подхачить реестр

Windows утраивает квант (т.н. quantum boost) для потоков, принадлежащих foreground-процессу (если выбрано "программ" в настройках системы -- см. картинку).

Page 13: Разработка  многопоточных приложений на .Net под Windows: основы

Где прочесть больше о планировщике Windows и потоках с точки зрения ОС

Page 14: Разработка  многопоточных приложений на .Net под Windows: основы

Каковы ваши вопросы по вводной части?

Page 15: Разработка  многопоточных приложений на .Net под Windows: основы

Многопоточные приложения: зачем?

•  более эффективное использование ресурсов компьютера;

•  повышение производительности;

•  повышение юзабилити (в UI);

•  более удобная программная модель (редко);

Page 16: Разработка  многопоточных приложений на .Net под Windows: основы

Многопоточные приложения: почему нет?

Их гораздо сложнее разрабатывать чем однопоточные: - у них более сложный control flow; - их отладка мало что дает в смысле поиска багов; - их сложнее тестировать; - состояние системы/компонента/класса труднее держать консистентным, если оно меняется больше чем одним потоком одновременно; - добавляют уникальные классы проблем -- race condition, deadlock и проч.;

Page 17: Разработка  многопоточных приложений на .Net под Windows: основы

Потоки в .Net: знакомимся ближе

Поток -- объект ядра ОС. Помимо прочего хранит контекст исполнения. Поток в .Net -- скромная обертка над этим объектом.

Потоку выделен стек пользовательского режима -- 1 Мб. И стек режима ядра -- 12 Кб (32-битная Windows), 24 Кб (64-битная). Еще есть другие структуры...

Состояния: Unstarted, Running, WaitSleepJoin, Suspended, Stopped, Aborted.

Page 18: Разработка  многопоточных приложений на .Net под Windows: основы

Создание потока: явное (пример)

Foreground-поток. 1 Мб стека.

Page 19: Разработка  многопоточных приложений на .Net под Windows: основы

Потоки: уничтожение

4 способа уничтожить поток:- выход из метода потока;- необработанные исключения (finally-блоки из стека отрабатывают);- Thread.Abort();- завершение процесса (background-потоки);

Page 20: Разработка  многопоточных приложений на .Net под Windows: основы

Поток как объект

Что можно делать с потоком, созданным явно - Start(); - Join(); - Abort(); // не лучшая идея в общем случае - Priority; - CurrentCulture; - CurrentUICulture; - IsThreadPoolThread; - IsBackground; - ... (остальное не рассматриваем)

 - Thread.Sleep(...); // зависит от точности таймера, нагрузки на систему - Thread.ResetAbort(...); // нужны права - Thread.CurrentThread; - ... (остальное не рассматриваем)

Page 21: Разработка  многопоточных приложений на .Net под Windows: основы

Потоки: взаимодействие (пример с багом)

Page 22: Разработка  многопоточных приложений на .Net под Windows: основы

Архитектура: управление состоянием в многопоточной среде

Общее состояние часто неизбежно.

Три главных приема для управления состоянием:

- неизменяемое общее состояние;

- изоляция;

- синхронизация (потоки координируют свои действия в отношении общего состояния);

Page 23: Разработка  многопоточных приложений на .Net под Windows: основы

Архитектура: управление состоянием, неизменяемость

"Неизменяемые" объекты могут быть:- поверхностно неизменяемыми;- глубоко неизменяемыми;

Также полезны условно неизменяемые типы.

Page 24: Разработка  многопоточных приложений на .Net под Windows: основы
Page 25: Разработка  многопоточных приложений на .Net под Windows: основы

Архитектура: управление состоянием, изоляция

- процессы;- домены;- соглашения;

Пример изоляции. Агент. Его свойства: 1) состояние изолировано 2) общается с миром при помощи асинхронных сообщений 3) слабо связан с другими агентами. Пример агента сегодня будет в виде асинхронного логгера.

Page 26: Разработка  многопоточных приложений на .Net под Windows: основы

Архитектура: управление состоянием, синхронизация

Синхронизация почти неизбежна.

Дальше -- подробнее о синхронизации.

Page 27: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация: условная классификация

Синхронизация контроля (как вариант: поток А ждет, пока поток Б сделает что-то нужное потоку А).

Синхронизация доступа к данным.

Page 28: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация: объекты ядра Windows, общие сведения

Как ждать событий от других потоков?

Busy wait?while (!predicate){ BusyWait;}

Для длительного ожидания нужны вещи поэффективнее!

Как работают объекты ядра?

2 состояния: сигнальное и несигнальное.

kernelObject.WaitSignal(...);

Page 29: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация: объекты ядра Windows, общие сведения

Объекты ядра: - хороши для длительных периодов ожидания; - могут использоваться для межпроцессной синхронизации; - не слишком быстры, тяжеловесны (не забывайте о Dispose!); - нечестны (не гарантируют FIFO);

Page 30: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация: объекты ядра Windows -- WaitHandle

Базовый класс для всех .Net- оберток к объектам ядра, служащим для синхронизации.

На вызове Wait*(...) может блокироваться любое число потоков.

* -- waitHandles: не больше 64 штук.

Page 31: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Объекты ядра. Мьютекс

Mutex = Mutual exclusion. Именно это основной сценарий использования.

Пример использования:

Page 32: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Объекты ядра. Мьютекс. Чиним race condition

Page 33: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Объекты ядра. Семафор

Служит для ограничения доступа к пулу ресурсов. Дает одновременный доступ не больше чем N интересующимся (N указывается при создании семафора). Остальные ждут.

Потокобезопасный счетчик.

- WaitOne(...);- Release(...);

Не привязывается к потокам, как мьютекс.

Page 34: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Объекты ядра. Семафор: пример

Page 35: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Объекты ядра. ManualResetEventКогда сигналит, пропускает любое количество ждущих потоков. Метафора для сигнального состояния -- распахнутые ворота.

Page 36: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Объекты ядра. AutoResetEventКогда сигналит, пропускает строго один ждущий поток. Метафора для сигнального состояния -- шлюз офиса CUSTIS в Архангельском пер.

Page 37: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Объекты ядра. События и баги

Значительная часть багов синхронизации, что я исправлял в production-коде разных команд, была связана с событиями.

События не запоминают, сколько раз вы вызываете Set(...).

Если вызываете Set(...), то точно не известно, в какой момент времени будут отпущены все потоки, ждущие события.

Bottom line: лучше используйте ManualResetEvent для однократного ожидания (Set => отпускаем всех ждущих, Reset не трогаем).

Page 38: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Объекты ядра. Потоки и ожидание

Не обязательно создавать поток только чтобы ждать какого-то события (потоки дороги). Пул потоков это отлично сделает за вас. См. ThreadPool.RegisterWaitForSingleObject(...).

К пулу мы еще сегодня вернемся :)

Page 39: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Пользовательский режим

- Interlocked-методы;- Thread.VolatileRead(...); [не сегодня]- Thread.VolatileWrite(...); [не сегодня]- SpinLock; [не сегодня]

Page 40: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Пользовательский режим: Interlocked-методыОперации типа Compare-And-Swap дают возможность совершить операцию, предполагающую атомарное, потокобезопасное чтение из ячейки памяти и запись в нее.

Page 41: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Пользовательский режим: Interlocked-методы

Под капотом -- это специальные процессорные инструкции.

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

Page 42: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Пользовательский режим: Interlocked-методы (чиним race condition)

Page 43: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Гибридные конструкции (user-mode/kernel mode)

Новые в .Net FW 4 обертки над объектам ядра: ManualResetEventSlim, SemaphoreSlim.

Чем они лучше прежних?

Еще мы поговорим о: Monitor, ReaderWriterLockSlim, CountdownEvent.

Page 44: Разработка  многопоточных приложений на .Net под Windows: основы

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

Page 45: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Гибридные конструкции. Monitor (чиним race condition)

Page 46: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Гибридные конструкции. Monitor

• Потоки, которым не удалось попасть в "критический регион", ждут своей очереди в течение указанного в Monitor.Enter(...) интервала. По умолчанию ждут бесконечно.

• Синхронизация кэшей -- другие потоки видят все изменения, но только если используют тот же монитор.

• Ожидание: Spin + AutoResetEvent.

• Зачем Monitor.Enter и Exit нужен объект -- экземпляр System.Object?

Page 47: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Гибридные конструкции. Monitor. Ключевое слово lock (пример)

Page 48: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Гибридные конструкции. Monitor. Хорошо известные косяки

• Синхронизация на разных объектах = отсутствие синхронизации.• Синхронизация с монитором на Value-типах.• Синхронизация с монитором на Domain-neutral-объектах

(typeof(Int32), интернированных строках, просто строках между доменами и др.).

• Синхронизация с монитором по ссылке на текущий объект (this) или ссылке на что-то еще не инкапсулированное (+ MethodImplAttribute).

• При использовании нескольких мониторов возможны дедлоки.

Page 49: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Гибридные конструкции. Monitor. Deadlock

Page 50: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Гибридные конструкции. ReaderWriterLockSlim

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

- Блокировка на чтение (EnterReadLock...);- Блокировка на запись (EnterWriteLock...);- Upgradeable-блокировка (только 1 поток) -- EnterUpgradeableReadLock;

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

Скорость.

Не используйте старый ReaderWriterLock: - он медленнее; - имеет проблемы с дизайном; - может приводить к дедлокам; - отпускает блокировки при апгрейде, нарушая атомарность;

Page 51: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Гибридные конструкции. CountdownEventПотокобезопасный счетчик, который находится в сигнальном состоянии, когда равен нулю, в несигнальном -- в остальных случаях. Сам по себе -- не объект ядра, хотя и называется event-ом.

Page 52: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Потокобезопасные классы (архитектура)• Что такое потокобезопасный класс?

• Самый простой, надежный и корректный способ

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

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

• Такой подход (вернее, его абстрактный эквивалент) называют грубым блокированием (coarse-grained). От него иногда переходят к точечному блокированию (fine-grained) и совсем редко избавляются от блокировок с ожиданием вообще (lock-free).

Page 53: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация. Потокобезопасные классы (архитектура)

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

Пример точечного блокирования.

Lock-free сегодня не рассматриваем.

Page 54: Разработка  многопоточных приложений на .Net под Windows: основы

Пример потокобезопасного кода. Логирующий агент, обертка

AsyncLoggerNaive.AsyncLoggerCopying. Потокобезопасная обертка.

Page 55: Разработка  многопоточных приложений на .Net под Windows: основы

Синхронизация: что было и не-

Page 56: Разработка  многопоточных приложений на .Net под Windows: основы

Каковы ваши вопросы о синхронизации, потокобезопасном коде, архитектуре?

Page 57: Разработка  многопоточных приложений на .Net под Windows: основы

Пул потоков CLR

- Существует потому, что потоки дороги;- Принимает задачи в очередь к исполнению на своих потоках;- Рабочие потоки и потоки для callback-ов IO-операций;- Один пул на экземпляр CLR (в большинстве случаев -- на процесс);

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

Page 58: Разработка  многопоточных приложений на .Net под Windows: основы

Отправка задачи в пул (пример)

ThreadPool.QueueUserWorkItem(    delegate        {            BigInteger factorial = 1;

            for (int i = 2; i < 15000; i++)            {                factorial *= i;            }        });

Когда-нибудь один из потоков пула доберется до задачи, которую мы поставили в очередь, и выполнит ее. (Мы не обязательно вынуждаем пул создать поток, у него их может быть достаточно. Все потоки пула -- background.)

Page 59: Разработка  многопоточных приложений на .Net под Windows: основы

Пул потоков

Page 60: Разработка  многопоточных приложений на .Net под Windows: основы

Пул потоков: ожидание (пример)

Как пул ждет?

Page 61: Разработка  многопоточных приложений на .Net под Windows: основы

Ожидание завершения задачи

Неудобно. Приходится писать вручную. Нет нужных абстракций! (О Task Parallel Library -- в другой раз).

Page 62: Разработка  многопоточных приложений на .Net под Windows: основы

Пул потоков: еще факты

• При нормальном завершении процесс не ждет окончания работ, поставленных в пул.

• Необработанное исключение в любой из задач и callback-ов пула ведет к завершению процесса.

• Задачи, поставленные в пул, будут выполнены в случайном порядке.

• Состояние пула можно посмотреть при помощи команды !threadpool в SOS или WinDbg.

Page 63: Разработка  многопоточных приложений на .Net под Windows: основы

Пул потоков: плохие идеи и не-

• Регистрирация ожидания мьютекса -- не лучшая идея.

• Ставить максимальное количество потоков в пуле -- дурная практика (из релиза к релизу это число росло -- было 25, 250 рабочих потоков на ядро, в .Net FW 4 это зависит от платформы -- 1023 или 32768). Если вам нужно менять максимум, вы что-то делаете не так :).

• Установка минимального количества потоков не работала до .Net FW 4 (в 3.5 и более ранних версиях до сих пор исправляется хотфиксом). Ее иногда имеет смысл выполнять.

Page 64: Разработка  многопоточных приложений на .Net под Windows: основы

Таймер (пример)

Page 65: Разработка  многопоточных приложений на .Net под Windows: основы

Пул потоков или явное создание потоков: когда лучше пул?• Пул потоков может ждать события за вас, не создавайте потоки только

чтобы ждать.

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

• Не создавайте потоки явно только чтобы они большую часть времени висели на операциях ввода-вывода или в качестве задач в пуле. Используйте модель асинхронного программирования (еще не добрались до нее).

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

Page 66: Разработка  многопоточных приложений на .Net под Windows: основы

Пул потоков или явное создание потоков: когда создавать потоки явно?

Создавайте поток явно:

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

делаете агента), но помните: одна из самых больших архитектурных ошибок server-side программистов именно в создании и поддержании слишком большого числа потоков.

Page 67: Разработка  многопоточных приложений на .Net под Windows: основы

Каковы ваши вопросы о пуле потоков?

Page 68: Разработка  многопоточных приложений на .Net под Windows: основы

Как быть с исключениями?

Маршалинг исключений. Пример.

Page 69: Разработка  многопоточных приложений на .Net под Windows: основы

Как быть с исключениями?

Что если исключений вызывающая сторона не ждет? Исключения и параллелизм.

Page 70: Разработка  многопоточных приложений на .Net под Windows: основы

Итого

Page 71: Разработка  многопоточных приложений на .Net под Windows: основы

Что еще?

Модель асинхронного программирования (APM) и ввод-вывод в Windows. Асинхрония в архитектуре.

Отменяемые задачи.

Параллелизм, закон Амдала. Task Parallel library. ParallelLINQ.

Серверная сторона: общие советы по использованию потоков.

Debug и Release -- большая разница. Баги в многопоточных приложениях. Как ловить с гарантией? Как писать тесты для потокобезопасных классов? Типичные ошибки разработчиков многопоточных приложений (torn reads, недостаток синхронизации, missed wakeups, deadlock и алгоритм банкира, блокировки и завершение процесса, очереди блокировок, лавины и др.). Потокобезопасные коллекции и типы в .Net FW4. Producer/Consumer.

Многопоточность в GUI (на примере WPF-приложения).

Продвинутая синхронизация (за исключением Interlocked-методов), модели памяти и lock-free алгоритмы. Не поговорили об оптимизации, о code motion, о том, что разные компиляторы себе позволяют (и чего позволить не могут).

Page 72: Разработка  многопоточных приложений на .Net под Windows: основы

Литература

"CLR via C# 2nd Edition"What Every Dev Must Know About Multithreaded Apps

Page 73: Разработка  многопоточных приложений на .Net под Windows: основы

Большое спасибо всем!Особенно -- Леше, Денису, Игорю

и Олегу.