Необходимость синхронизации и гонки
Пренебрежение вопросами синхронизации в многопоточной системе может привести к неправильному решению задачи или даже к краху системы. Сложность проблемы синхронизации кроется в нерегулярности возникающих ситуаций. Все определяется взаимными скоростями потоков и моментами их прерывания. Поэтому отладка взаимодействующих потоков является сложной задачей. Ситуации, когда два или более потоков обрабатывают разделяемые данные и конечный результат зависит от соотношения скоростей потоков, называются гонками.
Критическая секция
Критическая секция — это часть программы, результат выполнения которой может непредсказуемо меняться, если переменные, относящиеся к этой части программы, изменяются другими потоками в то время, когда выполнение этой части еще не завершено. Критическая секция всегда определяется по отношению к определенным критическим данным, при несогласованном изменении которых могут возникнуть нежелательные эффекты. Во всех потоках, работающих с критическими данными, должна быть определена критическая секция. Заметим, что в разных потоках критическая секция состоит в общем случае из разных последовательностей команд.
Чтобы исключить эффект гонок по отношению к критическим данным, необходимо обеспечить, чтобы в каждый момент времени в критической секции, связанной с этими данными, находился только один поток. При этом неважно, находится этот поток в активном или в приостановленном состоянии. Этот прием называют взаимным исключением.
Самый простой и в то же время самый неэффективный способ обеспечения взаимного исключения состоит в том, что операционная система позволяет потоку запрещать любые прерывания на время его нахождения в критической секции. Однако этот способ практически не применяется, так как опасно доверять управление системой пользовательскому потоку — он может надолго занять процессор, а при крахе потока в критической секции крах потерпит вся система, потому что прерывания никогда не будут разрешены.
Взаимоисключения
Объекты-взаимоисключения (мьютексы, mutex - от MUTual EXclusion) позволяют координировать взаимное исключение доступа к разделяемому ресурсу. Сигнальное состояние объекта (т.е. состояние "установлен") соответствует моменту времени, когда объект не принадлежит ни одной нити и его можно "захватить". И наоборот, состояние "сброшен" (не сигнальное) соответствует моменту, когда какая-либо нить уже владеет этим объектом. Доступ к объекту разрешается, когда нить, владеющая объектом, освободит его.
Две (или более) нити могут создать мьютекс с одним и тем же именем, вызвав функцию CreateMutex. Первая нить действительно создает мьютекс, а следующие - получают дескриптор уже существующего объекта. Это дает возможность нескольким нитям получить дескриптор одного и того же мьютекса, освобождая программиста от необходимости заботиться о том, кто в действительности создает мьютекс. Если используется такой подход, желательно установить флаг bInitialOwner в FALSE, иначе возникнут определенные трудности при определении действительного создателя мьютекса.
Для того чтобы объявить взаимоисключение принадлежащим текущей нити, надо вызвать одну из ожидающих функций. Нить, которой принадлежит объект, может его "захватывать" повторно сколько угодно раз (это не приведет к самоблокировке), но столько же раз она должна будет его освобождать с помощью функции ReleaseMutex.
События
Объекты-события используются для уведомления ожидающих нитей о наступлении какого-либо события. Различают два вида событий - с ручным и автоматическим сбросом. Ручной сброс осуществляется функцией ResetEvent. События с ручным сбросом используются для уведомления сразу нескольких нитей. При использовании события с автосбросом уведомление получит и продолжит свое выполнение только одна ожидающая нить, остальные будут ожидать дальше.
Функция CreateEvent создает объект-событие, SetEvent - устанавливает событие в сигнальное состояние, ResetEvent - сбрасывает событие. Функция PulseEvent устанавливает событие, а после возобновления ожидающих это событие нитей (всех при ручном сбросе и только одной при автоматическом), сбрасывает его. Если ожидающих нитей нет, PulseEvent просто сбрасывает событие.
Семафоры
Объект-семафор - это фактически объект-взаимоисключение со счетчиком. Данный объект позволяет "захватить" себя определенному количеству нитей. После этого "захват" будет невозможен, пока одна из ранее "захвативших" семафор нитей не освободит его. Семафоры применяются для ограничения количества нитей, одновременно работающих с ресурсом. Объекту при инициализации передается максимальное число нитей, после каждого "захвата" счетчик семафора уменьшается. Сигнальному состоянию соответствует значение счетчика больше нуля. Когда счетчик равен нулю, семафор считается не установленным (сброшенным).
Функция CreateSemaphore создает объект-семафор с указанием и максимально возможного начального его значения, OpenSemaphore – возвращает дескриптор существующего семафора, захват семафора производится с помощью ожидающих функций, при этом значение семафора уменьшается на единицу, ReleaseSemaphore - освобождение семафора с увеличением значения семафора на указанное в параметре число.
Сокеты Windows
Спецификация сокетов Windows определяет интерфейс для программирования сети, которая основывается на понятии "сокетов", введенном в Berkeley Software Distribution (BSD). Она включает в себя набор процедур в стиле сокетов Berkeley, а также дополнительный набор функций, специфичных Windows. WinSock (Windows Socket) – это Windows API, который взаимодействует с сетью.
Конечными точками соединения «клиент-сервер» являются socket’ы. И у клиента, и у сервера есть socket. Socket связан с определенным IP адресом и номером порта. IP (Internet Protocol) адрес состоит из 4-х чисел от 0 до 225, разделенных точками. В дополнении к адресу, TCP (Transfer Control Protocol) добавляет порт, по которому должна быть передана информация. Если IP-адреса используются для определения компьютера, которому надо передать данные, то порт определяет, какой запущенный процесс должен принять эти данные. Номер порта 16 битовый, поэтому ограничен 65536 значениями. Множество портов связаны с определенными службами. Например, www использует 80 порт, FTP – 21, e-mail использует 25 (SMTP) и 101 (POP). В пользовательских приложениях рекомендуется номера портов выбирать, начиная от 1024.
Почти все WinSock функции оперируют socket’ами. С помощью socket’а управляют соединением. Обе «стороны» соединения используют socket’ы, и они (socket’ы) платформенно-независимы. Это значит, что машина с Windows может «общаться» по сети с Unix машиной, используя сокеты. По socket’ам данные могут передаваться и приниматься.
Выделяют два типа socket’ов: потоковый socket (SOCK_STREAM) и, так называемый, дейтаграммный socket (datagram socket, SOCK_DGRAM). Потоковый вариант разработан для приложений, нуждающихся в надежном соединении и часто использующем продолжительные потоки данных. Протокол, использующийся для данного типа socket’ов – TCP (Transfer Control Protocol). Он чаще всего используется в хорошо известных протоколах, таких как SMTP, POP3, HTTP, TCP.
Дейтаграммные socket’ы используют UDP протокол и большой размер буфера данных. Они применяются в приложениях, которые отправляют данные малых размеров и не нуждаются в идеальной надежности. В отличии от потоковых socket’ов, дейтаграммные socket’ы не гарантируют стопроцентной передачи данных получателю, как и не гарантируют передачи данных в нужном порядке. Данный тип socket’ов полезнее для приложений, где надежность не является высоким приоритетом, таким как скорость (например аудио или видео трансляция).
Связывание (binding) socket’ов. Связать socket значит «прикрепить» определенный адрес (IP адрес и номер порта) к данному socket’у. Это можно сделать вручную, используя связывающую функцию, но в некоторых случаем WinSock сам автоматически свяжет socket.
Способ использования socket’ов зависит от того, где они используются: на клиентской или серверной части. Клиентская часть создает соединение путем создания socket’а и вызовом соединяющей функции с определенной адресной информацией. До того как socket не соединится, он не будет связан с адресом. Когда соединение вызвано, WinSock выберет IP адрес и номер порта для соединения и свяжет с ними socket до того, как клиент фактически соединится с сервером.
Сервер ждет входящих соединений и клиенту необходимо знать IP адрес и номер порта сервера, чтобы установить соединение. Чтобы упростить дело, на сервере всегда используется фиксированный номер порта (обычно это - порт, предусмотренный протоколом по умолчанию).
Ожидание входящего соединения по определенному адресу называется прослушиванием (listening). Обычно, перед тем как «войти» в режим прослушивания, socket должен быть связан с определенным адресом. Когда номер порта этого адреса установлен и зафиксирован (т.е. не изменится), сервер начинает ждать входящие соединения по этому порту. Когда клиент запрашивает соединение с сервером, сервер разрешит ему (или нет) и породит новый socket, который будет конечной точкой связи. Благодаря этому, socket, по которому происходило прослушивание, не используется для передачи данных и может находиться в режиме прослушивания дальше, «принимая» новых клиентов.