Литмир - Электронная Библиотека
Содержание  
A
A
Ждем, когда завершится выполнение какого-либо потока

53-54
 Мы вызываем функцию потоков
thr_join
Solaris с нулевым первым аргументом, чтобы дождаться завершения выполнения какого-либо из наших потоков. К сожалению, в Pthreads не предусмотрен способ, с помощью которого мы могли бы ждать завершения выполнения любого потока, и функция
pthread_join
требует, чтобы мы точно указали, завершения какого потока мы ждем. В разделе 26.9 мы увидим, что решение этой проблемы в случае применения технологии Pthreads оказывается сложнее и требует использования условной переменной для сообщения главному потоку о завершении выполнения дополнительного потока.

ПРИМЕЧАНИЕ

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

В листинге 26.9 показана функция

do_get_read
, которая выполняется каждым потоком. Эта функция устанавливает соединение TCP, посылает серверу команду HTTP
GET
и считывает ответ сервера.

Листинг 26.9. Функция do_get_read

//threads/web01.c

61 void*

62 do_get_read(void *vptr)

63 {

64  int fd, n;

65  char line[MAXLINE];

66  struct file *fptr;

67  fptr = (struct file*)vptr;

68  fd = Tcp_connect(fptr->f_host, SERV);

69  fptr->f_fd = fd;

70  printf("do_get_read for %s, fd %d, thread %d\n",

71   fptr->f_name, fd, fptr->f_tid);

72  write_get_cmd(fptr);

73  /* Чтение ответа сервера */

74  for (;;) {

75   if ((n = Read(fd, line, MAXLINE)) == 0)

76    break; /* сервер закрывает соединение */

77   printf ("read %d bytes from %s\n", n, fptr->f_name);

78  }

79  printf("end-of-file on %s\n\", fptr->f_name);

80  Close(fd);

81  fptr->f_flags = F_DONE; /* сбрасываем F_READING */

82  return (fptr); /* завершение потока */

83 }

Создание сокета TCP, установление соединения

68-71
 Создается сокет TCP, и с помощью функции
tcp_connect
устанавливается соединение. В данном случае используется обычный блокируемый сокет, поэтому поток будет блокирован при вызове функции
connect
, пока не будет установлено соединение.

Отправка запроса серверу

72
 Функция
write_get_cmd
формирует команду HTTP
GET
и отсылает ее серверу. Мы не показываем эту функцию заново, так как единственным отличием от листинга 16.12 является то, что в версии, использующей потоки, не вызывается макрос
FD_SET
и не используется
maxfd
.

Чтение ответа сервера

73-82
 Затем считывается ответ сервера. Когда соединение закрывается сервером, устанавливается флаг
F_DONE
и функция возвращает управление, завершая выполнение потока.

Мы также не показываем функцию

home_page
, так как она полностью повторяет версию, приведенную в листинге 16.10.

Мы вернемся к этому примеру, заменив функцию Solaris

thr_join
на более переносимую функцию семейства Pthreads, но сначала нам необходимо обсудить взаимные исключения и условные переменные.

26.7. Взаимные исключения

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

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

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

1. Выполняется поток А, который загружает в регистр значение переменной

nconn
(равное 3).

2. Система переключается с выполнения потока А на выполнение потока В. Регистры потока А сохранены, регистры потока В восстановлены.

3. Поток В выполняет три действия, составляющие оператор декремента в языке С (

nconn--
), сохраняя новое значение переменной
nconn
, равное 2.

4. Впоследствии в некоторый момент времени система переключается на выполнение потока А. Восстанавливаются регистры потока А, и он продолжает выполняться с того места, на котором остановился, а именно начиная со второго этапа из трех, составляющих оператор декремента. Значение регистра уменьшается с 3 до 2, и значение 2 записывается в переменную

nconn
.

Окончательный результат таков: значение

nconn
равно 2, в то время как оно должно быть равным 1. Это ошибка.

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

290
{"b":"225366","o":1}