Основной поток программы freereason вызвал критическую ошибку

Обновлено: 28.01.2023

Юрий Балыкин
дата публикации 15-07-2008 03:39

Работа с потоками в Delphi: так ли страшен чёрт, как его малюют?

Данная статья предназначена для начинающих программистов, которые никогда не работали с потоками, и хотели бы узнать основы работы с ними. Желательно, чтоб читатель знал основы ООП и имел какой-нибудь опыт работы в Delphi. Для начала давайте определимся, что под словом «поток» я подразумеваю именно Thread, который еще имеет название «нить».

Нередко встречал на форумах мнения, что потоки не нужны вообще, любую программу можно написать так, что она будет замечательно работать и без них. Конечно, если не делать ничего серьёзней «Hello World» это так и есть, но если постепенно набирать опыт, рано или поздно любой начинающий программист упрётся в возможности «плоского» кода, возникнет необходимость распараллелить задачи. А некоторые задачи вообще нельзя реализовать без использования потоков, например работа с сокетами, COM-портом, длительное ожидание каких-либо событий, и т.д.

Всем известно, что Windows система многозадачная. Попросту говоря, это означает, что несколько программ могут работать одновременно под управлением ОС. Все мы открывали диспетчер задач и видели список процессов. Процесс — это экземпляр выполняемого приложения. На самом деле сам по себе он ничего не выполняет, он создаётся при запуске приложения, содержит в себе служебную информацию, через которую система с ним работает, так же ему выделяется необходимая память под код и данные. Для того, чтобы программа заработала, в нём создаётся поток. Любой процесс содержит в себе хотя бы один поток, и именно он отвечает за выполнение кода и получает на это процессорное время. Этим и достигается мнимая параллельность работы программ, или, как её еще называют, псевдопараллельность. Почему мнимая? Да потому, что реально процессор в каждый момент времени может выполнять только один участок кода. Windows раздаёт процессорное время всем потокам в системе по очереди, тем самым создаётся впечатление, что они работают одновременно. Реально работающие параллельно потоки могут быть только на машинах с двумя и более процессорами.

Для создания дополнительных потоков в Delphi существует базовый класс TThread, от него мы и будем наследоваться при реализации своих потоков. Для того, чтобы создать «скелет» нового класса, можно выбрать в меню File — New — Thread Object, Delphi создаст новый модуль с заготовкой этого класса. Я же для наглядности опишу его в модуле формы. Как видите, в этой заготовке добавлен один метод — Execute. Именно его нам и нужно переопределить, код внутри него и будет работать в отдельном потоке. И так, попробуем написать пример — запустим в потоке бесконечный цикл:

Запустите пример на выполнение и нажмите кнопку. Вроде ничего не происходит — форма не зависла, реагирует на перемещения. На самом деле это не так — откройте диспетчер задач и вы увидите, что процессор загружен по-полной. Сейчас в процессе вашего приложения работает два потока — один был создан изначально, при запуске приложения. Второй, который так грузит процессор — мы создали по нажатию кнопки. Итак, давайте разберём, что же означает код в Button1Click:

  • tpTimeCritical — критический
  • tpHighest — очень высокий
  • tpHigher — высокий
  • tpNormal — средний
  • tpLower — низкий
  • tpLowest — очень низкий
  • tpIdle — поток работает во время простоя системы

Думаю, теперь вам понятно, как создаются потоки. Заметьте, ничего сложного. Но не всё так просто. Казалось бы — пишем любой код внутри метода Execute и всё, а нет, потоки имеют одно неприятное свойство — они ничего не знают друг о друге. И что такого? — спросите вы. А вот что: допустим, вы пытаетесь из другого потока изменить свойство какого-нибудь компонента на форме. Как известно, VCL однопоточна, весь код внутри приложения выполняется последовательно. Допустим, в процессе работы изменились какие-то данные внутри классов VCL, система отбирает время у основного потока, передаёт по кругу остальным потокам и возвращает обратно, при этом выполнение кода продолжается с того места, где приостановилось. Если мы из своего потока что-то меняем, к примеру, на форме, задействуется много механизмов внутри VCL (напомню, выполнение основного потока пока «приостановлено»), соответственно за это время успеют измениться какие-либо данные. И тут вдруг время снова отдаётся основному потоку, он спокойно продолжает своё выполнение, но данные уже изменены! К чему это может привести — предугадать нельзя. Вы можете проверить это тысячу раз, и ничего не произойдёт, а на тысяча первый программа рухнет. И это относится не только к взаимодействию дополнительных потоков с главным, но и к взаимодействию потоков между собой. Писать такие ненадёжные программы конечно нельзя.

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

Если вы создали шаблон класса автоматически, то, наверное, заметили комментарий, который дружелюбная Delphi поместила в новый модуль. Он гласит: «Methods and properties of objects in visual components can only be used in a method called using Synchronize». Это значит, что обращение к визуальным компонентам возможно только путём вызова процедуры Synchronize. Давайте рассмотрим пример, но теперь наш поток не будет разогревать процессор впустую, а будет делать что-нибудь полезное, к примеру, прокручивать ProgressBar на форме. В качестве параметра в процедуру Synchronize передаётся метод нашего потока, но сам он передаётся без параметров. Параметры можно передать, добавив поля нужного типа в описание нашего класса. У нас будет одно поле — тот самый прогресс:

Вот теперь ProgressBar двигается, и это вполне безопасно. А безопасно вот почему: процедура Synchronize на время приостанавливает выполнение нашего потока, и передаёт управление главному потоку, т.е. SetProgress выполняется в главном потоке. Это нужно запомнить, потому что некоторые допускают ошибки, выполняя внутри Synchronize длительную работу, при этом, что очевидно, форма зависает на длительное время. Поэтому используйте Synchronize для вывода информации — то самое двигание прогресса, обновления заголовков компонентов и т.д.

Вы наверное заметили, что внутри цикла мы используем процедуру Sleep. В однопоточном приложении Sleep используется редко, а вот в потоках его использовать очень удобно. Пример — бесконечный цикл, пока не выполнится какое-нибудь условие. Если не вставить туда Sleep мы будем просто нагружать систему бесполезной работой.

Теперь мы немного изменим, можно сказать даже упростим, реализацию метода Execute нашего потока:

Вот, в принципе, мы и рассмотрели основные способы работы с компонентами VCL из потоков. А как быть, если в нашей программе не один новый поток, а несколько? И нужно организовать работу с одними и теми же данными? Тут нам на помощь приходят другие способы синхронизации. Один из них мы и рассмотрим. Для его реализации нужно добавить в проект модуль SyncObjs.

Самый интересный способ, на мой взгляд — критические секции

Работают они следующим образом: внутри критической секции может работать только один поток, другие ждут его завершения. Чтобы лучше понять, везде приводят сравнение с узкой трубой: представьте, с одной стороны «толпятся» потоки, но в трубу может «пролезть» только один, а когда он «пролезет» — начнёт движение второй, и так по порядку. Еще проще понять это на примере и тем же ProgressBar’ом. Итак, запустите один из примеров, приведённых ранее. Нажмите на кнопку, подождите несколько секунд, а затем нажмите еще раз. Что происходит? ProgressBar начал прыгать. Прыгает потому, что у нас работает не один поток, а два, и каждый из них передаёт разные значения прогресса. Теперь немного переделаем код, в событии onCreate формы создадим критическую секцию:

У TCriticalSection есть два нужных нам метода, Enter и Leave, соответственно вход и выход из неё. Поместим наш код в критическую секцию:

Попробуйте запустить приложение и нажать несколько раз на кнопку, а потом посчитайте, сколько раз пройдёт прогресс. Понятно, в чем суть? Первый раз, нажимая на кнопку, мы создаём поток, он занимает критическую секцию и начинает работу. Нажимаем второй — создаётся второй поток, но критическая секция занята, и он ждёт, пока её не освободит первый. Третий, четвёртый — все пройдут только по-очереди.

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

В этой небольшой статье рассмотрены не все способы синхронизации, есть еще события (TEvent), а так же объекты системы, такие как мьютексы (Mutex), семафоры (Semaphore), но они больше подходят для взаимодействия между приложениями. Остальное, что касается использования класса TThread, вы можете узнать самостоятельно, в help’е всё довольно подробно описано. Цель этой статьи — показать начинающим, что не всё так сложно и страшно, главное разобраться, что есть что. И побольше практики — самое главное опыт!

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

. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.

8 июня 2010 г.

Как узнать, почему зависла программа?

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

Состоит она из шести частей:
— Подготовка — общие действия для всех случаев отладки.
— Delphi — отладка зависания в Delphi.
— EurekaLog — поиск причины зависания в EurekaLog.
— Process Explorer — поиск причины зависания утилитой Process Explorer.
— Threads Snapshot — поиск причины зависания утилитой Threads Snapshot.
— Практический пример — пример с искусственным зависанием в программе.

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

Итак, вам нужно включить (Project/Options/Linking): «MAP file» — Detailed, «Debug Information» (в старых версиях Delphi она называлась «Include TD32 debug info») — True, «Include remote debug symbols» — True:

Добавление отладочной информации в проект Delphi

Кроме отладочной информации, полезно включить опции «Stack Frames» и «Use Debug DCUs» на вкладке «Compiling»:

Включение отладочных опций для проекта Delphi

Уж позвольте мне не повторяться, что делают эти опции.

Не забываем сделать Build после изменения опций. Понятно, что если у вас несколько проектов (DLL, BPL и т.п.), то менять опции и пересобирать надо все. Также, если вы используете сборку с run-time пакетами — по возможности, выключите их на время тестирования, ибо пакеты сильно всё усложняют.

Вы можете подумать: «но я не могу воспроизвести это под отладчиком!» или «на проблемной машине у меня не стоит Delphi!». Но не спешите переходить к следующему разделу.

Во-первых, вам необязательно запускать программу в Delphi. Вы можете запустить программу как обычно, вне среды, и работать с ней, пока она не зависнет — после чего подключить к ней отладчик. Во-вторых, если на машине не стоит Delphi — вы можете установить на неё удалённый отладчик. Подробнее об удалённом отладчике — см. справку или мою статью (большой объём, вот вариант в PDF) — раздел 2, в конце секции 2.1.1. про работу с отладчиком.

Для отладки зависшего проекта в Delphi его, конечно же, нужно открыть. Предварительно он должен быть скомпилирован — с установленными опциями отладки, как мы сделали выше. Кроме того, для локальной машины желательно, чтобы вы запускали программу из Output path, указанном в опциях проекта (т.е. не перемещали бы скомпилированный exe перед запуском).

Итак, вы запустили свою программу, она сколько-то там поработала и зависла. Открываем Delphi, загружаем нужный проект и делаем Run/Attach to process:

Окно Delphi Run/Attach to process

Из списка выбираем вашу программу (введя при необходимости имя удалённой машины, если Delphi и программа находятся на разных машинах), устанавливаем галочку «Pause after attach» и жмём «Attach».

Отладчик подключится к процессу и установит его на паузу — путём возбуждения точки останова в потоке отладчика:

Остановка на паузу отладчика Delphi после подключения к процессу

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

Напомню, что при возможности отладку зависаний стоит производить в ОС Vista и выше — потому что в этих системах появилась новая возможность для отладчиком: Wait Chain Traversal. Отладчик Delphi последних версий поддерживает WCT. Поэтому, если вы используете BDS 2009 или выше и Windows Vista или выше, то в окне Threads напротив каждого потока в колонке «Wait Chain» можно увидеть его статус, чего он ждёт, есть ли взаимоблокировка и т.п.:

Поддержка Wait Chain Traversal в Delphi 2009 и выше

В EurekaLog есть фишка «Anti-Freeze». Собственно, её я уже разбирал в отдельной статье, поэтому не буду останавливаться сейчас — у нас и так сегодня куча материала.

Если же по каким-то причинам среда Delphi для вас недоступна — вам придётся производить отладку руками.

Перед тем, как производить отладку, надо подготовить Process Explorer. Здесь будет два шага — оба опциональных, но для максимального удобства лучше сделать оба.

Шаг первый — настроить Process Explorer на загрузку отладочных символов. Дело в том, что Windows поставляется с обычными исполняемыми модулями без отладочной информации. Для своих программ мы только что включили генерацию отладочной информации (см. первый пункт), то как мы можем сделать это для Windows, чьи исходники нам недоступны? Ну, Microsoft позаботилась об этом: она распространяет отладочную информацию для своих программ отдельно. Вы можете скачать её и получить читабельные стеки вызовов.

Для начала вам понадобится скачать и установить Windows Debugging Tools. Затем, вам нужно решить, качать ли всю отладочную информацию скопом или же пусть она качается по запросу отладочной программы. Если вы выбрали первый путь — то вперёд, качаем и устанавливаем. Лично я выбираю второй путь.

Для второго способа вам нужно создать папку на своей машине с правом чтения-записи файлов и папок в ней. После чего остаётся только настроить Process Explorer:

В первом поле указывается путь к DbgHelp.dll — если вы устанавливали Windows Debugging Tools, то берите библиотеку оттуда. Если нет — то берите C:WindowsSystem32dbghelp.dll (однако я не уверен, будет ли это работать).

После того, как вы это настроили, Process Explorer будет пытаться получить отладочную информацию о каждом необходимом файле и кэшировать её в указанной папке. Поэтому, когда вы просматриваете потоки процесса или их стек вызовов, вы можете иногда видеть надпись «Loading symbols for ABC.exe+0xXYZ. «. В общем, после этого вам станет доступно больше информации для системных модулей.

Второй момент, который нужно сделать — сконвертировать map-файл вашего проекта в формат, понимаемый Process Explorer. Дело в том, что Delphi создаёт только различные Borland-ские форматы отладочной информации, а Process Explorer, как утилита Microsoft, понимает только Microsoft-ские форматы отладочной информации. Я уже говорил об этом. Сделать это можно утилитой map2dbg. Это простая консольная утилитка. Качаете архив, распаковываете, открываете консоль и пишете:

По файлам Project1.exe и Project1.map утилита сделает вам файл Project1.dbg, который может быть использован в Microsoft-ских утилитах.

Что-ж, я тут много чего сказал. Давайте я продемонстрирую, что вы получаете, выполнив указанные выше вещи. Ниже — три скриншота. Слева направо: вид стека вызовов без выполнения обоих пунктов (т.е. без системной и без проектной отладочной информацией), вид стека вызовов с первым пунктом (с системной, но без проектной отладочной информацией) и вид стека вызовов с обоими пунктами (с системной и с проектной отладочной информацией):

Вид стека вызовов в Process Explorer без отладочной информации
Вид стека вызовов в Process Explorer с системной отладочной информацией
Вид стека вызовов в Process Explorer с полной отладочной информацией

Как видите, подключение отладочной информации даёт вам три вещи:

— Более читабельный стек вызовов (вместо имя-модуля+смещение вы получаете имя-модуля!процедура+смещение)
— Более полный стек вызовов (без отладочной информации эвристика трассировки стека может опускать вызовы)
— Более правдивый стек вызовов (анализатор может неверно определять имя функции, если идёт вызов внутренней функции, которая не имеет публичного имени, но рядом находится другая функция, которая как-раз таки имеет публичное имя — поэтому анализатор может посчитать внутреннюю функцию частью публичной)

Если вы не будете (или не сможете) подключать отладочную информацию — вам придётся работать со смещениями. Вы конечно, можете искать смещения в map-файле руками, но это весьма хлопотно. Гораздо проще просто выписать на бумажку все числа, приписав имя модуля. Затем запускаете проект у себя, ставите его на паузу и используете команду Search/Goto address. Вводите адрес — и Delphi переводит вас на строчку в исходном тексте, а если это невозможно — то открывает окно CPU. Какой вводить адрес? Два примера. Вы выписали Project1.exe + $1234 и Project2.dll + $4321. Базовый адрес exe обычно не меняется и равен $400000. Вы загрузили проект у себя. Базовый адрес exe тот-же — $400000, а вот DLL оказалась загруженной по адресу $50000000 (вы можете выяснить это в окне Modules: View/Debug Windows/Modules). Тогда вас интересуют адреса $400000 + $1234 = $401234 и $50000000 + $4321 = $50004321.

Фух, разобрались. Как видите, намного удобнее подключать отладочную информацию, чем работать без неё :)

Теперь, что вы собственно должны делать для отладки зависания. Ну, вы запускаете свою программу и работаете с ней, пока она не зависнет. Потом вы запускаете Process Explorer, выбираете в списке процессов свою программу, правый щелчок -> Properties (свойства). В свойствах процесса переходим на вкладку Threads (потоки):

Вкладка Threads в Process Explorer

Здесь вы можете посмотреть, чем занимаются потоки вашей программы. Кто кушает процессор, кто чего-то ждёт и т.п. Выберите поток и нажмите кнопку «Stack» для просмотра его стека вызова (пример окна — см. три скриншота чуть выше).

Конечно, вы не сможете посмотреть переменные или что-то такое — только состояние и точки выполнения потоков. Так что вам придётся использовать свои телепатические способности, чтобы определить причину зависания.

Ну, использование Process Explorer хотя и несложно, но не выглядит таковым. Для новичка тут много работы и новых понятий. Да и вся эта возня с отладочной информацией не слишком удобна. Поэтому я написал простую утилитку (сейчас — часть EurekaLog Tools), которая позволяет вам выбрать запущенный процесс и дампит в текстовый файл информацию по всем потокам, включая:

— Базовая инфа: ID, приоритет и т.п.
— Стек вызова. Используются следующие источники отладочной информации: Borland-ские, JCL, EurekaLog и madExcept. Чуть позже добавлю поддержку Microsoft-ских и закачку по запросу, как у Process Explorer.
— Информация от Wait Chain Traversal (на Windows Vista и выше).
— Контекст потока — регистры и флаги.

Собственно, вы запускаете свою проблемную программу и работаете в ней, пока она не зависнет. Потом запускаете утилиту Threads snapshot и тыркаете её на зависший процесс. Она снимет вам снимок потоков, который вы сможете проанализировать.

Утилита Threads Snapshot

Как видите — достаточно простая и удобная альтернатива ручной отладке с Process Explorer. Дополнительный плюс — вы можете попросить вашего клиента снять вам снимок процесса на его машине. В случае же с Process Explorer-ом — навряд ли вы сможете объяснить клиенту, что куда ставить и где жать. Не забудьте только передать все необходимые файлы (map-файлы, например).

Я написал эту утилиту буквально только что за два дня. Поэтому она «немножко» сыровата. Нормальная и отлаженная версия этой утилиты должна войти в EurekaLog v7.

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

В Delphi: если у вас есть поддержка WCT, то отладка первой кнопки вообще не представляет сложностей: запустили, нажали, поставили программу на паузу и смотрим окно Threads:

Deadlock 1 в Delphi с поддержкой Wait Chain Traversal (WCT)

Если же WCT у вас нет, то придётся поработать головой и руками. Вы должны проанализировать стеки вызова каждого потока: щёлкаете по потокам в окне «Threads» и смотрите стеки вызовов:

Deadlock 1 в Delphi без поддержки Wait Chain Traversal (WCT)

После переключения на поток открывается окно CPU с текущей выполняемой инструкцией, но вы можете щёлкать по строчкам в окне «Call stack», чтобы посмотреть исходный код.

Проанализировав стеки вызовов всех трёх потоков, вы составите картину произошедшего.

Как эта же ситуация выглядит в Process Explorer и Threads snapshot? Ну, вы запускаете Process Explorer и смотрите стеки потоков (у меня первый поток открывался около минуты, не знаю, с чем связано — потом пошло гладко):

Deadlock 1 в Process Explorer

Хотя вы не видите здесь номеров строк, но вы видите имена функций и последовательность вызовов — например, в примере выше обратите внимание на Thread1Func, различные dispatch-функции, CriticalSection.Aquire и т.п. И снова: вы смотрите стеки вызова всех потоков и реконструируете ситуацию. Сделать это будет сложнее, чем используя Delphi (ибо нет номеров строк), но с известной долей телепатии — вполне возможно.

Что касается Threads snapshot, то она даёт такой лог (я отрезал лишние части для уменьшения лога):

К сожалению, вывод WCT пока не очень форматирован (это моё домашнее задание! :) ), но цепочку ожиданий увидеть можно. Плюс стеки потоков (к сожалению, без системной отладочной информации — это моё второе домашнее задание!) самым решительным образом намекают на происходящее.

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

Итак, в Delphi вы запускаете программу, она виснет, мы ставим её на паузу. У нас только один поток, поэтому WCT нам не помощник, даже если он есть. Поэтому сразу переключаемся на главный поток и смотрим стек вызовов:

Deadlock 2 в Delphi

Опять-таки: щёлкая по стеку вызовов, мы видим исходный код.

В этот раз, простой анализ стека вызовов нам не помогает. Пока что неясно, что же случилось. Тем более, что программа вроде бы не висит, а что-то делает (т.е., строго говоря, у нас не зависание, а зацикливание). Поэтому мы начинаем пошаговый прогон программы. Пройдясь по коду мы видим, что код постоянно крутит цикл со Sleep, проверяя некий флаг. Мы видим, что этот код — код менеджера памяти FastMM. Почитав комментарии в коде, мы узнаём, что FastMM пытается получить блокировку. А проверяемый флаг — это признак занят/свободен. Поскольку FastMM проверяет этот флаг уже полчаса — ясно, что тут что-то не так. Этот же вывод следует из того, что в нашёй программе всего один поток — т.е. ждать-то и вовсе некого, кроме нас в программе никого нет. Иными словами, состояние флага не соответствует действительности — т.е. он испорчен вследствие повреждения памяти.

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

Поэтому, задача анализа зависания — выяснить причину. Мы её нашли: это — повреждение памяти. Дальше, анализ зависания закончен, но проблема пока не найдена и не устранена — мы переходим к анализу и поиску проблем с памятью.

Собственно, второй случай выглядит примерно аналогично везде (в Delphi, Process Exporer и Threads snapshot): мы получаем примерно один и тот же стек вызовов, который может меняться время от времени, но всегда это будет цикл и с вероятностью в 99% — стоять на Sleep. Делается просто несколько проверок, чтобы в этом убедиться. Для примера — вот как это выглядит в Process Explorer:

Deadlock 2 в Process Explorer

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

Фух. На сегодня у меня всё. Надеюсь, этот материал был полезен. Если хотите, вот ещё дополнение — презентация по ручному поиску места ошибки по адресу (см. «How to find the exception source line»). На английском, но может пригодится или будет интересно.

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

Закрытие файла вызывает падение программы
Подскажите, в чём дело. Создаю два двоичных файла со списками слов, перед каждым словом — пишу.

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

DateTime имеет формат MM.MM.yyyy H:mm:ss, что и вызывает ошибку
Проблема в том, что при вводе даты в формате 13.05.2014 16:13:34 появляется ошибка: При вводе.

Пока до таймера дело не доходит.
— Не загружен шрифт FontID(1)
— StartDrawing/StopDrawing не определён, куда рисовать то?
Все ошибки пишет среда PUreBasic внизу экрана, просто читайте и исправляйте.

Простите, торопился. Конечно же после объявления Global переменных:
Все ошибки пишет среда PUreBasic внизу экрана, просто читайте и исправляйте.

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

Значит я пока настолько тупой, что не понял принцип рисования в окне. (((
Буду разбираться. Подскажите, направите на путь правильный?
Спасибо!

Принцип один, перед рисованием указываем место, где будем рисовать, при помощи StartDrawing.
StartDrawing выполняется довольно медленно, потому желательно выносить по возможности за пределы длинных циклов,
внутри циклов только сами операторы рисования.
Кол-во StartDrawing должно быть равно кол-ву StopDrawing.
Нет. Спасибо! Буду изучать. Хотя у меня там много намешано.
Стыдно весь код представить )
Ожидание запуска исполняемого файла.
Тип исполняемого файла: Windows — x86 (32bit, Unicode)
Запущен исполняемый файл.
[ОШИБКА] Строка: 27
[ОШИБКА] StartDrawing() должна быть вызвана перед другой 2DDrawing функцией.
Всё получилось. Спасибо!
Только не придумал как сделать пропорции для часовой и минутной стрелки от разрешения экрана. При небольших размерах вроде нормально выглядят, а на высоких часовая слишком длинная.
Это самая первая моя программа на PureBasic.
Clock_acco.7z
Не знал. Изучал коды и везде видел объявление переменной. Пришёл к выводу, что все примеры очень древние и их приходится допиливать. Так и изучаю PB )
Так же есть встроенные функции перевод градусов в радианы и обратно.
Тут просто — Бейсик вычисления делает в радианах, а не в градусах. Даже на советских калькуляторах был такой переключатель «рад-гр», там тоже в формулах обычно радианы использовались.
Непонятно ведёт себя при выборе данного скринсейвера и при нажатии кнопки Просмотр — запускается два раза.
Тут есть специфика построения screensaver. Строго говоря не достаточно просто переименовать exe в scr.
Помимо некоторых правил, то как выход по нажатии любой кнопки, мышки, и запрета на запуск второй копии, ещё
есть некая внутренняя структура, которая управляется с ком. строки(ProgramParameter()), которую нужно правильно обработать:
Вот код с английского форума, демонстрирующий выбор. Т.е система командует параметрами, а ваша программа должна их правильно выполнять! Не система организует Просмотр, а вы должны это действие предусмотреть, как и конфигурирование параметров. Или всё правильно игнорировать, если нет настроек.
В интернете много информации на это счет, разберётесь.

Не критично, StartDrawing вызывается не в главном цикле, а по таймеру.

Но последний StopDrawing() явно лишний!

в строке 1 создаю изображение, начинаю рисование, потом в строке 3 завершаю рисование.а в строке 7 опять начинаю. Но зачем? Чтобы получить что? В хэлпе об этом ничего не сказано (((
в строке 1 создаю изображение, начинаю рисование, потом в строке 3 завершаю рисование.а в строке 7 опять начинаю. Но зачем?
Первое рисование по сути бессмысленно у вас. Там нужно только CreateImage, а Box по таймеру рисуется дальше в коде.
Width, Heght тоже в начале кода уже вычислили, заново нет смысла его вычислять.
Начало может быть таким:

Переписал в отдельные процедуры рисование циферблата и рисование стрелок. Рисование стрелок вызываю по таймеру. Но приходится вызывать и рисование циферблата, и рисование стрелок каждую секунду.
И задумался. А можно ли один раз нарисовать один циферблат, поместить на него мой логотип, а сверху расположить Image с прозрачным фоном и на нём каждую секунду перерисовывать стрелки?
Чтобы каждую секунду не перерисовывать всё окно чтобы стереть старые стрелки (Создание Image, наложение логотипа, рисование циферблата)?
Попытался, но изображение каждую секунду моргает. (((

ну так есть же грабёж, грабим маленький участок под стрелкой и возвращаем его
есть CustomFilterCallback(), что будет проще или удобней в этом случае?, надо пробовать оба варианта и делать свой выбор

стрелки кривоватые, может стрелки вектором рисовать?
если не вектором, то найти как рисуется линия со Зглажыванием

Каска с надписью Nord Stream

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

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

Эта операционная компания должна соответствовать немецкому законодательству, прежде чем проект стоимостью 10 миллиардов евро получит сертификацию.

Немецкий регулирующий орган заявил, что процедура сертификации будет приостановлена до тех пор, пока швейцарская материнская компания Nord Stream 2 не передаст основные активы и людские ресурсы своей дочерней компании в Германии, которая владеет и управляет немецкой частью трубопровода.

Решение, вероятно, отложит сертификацию на несколько месяцев, но и это еще не все: «Северный поток-2» должен получить зеленый свет от Европейской комиссии.

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

«Северный поток-2», проходящий по дну Балтийского моря, удвоит экспорт газа из Москвы в Германию, но при этом идет в обход Украины, которая во многом полагается на существующие трубопроводы для получения доходов и сильно пострадает от потери транзитных сборов.

Между тем, по данным немецкого сетевого оператора Gascade, поток российского газа по трубопроводу «Ямал-Европа» (один из основных маршрутов экспорта российского газа в Европу) в Германию в среду утром был стабильным и превысил уровень выходных.

Однако, несмотря на это, декабрьский нейтральный индекс цен на газ виртуальной бирже TTF (Title Transfer Facility), по которому рассчитывается средневзвешенная по объему цена на газ в Европе, к утру среды вырос на 6%.

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

Политическое оружие

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

Украина выступала против «Северного потока-2», который президент Владимир Зеленский назвал «опасным геополитическим оружием».

Многие начинающие пользователи сталкиваются с такой проблемой:

«Прекращена работа программы . «

И многих эта проблема раздражает.

Сейчас я вам расскажу,как справится с этой проблемой.

Подробности

Для начала разберёмся с возможными вариантами,из-за чего эта трабла возникает :

1. Установлено много стороннего ПО,которое «ест» ресурсы системы.

2. Программе не хватает оперативной памяти.

3. В системе не установлено необходимое ПО для «правильной» работы программы.

5. Проблема в самой программе.

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

Теперь пройдёмся по каждому этому варианту:

1. Посмотрите будет ли программа вылетать в режиме «чистой» загрузки ,если в этом режиме всё нормально работает,то попробуем выявит виновника,среди всего установленного ПО, с помощью метода «половинного деления».

Зайдите в Конфигурацию системы -> Службы и включите половину служб и перезагрузитесь. Если проблема не появляется, причина в оставшихся отключенных службах. Если проблема воспроизводится, причина во включенных службах — отключите половину из них и снова перезагрузитесь. Тоже самое и для ПО в Автозагрузке.

2. Убедитесь,что у вас включён файл подкачки,для этого:

а) Нажмите Пуск –> Панель управления –> Система –> Все элементы панели управления –> Дополнительные параметры системы -> Дополнительно:

б) В разделе Быстродействие нажмите Параметр,откройте вкладку Дополнительно и нажмите Изменить;

в) И посмотрите,чтобы стояла галочка напротив надписи «Автоматически выбирать объём файла подкачки».

3. Убедитесь,что у вас установлено следующее ПО:

Для 32 (x86) bit’ных систем :

Для 64 bit’ных систем :

Потом после их установки установите все обновления,которые будут в Центре обновления Windows !

4. Проверьте систему на наличие «зловредов» с помощью Dr.Web CureIt.

5. Проблема может быть в самой программе:

а) Если у вас установлена пиратская версия программы (взломанная , RePack),то обращайтесь к тому,у кого вы ею скачали;

б) Если у вас установлена Beta-версия программы,удалите её и найдите законченную версию программы у разработчика :

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

6. Определим,кто виноват в вылете программы,для этого:

а) Скачайте программу ProcDump и распакуйте её в папку C:ProcDump;

б) Откройте командную строку от имени администратора и выполните:

  • C:ProcDumpprocdump.exe -accepteula -e -w [имя сбойного приложения] C:ProcDump

в) Как определить имя сбойного приложения:

1) зайдите в Панель управления -> Все элементы панели управления -> Центр поддержки ->Монитор стабильности системы -> Отчеты о проблемах.

2) Найдите событие,когда вылетело проблемное приложение,щёлкните по нему 2 раза левой кнопкой мыши и там вы увидите надпись «Имя приложения:

в) Запустите это приложение и дождитесь вылета.

г) После этого у вас появится файл с расширением .dmp в C:ProcDump

д) Теперь заглянем в это дам (заглядывать в него можно также,как и и в дампы синих экранов Анализ причин возникновения BSOD при помощи Debugging Tools for Windows (только команда выгладит по другому: Kdfe -v [путь к дампу]).

е) Как определите,что за файл виноват — определите системный ли он или принадлежит сторонней программе (для этого достаточно его «погуглить «) ,если к сторонней программе,то определите к какой и удалит её.

Если файл системный,то запустите командную строку от имени администратора и выполните команду:

Дождитесь конца проверки и:

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

Если у вас Windows 8/8.1,то вам достаточно в командной строке,запущенной от имени администратора, при подключённом интернете , выполнить команду:

Если у вас Windows 7,то обратимся к другой статье ( пишется ) за помощью.

Читайте также:

  • Nvidia process and module monitoring driver что это
  • Как сбросить настройки adobe animate
  • Программы для телефона самсунг 5230
  • Как в ворд пад сделать альбомный лист
  • Xerox workcentre 3225 программа для сканирования

скалогрыз
Я ошибся, сделал тот же код на Дельфе, поведение такое же неадекватное.
«где такое написано?»
Я искал описания работы критической секции в подробностях и нашёл такое:
Когда поток выполняет вход в критическую секцию, он проверяет хозяина критической секции, если хозяин не он сам, то создаётся объект синхронизации, и поток начинает ожидать сигнальное состояние только, что созданного объекта. Когда поток выходит из критической секции, то выполняется проверка наличия созданного (кем-то другим) объекта синхронизации, если такой объект есть, то поток выходящий из КС, перестаёт быть хозяином и выставляет сигнальное состояние у объекта синхронизации.
Это можно назвать очередью из 1 одного элемента.

«гарантирует тот факт, что EnterCriticalSection 2 произойдёт только после того как отработал соседний поток ожидающий cs?» — Если честно, не очень понял этот вопрос.
Вы писали:
«
* Твой поток-счётчик, запускается и забирает критическую секцию (ну и спит с ней).
* В основном потоке, приходит сообщение: «Нажата кнопка».
* Основной поток идёт к крит-секции и ждёт пока она освободится (т.е. пока спит поток-счётчик).
* После захвата крит-секции он засыпает на 5 секунд (что вызывает эффект «залипания» т.к. другие сообщения не обрабатываются).
* (к этому времени, поток-счётчик уже ждёт своей очереди на сон, ожидая крит-секцию)
* Поспав пять секунд, основной поток возвращается в норму.
«
я с этим согласен, именно такого поведения я и ожидал, проблема в » Основной поток идёт к крит-секции и ждёт пока она освободится (т.е. пока спит поток-счётчик). «, основной поток никак не может дождаться критической секции не смотря на то, что поток-счётчик уходит в Sleep, что означает, что у основного потока куча времени на то, чтобы захватить КС.

» Исключать нужно короткие (по времени исполнения) куски кода.» — что это значит? Как я уже писал, если внутри потока счётчика убрать Sleep (т.е. поток-счётчик только и будет входить в КС, увеличивать счётчик, выводить его, выходить из КС) , то приведённый в самом начале код, работает как ожидается.

«Пока работает Sleep — другие отдыхаю..» — поработает Sleep, другие работают. Sleep для каждого потока свой.
» у тебя один поток сильно зависит от другого..» — Тут как раз наоборот, поток-счётчик никак не хочет быть зависимым от основного потока, потому, что при попытке входа основным потоком в КС, поток счётчик работает так, как будто никто другой и не пытается войти в КС.

Добавлено спустя 10 минут 34 секунды:
Вот http://rsdn.org/article/baseserv/critsec.xml где написано. Там сказано, что поток который выходит из КС, сам проверяет наличие попытки другого потока войти в КС, если такая попытка была (пока он занимался своими делами, к примеру тупо спал), то поток хозяин КС выставляет объект синхронизации в сигнальное состояние, и поток который ожидал этот сигнальный объект пробуждается.

Добавлено спустя 5 минут 44 секунды:

Код: Выделить всё
  repeat
    CritSec.Acquire;
      Inc(Counter);
      WriteLn(Format('Counter=%d',[Counter]));
      Sleep(100);
    CritSec.Release;
    [b]SwitchToThread;[/b]
  until Self.Terminated;

Использование SwitchToThread; тоже не помогает (попробовал в Винде), как обычно, основной поток висит на ожидании КС, поток-счётчик, считает, выводит и не парится.

Добавлено спустя 24 минуты 13 секунд:
Вот другой вариант на Дельфи.

Код: Выделить всё
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, SyncObjs, ExtCtrls;

type
  TSimpleThread = class(TThread)
  protected
    procedure Execute; override;
  public
    Delta:Integer;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Memo1: TMemo;
    Button3: TButton;
    Timer1: TTimer;
    Button4: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure Timer1Timer(Sender: TObject);
    procedure Button4Click(Sender: TObject);
  private
    FSTh1:TSimpleThread;
    FSTh2:TSimpleThread;
  end;

var
  Form1: TForm1;
  CritSec:TCriticalSection;
  Counter:Integer;

implementation

{$R *.dfm}

{ TSimpleThread }

procedure TSimpleThread.Execute;
begin
  repeat
    CritSec.Acquire;
    Counter:=Counter+Self.Delta;
    Sleep(100);
    CritSec.Release;
  until Self.Terminated;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  FSTh1:=TSimpleThread.Create(True);
  FSTh1.FreeOnTerminate:=True;
  FSTh1.Delta:=1;

  FSTh2:=TSimpleThread.Create(True);
  FSTh2.FreeOnTerminate:=True;
  FSTh2.Delta:=-1;

  FSTh1.Resume;
  FSTh2.Resume;
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  FSTh1.Terminate;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  CritSec.Acquire;
  Memo1.Lines.Append(Format('Counter=%d',[Counter]));
  Sleep(1000);
  CritSec.Release;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  CritSec:=TCriticalSection.Create;
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  CritSec.Free;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Memo1.Lines.Append(Format('Counter=%d',[Counter]));
end;

procedure TForm1.Button4Click(Sender: TObject);
begin
  FSTh2.Terminate;
end;

end.

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


Добро пожаловать!

Войдите или зарегистрируйтесь сейчас!

Войти

Страница 2 из 2


  1. Valeriy_JW

    Форумчанин

    Регистрация:
    23 июл 2013
    Сообщения:
    43
    Симпатии:
    5
    Адрес:

    Геленджик

    Звонил 2 раза и никто не ответил — просто тел не работал. Куда вы подевались?..

    #21


  2. N@ck

    Регистрация:
    21 янв 2016
    Сообщения:
    4
    Симпатии:
    0

    Здравствуйте, мы геодезисты из Тольятти, у нас возникла необходимость перевести съемку из Автокада в freereason, возможно ли это сделать без покупки оф. версии программы? Может существует демо-версия или тому подобное? Работа разовая, для постоянного пользования нам программа не нужна.

    #22


  3. panas

    Регистрация:
    24 сен 2013
    Сообщения:
    4
    Симпатии:
    0

    А можно ли применять коды в программе?

    #23


  4. alex_xs25

    Форумчанин

    скинь файлы вличку — переведу в acad

    #24


  5. N@ck

    Регистрация:
    21 янв 2016
    Сообщения:
    4
    Симпатии:
    0

    У нас есть топографический план, возможно ли будет его перевести в freereason из автокада и в каком виде он там будет? Сохраняться ли типы линий и шрифты? И возможно ли подгружить ПДФ в freereason?

    #25


  6. alex_xs25

    Форумчанин

    в Fr можно подгрузить автокадовский чертеж как подложку (есть возможность импортиромать чертеж по слоям, но это слишком муторно когда много слоев), pdf только подложка

    #26


  7. Valeriy_JW

    Форумчанин

    Регистрация:
    23 июл 2013
    Сообщения:
    43
    Симпатии:
    5
    Адрес:

    Геленджик

    Можно. Там есть встроенный язык программирования и даже не один.

    #27


  8. N@ck

    Регистрация:
    21 янв 2016
    Сообщения:
    4
    Симпатии:
    0

    Нам нужно перевести этот файл в freereason, но еще нам нужно посмотреть как там отображается эта съемка. Можно ли сделать скрин или ПДФ того, как получилась переведенная съемка в freereason? Заранее благодарим.

    Вложения:

    #28


  9. alex_xs25

    Форумчанин

    Можно из FR в AutoCAD, импорт DXF в fr займет много времени

    #29


  10. N@ck

    Регистрация:
    21 янв 2016
    Сообщения:
    4
    Симпатии:
    0

    Нам надо из автокада в freereason. Можем скинуть этот файл в DXF.

    #30


  11. alex_xs25

    Форумчанин

    Эт я понял
    Импорт DXF в fr займет много времени, в FR проще будет нарисовать заново!

    #31


  12. Nicker1978

    Форумчанин

    Всем Доброго времени суток! Столкнулся тоже с такой проблемкой. В Краснодарском Крае ведут ИСОГД в freereason. Мы с такой программой никогда не сталкивались. Сами работаем в Digitals, а потом экспортируем в dwg или dxf. Наша программа экспорт в формат dbi и dbs не делает. Может кто поможет конвертировать dwg или dxf в формат dbi и dbs? Объект одноразовый, поэтому покупать прогу или учиться ею пользоваться не вариант. Спасибо за помощь!

    #32


  13. Valeriy_JW

    Форумчанин

    Регистрация:
    23 июл 2013
    Сообщения:
    43
    Симпатии:
    5
    Адрес:

    Геленджик

    Можно попробовать. Скиньте материал.
    А еще по этому вопросу поговорите с Сергеем по тел +79528234002

    #33


  14. Nicker1978

    Форумчанин

    Благодарю за помощь. А на какую почту скинуть?

    #34


  15. TORIKSERG1958

    Регистрация:
    14 фев 2017
    Сообщения:
    1
    Симпатии:
    0
    Адрес:

    г. Геленджик

    FreeReason СУБД, не все алгоритмы триангуяции корректно работают, (см. CREDO)

    #35


  16. GROM777

    Регистрация:
    9 июл 2013
    Сообщения:
    8
    Симпатии:
    0

    Добрый день, ребята нужна помощь, можете помочь сконвертировать файлы из freereason в mapinfo?

    #36

Страница 2 из 2

Поделиться этой страницей

Время на прочтение
21 мин

Количество просмотров 69K

Привет, Хабр! Предлагаю вашему вниманию перевод статьи «Top 20 C++ multithreading mistakes and how to avoid them» автора Deb Haldar.


Сцена из фильма «Петля времени» (2012)

Многопоточность— одна из наиболее сложных областей в программировании, особенно в C++. За годы разработки я совершил множество ошибок. К счастью, большинство из них были выявлены на код ревью и тестировании. Тем не менее, некоторые каким-то образом проскакивали на продуктив, и нам приходилось править эксплуатируемые системы, что всегда дорого.

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

Ошибка №1: Не использовать join() для ожидания фоновых потоков перед завершением приложения

Если вы забыли присоединить поток (join()) или открепить его (detach()) (сделать его не joinable) до завершения программы, это приведет к аварийному завершению. (В переводе будут встречаться слова присоединить в контексте join() и открепить в контексте detach(), хотя это не совсем корректно. Фактически join() это точка, в которой один поток выполнения дожидается завершения другого, и никакого присоединения или объединения потоков не происходит [прим. переводчика]).

В примере ниже, мы забыли выполнить join() потока t1 в основном потоке:

#include "stdafx.h"
#include <iostream>
#include <thread>using namespace std;void LaunchRocket()
{
   cout << "Launching Rocket" << endl;
}
int main()
{
   thread t1(LaunchRocket);
   //t1.join(); // как только мы забыли join- мы получаем аварийное завершение программы
   return 0;
}

Почему программа упала?! Потому что в конце функции main() переменная t1 вышла из области видимости и был вызван деструктор потока. В деструкторе происходит проверка является ли поток t1 joinable. Поток является joinable, если он не был откреплен. В этом случае в его деструкторе вызывается std::terminate. Вот что, например, делает компилятор MSVC++.

~thread() _NOEXCEPT
{  // clean up
    if (joinable())
        XSTD terminate();
}

Есть два способа исправления проблемы в зависимости от задачи:

1. Вызвать join() потока t1 в основном потоке:

int main()
{
  thread t1(LaunchRocket);
  t1.join(); // выполняем join потока t1, ожидаем завершение этого потока в основном потоке выполнения
    return 0;
}

2. Открепить поток t1 от основного потока, позволить ему продолжить работать как «демонизированный» поток:

int main()
{
    thread t1(LaunchRocket);
    t1.detach(); // открепление  t1 от основного потока
    return 0;
}

Ошибка №2: Пытаться присоединить поток, который ранее был откреплен

Если в какой-то точке работы программы у вас есть открепленный (detach) поток, вы не можете присоединить его обратно к основному потоку. Это очень очевидная ошибка. Проблема в том, что вы можете открепить поток, а потом написать несколько сотен строк кода и попробовать вновь присоединить его. В конце концов, кто помнит, что он писал 300 строк назад, верно?

Проблема в том, что это не вызовет ошибку компиляции, вместо этого программа аварийно завершится при запуске. Например:

#include "stdafx.h"
#include <iostream>
#include <thread>using namespace std;void LaunchRocket()
{
    cout << "Launching Rocket" << endl;
}int main()
{
    thread t1(LaunchRocket);
    t1.detach();
    //..... 100 строк какого-то кода
    t1.join(); // CRASH !!!
    return 0;
}

Решение заключается в том, что необходимо всегда делать проверку потока на joinable() перед тем как пытаться его присоединить к вызывающему потоку.

int main()
{
  thread t1(LaunchRocket);
  t1.detach();
  //..... 100 строк какого-то кодаif (t1.joinable())
  {
    t1.join(); 
  }return 0;
}

Ошибка №3: Непонимание того, что std::thread::join() блокирует вызывающий поток выполнения

В реальных приложениях вам часто может потребоваться выделить в отдельный поток «долгоиграющие» операции обработки сетевого ввода-вывода или ожидания нажатия пользователя на кнопку и т.п. Вызов join() для таких рабочих потоков (например поток отрисовки UI) может привести к зависанию пользовательского интерфейса. Существуют более подходящие способы реализации.

Например, в GUI приложениях рабочий поток при завершении может отправить сообщение UI потоку. UI поток имеет собственный цикл обработки событий таких как: перемещение мыши, нажатие на клавиши и т.д. Этот цикл также может принимать сообщения от рабочих потоков и реагировать на них без необходимости вызова блокирующего метода join().

По этой самой причине в платформе WinRT от Microsoft практически все взаимодействия с пользователем сделаны асинхронными, а синхронные альтернативы недоступны. Эти решения были приняты для гарантии того, что разработчики будут использовать API, которое предоставляет наилучший опыт использования для конечных пользователей. Можно обратиться к руководству «Modern C++ and Windows Store Apps» для получения более подробной информации по данной теме.

Ошибка №4: Считать, что аргументы функции потока по умолчанию передаются по ссылке

Аргументы функции потока по умолчанию передаются по значению. Если вам необходимо внести изменения в передаваемые аргументы, необходимо передавать их по ссылке с помощью функции std::ref().

Под спойлером примеры из другой статьи C++11 Multithreading Tutorial via Q&A – Thread Management Basics (Deb Haldar), иллюстрирующие передачу параметров [прим. переводчика].

подробнее:

При выполнении кода:
#include "stdafx.h"
#include <string>
#include <thread>
#include <iostream>
#include <functional>using namespace std;void ChangeCurrentMissileTarget(string& targetCity)
{
  targetCity = "Metropolis";
  cout << " Changing The Target City To " << targetCity << endl;
}int main()
{
  string targetCity = "Star City";
  thread t1(ChangeCurrentMissileTarget, targetCity);
  t1.join();
  cout << "Current Target City is " << targetCity << endl;return 0;
}
 

Будет выведено в терминал:
Changing The Target City To Metropolis
Current Target City is Star City

Как видите, значение переменной targetCity, получаемой функцией, вызываемой в потоке, по ссылке не изменилось.

Перепишем код с использованием std::ref() для передачи аргумента:

#include "stdafx.h"
#include <string>
#include <thread>
#include <iostream>
#include <functional> using namespace std; void ChangeCurrentMissileTarget(string& targetCity)
{
  targetCity = "Metropolis";
  cout << " Changing The Target City To " << targetCity << endl;
} int main()
{
  string targetCity = "Star City";
  thread t1(ChangeCurrentMissileTarget, std::ref(targetCity));
  t1.join();
  cout << "Current Target City is " << targetCity << endl; return 0;
}

Будет выведено:
Changing The Target City To Metropolis
Current Target City is Metropolis

Изменения, сделанные в новом потоке, отразятся на значении переменной targetCity объявленной и инициализированной в функции main.

Ошибка №5: Не защищать разделяемые данные и ресурсы с помощью критической секции (например мьютексом)

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

В примере ниже std::cout является разделяемым ресурсом, с которым работают 6 потоков (t1-t5 + main).

#include "stdafx.h"
#include <iostream>
#include <string>
#include <thread>
#include <mutex> using namespace std;

 
std

::mutex mu; void CallHome(string message)
{
  cout << "Thread " << this_thread::get_id() << " says " << message << endl;
} int main()
{
  thread t1(CallHome, "Hello from Jupiter");
  thread t2(CallHome, "Hello from Pluto");
  thread t3(CallHome, "Hello from Moon");

 
  CallHome

("Hello from Main/Earth");

 
  thread t4

(CallHome, "Hello from Uranus");
  thread t5(CallHome, "Hello from Neptune");

 
  t1.

join();
  t2.join();
  t3.join();
  t4.join();
  t5.join(); return 0;
}
 

Если мы выполним эту программу, то получим вывод:

Thread 0x1000fb5c0 says Hello from Main/Earth
Thread Thread Thread 0x700005bd20000x700005b4f000 says says Thread Thread Hello from Pluto0x700005c55000Hello from Jupiter says 0x700005d5b000Hello from Moon
0x700005cd8000 says says Hello from Uranus

Hello from Neptune

Это происходит потому, что пять потоков одновременно обращаются к потоку вывода в произвольном порядке. Чтобы сделать вывод более определенным, необходимо защитить доступ к разделяемому ресурсу с помощью std::mutex. Просто изменим функцию CallHome() таким образом, чтобы она захватывала мьютекс перед использованием std::cout и освобождала его после.

void CallHome(string message)
{
  mu.lock();
  cout << "Thread " << this_thread::get_id() << " says " << message << endl;
  mu.unlock();
}

Ошибка №6: Забыть освободить блокировку после выхода из критической секции

В предыдущем пункте вы видели как защитить критическую секцию с помощью мьютекса. Однако, вызов методов lock() и unlock() непосредственно у мьютекса не является предпочтительным вариантом потому, что вы можете забыть отдать удерживаемую блокировку. Что произойдет дальше? Все остальные потоки, которые ожидают освобождения ресурса, будут бесконечно заблокированы и программа может зависнуть.

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

void CallHome(string message)
{
  mu.lock();
  cout << "Thread " << this_thread::get_id() << " says " << message << endl;
  //mu.unlock();  мы забыли освободить блокировку
}

Ниже приведен вывод данного кода– программа зависла, выведя единственное сообщение в терминал, и не завершается:

Thread 0x700005986000 says Hello from Pluto

Подобные ошибки часто случаются, именно поэтому нежелательно использовать методы lock()/unlock() напрямую из мьютекса. Вместо этого следует использовать шаблонный класс std::lock_guard, который использует идиому RAII для управления временем жизни блокировки. Когда объект lock_guard создаётся, он пытается завладеть мьютексом. Когда программа выходит из области видимости lock_guard объекта, вызывается деструктор, который освобождает мьютекс.

Перепишем функцию CallHome() с применением std::lock_guard объекта:

void CallHome(string message)
{
  std::lock_guard<std::mutex> lock(mu);  // пытаемся захватить блокировку
  cout << "Thread " << this_thread::get_id() << " says " << message << endl;
}// объект lock_guard уничтожится и освободит мьютекс

Ошибка №7: Делать размер критической секции больше, чем это необходимо

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

void CallHome(string message)
{
  std::lock_guard<std::mutex> lock(mu); // Начало критической секции, защищаем доступ к std::cout

 
  ReadFifyThousandRecords

(); cout << "Thread " << this_thread::get_id() << " says " << message << endl; }// при уничтожении объекта lock_guard блокировка на мьютекс mu освобождается

Метод ReadFifyThousandRecords() не модифицирует данные. Нет никаких причин выполнять его под блокировкой. Если данный метод будет выполняться 10 секунд, считывая 50 тысяч строк из БД, все остальные потоки будут заблокированы на весь этот период без необходимости. Это может серьезно сказаться на производительности программы.

Правильным решением было бы держать в критической секции только работу с std::cout.

void CallHome(string message)
{
  ReadFifyThousandRecords(); // Нет необходимости держать данный метод в критической секции т.к. он не модифицирует данные
  std::lock_guard<std::mutex> lock(mu); // Начало критической секции, защищаем доступ к std::cout
  cout << "Thread " << this_thread::get_id() << " says " << message << endl; }//  при уничтожении объекта lock_guard блокировка на мьютекс mu освобождается

Ошибка №8: Взятие нескольких блокировок в разном порядке

Это одна из наиболее распространенных причин взаимной блокировки (deadlock), ситуации, в которой потоки оказываются бесконечно заблокированы из-за ожидания получения доступа к ресурсам, заблокированным другими потоками. Рассмотрим пример:

поток 1 поток 2
lock A lock B
//… какие-то операции //… какие-то операции
lock B lock A
//… какие-то еще операции //… какие-то еще операции
unlock B unlock A
unlock A unlock B

Может возникнуть ситуация, в которой поток 1 попытается захватить блокировку B и окажется заблокированным, потому что поток 2 уже ее захватил. В тоже время, второй поток пытается захватить блокировку A, но не может этого сделать, потому что ее захватил первый поток. Поток 1 не может освободить блокировку A пока не захватит блокировку B и т.д. Другими словами, программа зависнет.

Данный пример кода поможет вам воспроизвести deadlock:

#include "stdafx.h"
#include <iostream>
#include <string>
#include <thread>
#include <mutex> using namespace std;

 
std

::mutex muA;
std::mutex muB; void CallHome_Th1(string message)
{
  muA.lock();
  // выполнение каких-то операций
  std::this_thread::sleep_for(std::chrono::milliseconds(100));
  muB.lock(); cout << "Thread " << this_thread::get_id() << " says " << message << endl;

 
  muB.

unlock();
  muA.unlock();
} void CallHome_Th2(string message)
{
  muB.lock();
  // какие-то дополнительные операции
  std::this_thread::sleep_for(std::chrono::milliseconds(100));
  muA.lock(); cout << "Thread " << this_thread::get_id() << " says " << message << endl;

 
  muA.

unlock();
  muB.unlock();
} int main()
{
  thread t1(CallHome_Th1, "Hello from Jupiter");
  thread t2(CallHome_Th2, "Hello from Pluto");

 
  t1.

join();
  t2.join(); return 0;
}

Если вы запустите этот код, он зависнет. Если залезть глубже в отладчик в окно потоков, вы увидите, что первый поток (вызванный из функции CallHome_Th1()) пытается получить блокировку мьютекса B, в то время как поток 2 (вызванный из CallHome_Th2()) пытается заблокировать мьютекс A. Никто из потоков не может достичь успеха, что и приводит к взаимной блокировке!


(картинка кликабельна)

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

В зависимости от ситуации можно воспользоваться другими стратегиями:

1. Использовать класс-обертку std::scoped_lock для совместного захвата нескольких блокировок:

std::scoped_lock lock{muA, muB};

2. Воспользоваться классом std::timed_mutex, в котором можно указать таймаут, по истечении которого блокировка будет снята, если ресурс не стал доступен.

std::timed_mutex m; void DoSome(){
    std::chrono::milliseconds timeout(100); while(true){
        if(m.try_lock_for(timeout)){
            std::cout << std::this_thread::get_id() << ": acquire mutex successfully" << std::endl;
            m.unlock();
        } else {
            std::cout << std::this_thread::get_id() << ": can’t  acquire mutex, do something else" << std::endl;
        }
    }
}

Ошибка №9: Пытаться дважды захватить блокировку std::mutex

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

#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>

 
std

::mutex mu; static int counter = 0; void StartThruster()
{
  try
  {
    // какие-то операции
  }
  catch (...)
  {
    std::lock_guard<std::mutex> lock(mu);
    std::cout << "Launching rocket" << std::endl;
  }
} void LaunchRocket()
{
  std::lock_guard<std::mutex> lock(mu);
  counter++;
  StartThruster();
} int main()
{
  std::thread t1(LaunchRocket);
  t1.join();
  return 0;
}
 

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

Ошибка №10: Использовать мьютексы, когда достаточно std::atomic типов

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

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

int counter;
...
mu.lock();
counter++;
mu.unlock();

Лучше объявить переменную как std::atomic:

std::atomic<int> counter;
...
counter++;

Для получения подробного сравнения mutex и atomic обратитесь к статье «Comparison: Lockless programming with atomics in C++ 11 vs. mutex and RW-locks»

Ошибка №11: Создавать и разрушать большое количество потоков напрямую, вместо использования пула свободных потоков

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

Еще одно преимущество пула потоков, по сравнению с порождением и уничтожением потоков самостоятельно, заключается в том, что вам не нужно беспокоиться об thread oversubscription (ситуация, в которой количество потоков превышает количество доступных ядер и значительная часть процессорного времени тратится на переключение контекста [прим. переводчика]). Это может повлиять на производительность системы.

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

Две наиболее популярные библиотеки, реализующие пул потоков: Intel Thread Building Blocks(TBB) и Microsoft Parallel Patterns Library(PPL).

Ошибка №12: Не обрабатывать исключения, возникающие в фоновых потоках

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

#include "stdafx.h"
#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept> static std::exception_ptr teptr = nullptr; void LaunchRocket()
{
  throw std::runtime_error("Catch me in MAIN");
} int main()
{
  try
  {
    std::thread t1(LaunchRocket);
    t1.join();
  }
  catch (const std::exception &ex)
  {
    std::cout << "Thread exited with exception: " << ex.what() << "n";
  } return 0;
}
 

При выполнении этой программы произойдет аварийное завершение, однако, catch блок в функции main() не выполнится и не обработает исключение, выброшенное в потоке t1.

Решение данной проблемы заключается в использовании возможности из C++11: std::exception_ptr применяется для обработки исключения, выброшенного в фоновом потоке. Вот шаги, которые необходимо предпринять:

  • Создать глобальный экземпляр класса std::exception_ptr, инициализированный nullptr
  • Внутри функции, которая выполняется в отдельном потоке, обрабатывать все исключения и устанавливать значение std::current_exception() глобальной переменной std::exception_ptr, объявленой на предыдущем шаге
  • Внутри основного потока проверять значение глобальной переменной
  • Если значение установлено, использовать функцию std::rethrow_exception(exception_ptr p) для повторного вызова пойманного ранее исключения, передав его по ссылке как параметр

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

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

#include "stdafx.h"
#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept> static std::exception_ptr globalExceptionPtr = nullptr; void LaunchRocket()
{
  try
  {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    throw std::runtime_error("Catch me in MAIN");
  }
  catch (...)
  {
    //При возникновении исключения присваиваем значение указателю
    globalExceptionPtr = std::current_exception();
  }
} int main()
{
  std::thread t1(LaunchRocket);
  t1.join(); if (globalExceptionPtr)
  {
    try
    {
      std::rethrow_exception(globalExceptionPtr);
    }
    catch (const std::exception &ex)
    {
      std::cout << "Thread exited with exception: " << ex.what() << "n";
    }
  } return 0;
}
 

Ошибка №13: Использовать потоки для симуляции асинхронной работы, вместо применения std::async

Если вам нужно, чтобы код выполнился асинхронно, т.е. без блокировки основного потока выполнения, наилучшим выбором будет использование std::async(). Это равносильно созданию потока и передаче необходимого кода на выполнение в этот поток через указатель на функцию или параметр в виде лямбда функции. Однако, в последнем случае вам необходимо следить за созданием, присоединением/отсоединением этого потока, а также за обработкой всех исключений, которые могут возникнуть в этом потоке. Если вы используете std::async(), вы избавляете себя от этих проблем, а также резко снижаете свои шансы попасть в deadlock.

Другое значительное преимущество использования std::async заключается в возможности получить результат выполнения асинхронной операции обратно в вызывающий поток с помощью std::future объекта. Представим, что у нас есть функция ConjureMagic(), которая возвращает int. Мы можем запустить асинхронную операцию, которая установит значение в будущем в future объект, когда выполнение задачи завершится, и мы сможем извлечь результат выполнения из этого объекта в том потоке выполнения, из которого операция была вызвана.

// запуск асинхронной операции и получение обработчика для future 
std::future asyncResult2 = std::async(&ConjureMagic); //... выполнение каких-то операций пока future не будет установлено // получение результата выполнения из future 
 int v = asyncResult2.get();

Получение результата обратно из работающего потока в вызывающий более громоздко. Возможны два способа:

  1. Передача ссылки на выходную переменную потоку, в которой он сохранит результат.
  2. Хранить результат в переменной-поле объекта рабочего потока, которую можно будет считать как только поток завершит выполнение.

Kurt Guntheroth обнаружил, что с точки зрения производительности, накладные расходы на создание потока в 14 раз больше, чем использование async.

Итог: используйте std::async() по умолчанию, пока вы не найдете весомые аргументы в пользу использования непосредственно std::thread.

Ошибка №14: Не использовать std::launch::async если требуется асинхронность

Функция std::async() носит не совсем корректное название, потому что по умолчанию может не выполняться асинхронно!

Есть две политики выполнения std::async:

  1. std::launch::async: переданная функция начинает выполняться незамедлительно в отдельном потоке
  2. std::launch::deferred: переданная функция не запускается сразу же, ее запуск откладывается до того как будут произведены вызовы get() или wait() над std::future объектом, который будет возвращен из вызова std::async. В месте вызова этих методов, функция будет выполняться синхронно.

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

  • невозможность предсказать правильность доступа к локальным переменным потока
  • асинхронная задача может и вовсе не запуститься из-за того, что вызовы методов get() и wait() могут не быть вызваны в течение выполнения программы
  • при использовании в циклах, в которых условие выхода ожидает готовности std::future объекта, эти циклы могут никогда не завершиться, потому что std::future, возвращаемое вызовом std::async, может начаться в отложенном состоянии.

Для избежания всех этих сложностей всегда вызывайте std::async с политикой запуска std::launch::async.

Не делайте так:

//выполнение функции myFunction используя std::async с политикой запуска по умолчанию
auto myFuture = std::async(myFunction);

Вместо этого делайте так:

//выполнение функции myFunction асинхронно
auto myFuture = std::async(std::launch::async, myFunction);

Более подробно этот момент рассмотрен в книге Скотта Мейерса «Эффективный и современный С++».

Ошибка №15: Вызывать метод get() у std::future объекта в блоке кода, время выполнение которого критично

Приведенный ниже код обрабатывает результат, полученный из std::future объекта асинхронной операции. Однако, цикл while будет заблокирован, пока асинхронная операция не выполнится (в данном случае на 10 секунд). Если вы хотите использовать данный цикл для вывода информации на экран, это может привести к неприятным задержкам отрисовки пользовательского интерфейса.

#include "stdafx.h"
#include <future>
#include <iostream> int main()
{
  std::future<int> myFuture = std::async(std::launch::async[]()
  {
    std::this_thread::sleep_for(std::chrono::seconds(10));
    return 8;
  }); // Цикл обновления для выводимых данных
  while (true)
  {
    // вывод некоторой информации в терминал          
    std::cout << "Rendering Data" << std::endl;
    int val = myFuture.get(); // вызов блокируется на 10 секунд
    // выполнение каких-то операций над Val
  } return 0;
}
 

Замечание: еще одна проблема приведенного выше кода в том, что он пытается обратиться к std::future объекту второй раз, хотя состояние std::future объекта было извлечено на первой итерации цикла и повторно не может быть получено.

Правильным решением было бы проверять валидность std::future объекта перед вызовом get() метода. Таким образом, мы не блокируем завершение асинхронного задания и не пытаемся повторно опросить уже извлеченный std::future объект.

Данный фрагмент кода позволяет достичь этого:

#include "stdafx.h"
#include <future>
#include <iostream> int main()
{
  std::future<int> myFuture = std::async(std::launch::async[]()
  {
    std::this_thread::sleep_for(std::chrono::seconds(10));
    return 8;
  }); // Цикл обновления для выводимых данных
  while (true)
  {
    // вывод некоторой информации в терминал           
    std::cout << "Rendering Data" << std::endl; if (myFuture.valid())
    {
      int val = myFuture.get(); // вызов блокируется на 10 секунд //  выполнение каких-то операций над Val
    }
  } return 0;
}
 

Ошибка №16: Непонимание того, что исключения, выброшенные внутри асинхронной операции, передадутся в вызывающий поток только при вызове std::future::get()

Представим что у нас есть следующий фрагмент кода, как вы думаете, каким будет результат вызова std::future::get()?

#include "stdafx.h"
#include <future>
#include <iostream> int main()
{
  std::future<int> myFuture = std::async(std::launch::async[]()
  {
    throw std::runtime_error("Catch me in MAIN");
    return 8;
  }); if (myFuture.valid())
  {
    int result = myFuture.get();
  } return 0;
}
 

Если вы предположили что программа упадет– вы совершенно правы!

Исключение, выброшенное в асинхронной операции прокидывается только когда происходит вызов метода get() у std::future объекта. И если метод get() вызван не будет, то исключение будет проигнорировано и отброшено, когда std::future объект выйдет из области видимости.

Если ваша асинхронная операция может выбросить исключение, то необходимо всегда оборачивать вызов std::future::get() в try/catch блок. Пример как это может выглядеть:

#include "stdafx.h"
#include <future>
#include <iostream> int main()
{
  std::future<int> myFuture = std::async(std::launch::async[]() 
  {
    throw std::runtime_error("Catch me in MAIN");
    return 8;
  }); if (myFuture.valid())
  {
    try
    {
      int result = myFuture.get();
    }
    catch (const std::runtime_error& e)
    {
       std::cout << "Async task threw exception: " << e.what() << std::endl;
    }
  }
  return 0;
}
 

Ошибка №17: Использование std::async, когда требуется чёткий контроль над исполнением потока

Хотя std::async() достаточно в большинстве случаев, бывают ситуации, в которых вам может потребоваться тщательный контроль над выполнением вашего кода в потоке. Например, если вы хотите привязать определенный поток к конкретному ядру процессора в многопроцессорной системе (например Xbox).

Приведенный фрагмент кода устанавливает привязку потока к 5-му процессорному ядру в системе.

#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include <thread> using namespace std; void LaunchRocket()
{
  cout << "Launching Rocket" << endl;
} int main()
{
  thread t1(LaunchRocket);

 
  DWORD result 

= ::SetThreadIdealProcessor(t1.native_handle()5);

 
  t1.

join(); return 0;
}
 

Это возможно благодаря методу native_handle() объекта std::thread, и передаче его в потоковую функцию Win32 API. Существует множество других возможностей, предоставляемых через потоковое Win32 API, которые не доступны в std::thread или std::async(). При работе через std::async() эти базовые функции платформы недоступны, что и делает этот способ непригодным для более сложных задач.

Альтернативный вариант— создать std::packaged_task и переместить его в нужный поток выполнения после установки свойств потока.

Ошибка №18: Создавать намного больше «выполняющихся» потоков, чем доступно ядер

С точки зрения архитектуры потоки можно классифицировать на две группы: «выполняющиеся» и «ожидающие».

Выполняющиеся потоки утилизируют 100% процессорного времени ядра на котором работают. Когда более одного выполняющегося потока выделено на одно ядро, эффективность утилизации процессорного времени падает. Мы не получаем выигрыша в производительности, если выполняем более одного выполняющегося потока на одном процессорном ядре– в действительности производительность падает из-за дополнительных переключений контекста.

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

Итак, как понять какое количество выполняющихся потоков поддерживает система? Используйте метод std::thread::hardware_concurrency(). Эта функция обычно возвращает количество ядер процессора, но при этом учитывает ядра, которые ведут себя как два или более логических ядер из-за гипертрединга.

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

Ошибка №19: Использование ключевого слова volatile для синхронизации

Ключевое слово volatile перед указанием типа переменной не делает операции с этой переменной атомарными или потокобезопасными. То, что вы, вероятно, хотите, это std::atomic.

Посмотрите обсуждение на stackoverflow для получения подробностей.

Ошибка №20: Использование Lock Free архитектуры, кроме случаев когда это совершенно необходимо

В сложности есть что-то, что нравится каждому инженеру. Создание программ, работающих без блокировок (lock free), звучит очень соблазнительно по сравнению с обычными механизмами синхронизации, такими как мьютекс, условные переменные, асинхронность и т. д. Однако, каждый опытный разработчик C ++, с которым я говорил, придерживался мнения, что применение программирования без блокировок в качестве исходного варианта является видом преждевременной оптимизации, которая может выйти боком в самый неподходящий момент (подумайте о сбое в эксплуатируемой системе, когда у вас нет полного дампа кучи!).

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

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

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

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

[От. переводчика: огромное спасибо пользователю vovo4K за помощь в подготовке данной статьи.]

Я столкнулся с проблемой синхронизации потоков и критических разделов в Windows 10.

Приложение вылетит в этом случае:

  • Приложение имеет два потока.
  • Поток 1 вызывает EnterCriticalSection с объектом m_CS
  • Затем поток 2 пытается войти в тот же критический раздел.
  • Поток 1 завершает поток 2, используя TerminateThread
  • Поток 1 вызывает LeaveCriticalSection

В предыдущих версиях Windows, которые мне удалось протестировать (7, 8, 8.1), это работает правильно. Поток 2 завершается, а поток 1 без исключения покидает критическую секцию.

В Windows 10, когда поток 1 покидает критическую секцию, происходит сбой приложения с нарушением прав доступа. Это происходит только тогда, когда другой поток был завершен во время ожидания EnterCriticalThread.

Глядя на трассировку стека, она выглядит так (последний кадр вверху):

RtlpWakeByAddress
RtlpUnWaitCriticalSection
RtlLeaveCriticalSection

Я потратил так много времени на отладку этой проблемы. В моем случае m_CS совершенно нормально работает при вызове LeaveCriticalSection. Я провел отладку и некоторое время анализировал дизассемблированный код функций ntdll.dll. Похоже, что объект где-то повреждается во время выполнения RtlpUnWaitCriticalSection, а затем передается в RtlpWakeByAddress при сбое. По сути, ntdll.dll могла изменять свойства объекта CRITICAL_SECTION, такие как количество блокировок в RtlLeaveCriticalSection.

В Интернете я не нашел ответа на этот вопрос или заявления о том, что изменилось в Windows 10. Только ветка на Reddit и ~ 1800 отчетов о сбоях для Mozilla Firefox с тем же стеком вызовов за последний месяц. Я связался с автором сообщения на Reddit, и он пока не смог это исправить.

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

Любой ответ приветствуется. заранее спасибо

4 ответа

Реализация CRITICAL_SECTION очень изменчива от версии к версии. когда в потоке последней версии Windows начинается ожидание CRITICAL_SECTION, он вызывает WaitOnAddress. хорошо, действительно это внутренняя реализация ntdll — RtlpWaitOnAddress, но это не меняет сути. этот внутренний вызов функции RtlpAddWaitBlockToWaitList — и здесь ключевой момент — WaitBlock размещается в стеке потоков, и указатель на этот блок ожидания добавляется в список. затем, когда владелец CRITICAL_SECTION уходит, он звонит WakeByAddressSingle (на самом деле это внутренняя реализация RtlpWakeByAddress), и эта функция извлекает первый блок ожидания из списка, извлекает из него идентификатор потока и вызывает NtAlertThreadByThreadId(новый API из win 8.1) — для пробуждения какой-то поток ждал в EnterCriticalSection. но когда вы завершили поток, подождав EnterCriticalSection — его стек освобождается. поэтому адрес блока WaitBlock становится недействительным. поэтому поток, который вызвал RtlpWakeByAddress (как часть LeaveCriticalSection), получил нарушение прав доступа при попытке прочитать идентификатор потока из WaitBlock (умерший стек потоков). вывод — если вызвать TerminatedThread — процесс уже стал нестабильным, баг может быть в любой момент и в любой момент. поэтому — не вызывайте эту функцию, особенно из собственного процесса.


7

RbMm
22 Сен 2016 в 13:39

Поток 1 завершает поток 2, используя TerminateThread

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

Из https://msdn.microsoft .com/en-us/library/windows/desktop/ms686717(v=vs.85).aspx

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

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

Что вам нужно сделать, так это связаться с потоком 2 и позволить потоку 2 завершить работу правильно и безопасно.


5

IanM_Matrix1
22 Сен 2016 в 18:39

Я бы изменил код потока 2, чтобы использовать TryEnterCriticalSection

if(!TryEnterCriticalSection(&m_CS)) {
    return 0;    // Terminate thread
}
//code
LeaveCriticalSection(&m_CS);

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


1

alain
28 Сен 2016 в 23:35

Да, я могу подтвердить это поведение и потратил более 3 дней на поиск утечки памяти в нашем коде, которая разрушает мой CRITICAL_SECTION. Проблема заключалась в старом вызове TerminateThread. Программа работала хорошо, но теперь в Windows10 у нас, по-видимому, возникали нарушения прав доступа в EnterCriticalSection или LeaveCriticalSection. Большое спасибо, это сделало мой день.


0

woelfchen42
21 Дек 2021 в 13:59

  • Основной ошибкой ведения переговоров можно считать
  • Основной лейтмотив это ошибка
  • Основной источник ошибок при выполнении высокоточного тригонометрического нивелирования
  • Основная часть средств вырученные на этом благотворительном концерте ошибка
  • Основная часть памятника пушкину постамент ошибка