Московский государственный технический университет им. Н.Э. Баумана
Опубликован: 28.06.2006 | Доступ: свободный | Студентов: 12464 / 342 | Оценка: 4.54 / 3.83 | Длительность: 22:03:00
ISBN: 978-5-9556-0055-0
Лекция 14:

Взаимодействие процессов и потоков

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

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

Синхронизация потоков

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

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

#include <stdio.h>
#include <process.h>
#include <windows.h>

#define THREADS 10
#define ASIZE  10000000
static LONG   array[ASIZE];
unsigned __stdcall ThreadProc( void *param )
{
  int  i;
  for ( i = 0; i < ASIZE; i++ ) array[i]++;
  return 0;
}

int main( void )
{
  HANDLE  	hThread[THREADS];
  unsigned 	dwThread;
  int 			i, errs;
  for ( i = 0; i < THREADS; i++ )
    hThread[i] = (HANDLE)_beginthreadex(
      NULL, 0, ThreadProc, NULL, 0, &dwThread
    );
  WaitForMultipleObjects( THREADS, hThread, TRUE, INFINITE );
  for ( i = 0; i < THREADS; i++ ) CloseHandle( hThread[i] );
  for ( errs=i=0; i<ASIZE; i++ )
    if ( array[i] != THREADS ) errs++;
  if ( errs ) printf("Detected %d errors!\n", errs );
  return 0;
}

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

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

несколько разных способов решения подобных проблем:

  • Атомарные операции.

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

  • Критические секции.

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

  • Синхронизация с использованием объектов ядра.

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

  • Ожидающие таймеры.

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

Анастасия Булинкова
Анастасия Булинкова
Рабочим названием платформы .NET было