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