TRTLCriticalSection Sect1; // Критическая секция как структура



Создание вторичных потоков в процессе

Лабораторная работа № 2

Цель работы

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

Основные сведения о потоках

 

Поток (thread) – это последовательность исполнения кода в процессе. При инициализации процесса система всегда создает первичный поток.

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

 

         Процесс ничего не исполняет, он просто служит контейнером потоков.

 

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

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

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

Однако многопоточность следует использовать разумно. Не создавайте несколько потоков только потому, что это возможно. Многие полезные и мощные программы по-прежнему строятся на основе одного первичного потока, принадлежащего процессу.

 

Входная функция вторичного потока

Каждый поток начинает выполнение с некоторой входной функции. В первичном потоке ею является main ()  или  WinMain (). Если Вы хотите создать вторичный поток, в нем тоже должна быть входная функция, шаблон которой выглядит так:

 

DWORD WINAPI ThreadFunc(PVOID pvParam)
    {
     DWORD result = 0;

      .................

      return result;
    }

 

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

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

  • Функциям потоков передается единственный параметр (адрес некоторого объекта), смысл которого определяется Вами, а не операционной системой. Например, так можно передать целое или вещественное число, указатель на  объект класса и.т.п.
  •  Функция потока должна возвращать значение, которое будет использоваться как код завершения потока.
  • Функции потоков должны по мере возможности обходиться своими параметрами и локальными переменными. Так как к статической или глобальной переменной могут одновременно обратиться несколько потоков, есть риск повредить ее содержимое (Race Condition).

 

Функция CreateThread

Если Вы хотите создать дополнительные потоки, нужно вызвать из первичного потока функцию:

 

HANDLE CreateThread(

                 PSECURITY_ATTRIBUTES psa,

                 DWORD cbStack,
                 PTHREAD_START_ROUTINE pfnStartAddr,

                 PVOID pvParam,

                 DWORD fdwCreate,

                 PDWORD pdwThreadID

              );

 

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

Система выделяет память под стек потока из адресного пространства процесса. Новый поток выполняется в контексте того же процесса, что и родительский поток. Поэтому он получает доступ ко всем описателям объектов ядра, всей памяти и стекам всех потоков в процессе. За счет этого потоки в рамках одного процесса могут легко взаимодействовать друг с другом, но при этом могут легко друг другу навредить.

 

 

Параметр psa

Параметр psa является указателем на структуру SECURITY_ATTRIBUTES. Если Вы хотите, чтобы объекту ядра "поток" были присвоены атрибуты защиты по умолчанию (что чаще всего и бывает), передайте в этом параметре NULL.

 

Параметр cbStack

Этот параметр определяет, какую часть адресного пространства поток сможет использовать под свой стек. Если Выпередаете в параметре cbStack ненулевое значение, функция резервирует всю указанную Вами память.Если же Вы передаете в параметре cbStack нулевое значение, CreateThread создает стандартный стек для нового потока, используя информацию, встроенную компоновщиком в exe-файл.

 

Параметры pfnStartAddr и pvParam

Параметр pfnStartAddr определяет адрес функции потока, с которой должен будет начать работу создаваемый поток, а параметр pvParam будет передан в виде параметра  рvРаrат функции потока. CreateT h read лишьпередает этот параметр по эстафете той функции, с которой начинается выполнение создаваемого потока. Таким образом, данный параметр позволяет передавать функции потока какое-либо инициализирующее значение. Оно может быть или просто числовым значением, или указателем на структуру данных с дополнительной информацией, но лучше всего этот параметр использовать для передачи указателя на объект класса.

Вполне допустимо и даже полезно создавать несколько потоков, у которых в качестве входной точки используется адрес одной и той же функции. Например, можно реализовать Web-сервер, который обрабатывает каждый клиентский запрос в отдельном потоке. При создании каждому потоку передается свое значение рvParam .

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

Параметр fdwCreate

Этот параметр определяет дополнительные флаги, управляющие созданием потока. Он принимает одно из двух значений: 0 (исполнение потока начинается немедленно) или CREATE_SUSPENDED. В последнем случае система создает поток, инициализирует его и приостанавливает до последующих указаний.

Флаг CREATE_SUSPENDED позволяет программе изменить какие-либо свойства потока перед тем, как он начнет выполнять код, например, изменить приоритет потока.

 

Параметр pdwThreadlD

Последний параметр функции CreateT h read — это адрес переменной типа DWORD, в которой функция возвращает идентификатор, приписанный системой новому потоку. В этом параметре можно передавать NULL (обычно так и делается). Тем самым Вы сообщаете функции, что Вас не интересует идентификатор потока.

 

 Пример создания вторичного потока

HANDLE hThread; 

//-------------------------------------

//  Функция - точка входа нового потока (нити)

//-------------------------------------

 DWORD CALLBACK ThreadFunc(void* P)

 {

..............................

.........................

return 0;  

 }

 

//---------------------------------------------------------------------------

      // создание потока

 

 hThread = CreateThread(0,0,ThreadFunc,0,0,NULL);

 

Завершение потока

Поток можно завершить четырьмя способами:

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

 

Все нежелательные способы являются «аварийными» способами завершения потока, нужно стараться не использовать их в штатной работе.

 

 

Возврат управления функцией потока

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

  • любые С++ - объекты, созданные данным потоком, уничтожаются соответствующими деструкторами;
  • система корректно освобождает память, которую занимал стек потока;
  • система устанавливает код завершения данного потока , который возвращает функция потока;
  • счетчик пользователей данного объекта ядра "поток" уменьшается на 1

 

 

Функция ExitThread

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

 

  void ExitThread(DWORD dwExitCоde);

 

В параметр dwExitCode Вы помещаете значение, которое система рассматривает как код завершения потока. Возвращаемого значения у этой функции нет, потому что после ее вызова поток перестает существовать. При этом освобождаются все ресурсы операционной системы, выделенные данному потоку, но C/C++ - pеcypcы (например, объекты, созданные из С++-классов) не очищаются, не вызываются их деструкторы. Именно поэтому лучше возвращать управление из функции потока, чем изнутри него вызывать функцию ExitThread .

 

 

Функция TerminateThread

Вызов этой функции также завершает поток:

 

     bool TerminateThread(HANDLE hThread, DWORD dwExitCode);

 

Эта функция завершает поток, указанный в параметре hThread . В параметр dwExitCode заносится значение, которое система рассматривает как код завершения потока. После того как поток будет уничтожен, счетчик пользователей его объекта ядра "поток» уменьшится на 1.

   Корректно написанное приложение не должно вызывать эту функцию, поскольку поток не получает никакого уведомления о завершении; из-за этого он не может выполнить должную очистку ресурсов.

 

    Функции ExitProcess ()и TerminateProcess (), рассмотренные ранее, прекращают выполнение всех потоков, принадлежавших завершаемому процессу.  При этом гарантируется высвобождение любых выделенных процессу ресурсов, в том числе стеков потоков. Однако эти две функции уничтожают потоки принудительно — так, будто для каждого из них вызывается функция TerminateThread () . А это означает, что очистка проводится некорректно, деструкторы С++ - объектов не вызываются, данные на диск не сбрасываются и т. д.

 

 

Приостановка и возобновление потоков

В объекте ядра "поток" имеется переменная — счетчик числа простоев данного потока. При вызове CreateProcess или CreateThread он инициализируется значением, равным 1, которое запрещает системе выделять новому потоку процессорное время. Такая схема весьма разумна: сразу после создания поток не готов к выполнению, ему нужно время для инициализации.

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

Создав поток в приостановленном состоянии, Вы можете настроить некоторые его свойства, например, приоритет. Закончив настройку, Вы должны разрешить выполнение потока. Для этого вызовите ResumeThread и передайте дескриптор потока, возвращенный функцией CreateThread .

 

                            DWORD ResumeThread(HANDLE hThread);

 

Если вызов ResumeThread прошел успешно, она возвращает предыдущее значение счетчика простоев данного потока; в ином случае — 0xFFFFFFFF.

Выполнение отдельного потока можно приостанавливать несколько раз. Если поток приостановлен 3 раза, то и возобновлен он должен быть тоже 3 раза — лишь тогда система выделит ему процессорное время.

 Выполнение потока можно приостановить не только при его создании с флагом CREATE_SUSPENDED, но и вызовом SuspendThread .

 

                      DWORD S uspendThread (HANDLE hThread);

 

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

 

         Приостановить свое выполнение поток способен сам, а возобновить себя

         без посторонней помощи — нет.

 

          Как и ResumeThread , функция SuspendThread возвращает предыдущее значение счетчика простоев данного потока. Поток можно приостанавливать не более чем MAXIMUM_SUSPEND_COUNT раз (в файле WinNT.h это значение определено как 127).

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

 

 

Функция Sleep

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

 

      void Sleep(DWORD dwMilliseconds);

 

Эта функция приостанавливает поток на dwMilliseconds миллисекунд. Отметим несколько важных моментов:

  • Вызывая Sleep , поток добровольно отказывается от остатка выделенного ему кванта времени.
  • Система прекращает выделять потоку процессорное время на период, примерноравный заданному. Если Вы укажете остановить поток на 100 мс, приблизительно на столько он и "заснет", хотя не исключено, что его сон продлится на несколько миллисекунд секунд или даже секунд больше. Вспомните, Windows не является системой реального времени. Ваш поток может возобновиться и в заданный момент, но это зависит от того, какая ситуация сложится в системе к тому времени.
  • Вы можете вызвать Sle ep и передать в dwMilliseconds значение INFINITE, вообще запретив планировать поток. Но это не очень практично — куда лучше корректно завершить поток, освободив его стек и объект ядра.
  • Вы можете вызвать Sleep и передать в dwMilliseconds нулевое значение. Тогда Вы откажетесь от остатка своего кванта времени и заставите систему подключить к процессору другой поток. Однако система может снова запустить Ваш поток, если других планируемых потоков с тем же приоритетом нет.

 

Критическая секция

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

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

 

   Предварительно критическую секцию следует объявить как глобальный объект:

 

TRTLCriticalSection Sect1; // Критическая секция как структура

 

 и инициализировать:

 

InitializeCriticalSection (&Sect1);

Охраняемая ею часть программы – это блок кода, который начинается вызовом функции EnterCriticalSection и заканчивается вызовом LeaveCriticalSection:

EnterCriticalSection (&cs);

 

      < Защищаемый блок кода >

 

LeaveCriticalSection (&cs);

Если объект «Критическая секция» больше не нужен, его следует удалить:

 

DeleteCriticalSection (& Sect 1);

Пример использования критической секции в головной функции вторичного потока:

 DWORO WINAPI SomeThread(PVOID pvParam)
{

 

for(int k = 0; k < kmax; k++)

   {
      EnterCriticalSection(&Sect1);
               < Защищаемый блок кода >

      LeaveCriticalSection(&Sect1);

  }

 return 0 ;
}

 


Дата добавления: 2019-07-15; просмотров: 213; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!