Поделиться через


Реализация многопоточности на языке C с помощью функций Win32

Компилятор Microsoft C/C++ (MSVC) обеспечивает поддержку создания многопоточных приложений. Рассмотрите возможность использования нескольких потоков, если приложению требуется выполнять дорогостоящие операции, что приведет к тому, что пользовательский интерфейс не отвечает.

С помощью MSVC существует несколько способов программирования с несколькими потоками: вы можете использовать C++/WinRT и библиотеку среда выполнения Windows, библиотеку класса Microsoft Foundation (MFC), C++/CLI и среду выполнения .NET, или библиотеку времени выполнения C и API Win32. В этой статье описывается многопоточность в C. Пример кода см. в примере многопоточной программы в C.

Многопоточные программы

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

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

Каждый поток в процессе работает независимо. Если они не отображаются друг другу, потоки выполняются по отдельности и не знают о других потоках в процессе. Однако потоки, совместно использующие общие ресурсы, должны координировать свою работу с помощью семафоров или другого метода межпроцессного взаимодействия. Дополнительные сведения о синхронизации потоков см. в статье "Написание многопоточной программы Win32".

Поддержка многопоточности библиотеками

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

Включаемые файлы для многопоточности

Стандартная библиотека CRT включает файлы, объявляемые функциями библиотеки времени выполнения C, так как они реализуются в библиотеках. Если параметры компилятора указывают __fastcall или __vectorcall соглашения о вызовах, компилятор предполагает, что все функции должны вызываться с помощью соглашения о вызове регистра. Функции библиотеки времени выполнения используют соглашение о вызовах C, а объявления в стандартном формате включают файлы, чтобы компилятор создавал правильные внешние ссылки на эти функции.

Функции CRT для управления потоками

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

Библиотеки CRT предоставляют следующие функции для создания и завершения потоков: _beginthread, _beginthreadex, _endthread и _endthreadex.

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

Примечание.

При вызове подпрограмм времени выполнения C из программы, созданной с помощью libcmt.lib, необходимо запустить потоки с _beginthread помощью или _beginthreadex функции. Не используйте функции ExitThread Win32 и CreateThread. Использование SuspendThread может привести к взаимоблокировке, если несколько потоков заблокированы, ожидая завершения доступа к структуре данных во время выполнения C.

Функции _beginthread и _beginthreadex

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

_beginthread и _beginthreadex похожи на функцию CreateThread в API Win32, но имеют следующие отличия:

  • Они инициализируют определенные переменные библиотеки времени выполнения C. Это важно, только если в потоках используется библиотека времени выполнения C.

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

_beginthread и _beginthreadex возвращает дескриптор в новый поток в случае успешного выполнения или кода ошибки, если произошла ошибка.

Функции _endthread и _endthreadex

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

Написание многопоточной программы Win32

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

Совместное использование общих ресурсов между потоками

Примечание.

Аналогичное обсуждение с точки зрения MFC см. в разделе "Многопоточность: программирование Советы и многопоточность: когда следует использовать классы синхронизации".

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

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

Мьютекс (короткий для мутативной эксклюзии) — это способ взаимодействия между потоками или процессами, выполняющимися асинхронно друг от друга. Этот обмен данными можно использовать для координации действий нескольких потоков или процессов, как правило, путем управления доступом к общему ресурсу путем блокировки и разблокировки ресурса. Чтобы решить эту проблему обновления координат x,y, поток обновления задает мьютекс, указывающий, что структура данных используется перед выполнением обновления. Было бы ясно мьютекс после обработки обоих координат. Перед обновлением дисплея поток должен ожидать очистки мьютекса. Этот процесс ожидания мьютекса часто вызывается блокировкой для мьютекса, так как процесс блокируется и не может продолжаться до очистки мьютекса.

Программа Bounce.c, показанная в примере многопоточной программы C, использует мьютекс с именем ScreenMutex для координации обновлений экрана. Каждый раз, когда один из потоков отображения готов к записи на экран, он вызывает WaitForSingleObject дескриптор ScreenMutex и константу INFINITE, чтобы указать, что WaitForSingleObject вызов должен блокироваться в мьютексе и не истекает время ожидания. Если ScreenMutex ясно, функция ожидания задает мьютекс, чтобы другие потоки не могли мешать отображению, и продолжает выполнять поток. В противном случае поток блокируется до очистки мьютекса. Когда поток завершит обновление отображения, он освобождает мьютекс путем вызова ReleaseMutex.

Отображение экрана и статические данные — это только два ресурса, требующих тщательного управления. Например, программа может иметь несколько потоков, обращаюющихся к одному файлу. Так как другой поток, возможно, переместил указатель на файл, каждый поток должен сбросить указатель файла перед чтением или записью. Кроме того, каждый поток должен убедиться, что он не преумножен между временем, когда он размещает указатель и время доступа к файлу. Эти потоки должны использовать семафор для координации доступа к файлу путем скобки каждого доступа к файлам и WaitForSingleObjectReleaseMutex вызовов. Следующий пример кода иллюстрирует этот метод:

HANDLE    hIOMutex = CreateMutex (NULL, FALSE, NULL);

WaitForSingleObject( hIOMutex, INFINITE );
fseek( fp, desired_position, 0L );
fwrite( data, sizeof( data ), 1, fp );
ReleaseMutex( hIOMutex);

Стеки потоков

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

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

Потоки, которые вызывают библиотеку времени выполнения C или API Win32, должны разрешать достаточно места стека для вызываемых функций библиотеки и API. Для функции C printf требуется более 500 байт пространства стека, и при вызове подпрограмм API Win32 должно быть доступно 2 КБ.

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

Win32 также предоставляет хранилище thread-local (TLS) для хранения данных на поток. Дополнительные сведения см. в разделе "Локальное хранилище потоков" (TLS).

Устранение потенциальных проблем при работе с многопоточными программами

При создании, связывании или выполнении многопоточной программы C может возникнуть несколько проблем. Некоторые из наиболее распространенных проблем описаны в следующей таблице. (Аналогичное обсуждение с точки зрения MFC см. в разделе Многопоточность: программирование Советы.)

Проблема Возможные причины
Появится окно сообщения, показывающее, что программа вызвала нарушение защиты. Многие ошибки программирования Win32 вызывают нарушения защиты. Распространенной причиной нарушений защиты является косвенное назначение данных указателям NULL. Так как программа пытается получить доступ к памяти, к которой она не принадлежит, возникает нарушение защиты.

Простой способ определить причину нарушения защиты заключается в компиляции программы с информацией об отладке, а затем запустить ее с помощью отладчика в среде Visual Studio. При возникновении сбоя защиты Windows передает управление отладчику и курсор размещается в строке, вызвавшей проблему.
Программа создает множество ошибок компиляции и связывания. Вы можете устранить множество потенциальных проблем, задав уровень предупреждения компилятора на один из его самых высоких значений и заверив предупреждающие сообщения. С помощью параметров уровня 3 или уровня 4 вы можете обнаружить непреднамеренные преобразования данных, отсутствующие прототипы функций и использовать функции, отличные от ANSI.

См. также

Поддержка многопоточности для устаревшего кода (Visual C++)
Пример многопоточной программы в C
Локальное хранилище потоков (TLS)
Параллельные обработка и выполнение асинхронных операций с помощью C++/WinRT
Реализация многопоточности на языке C++ с помощью классов MFC