Обратите также внимание на то, что главный поток не закрывает присоединенный сокет, что всегда происходило, когда параллельный сервер вызывал функцию
fork
. Это объясняется тем, что все потоки внутри процесса совместно используют все дескрипторы, поэтому если главному потоку потребуется вызвать функцию
close
, это приведет к закрытию соединения. Создание нового потока не влияет на счетчики ссылок для открытых дескрипторов, в отличие от того, что происходит при вызове функции
fork
.
В этой программе имеется одна неявная
ошибка, о которой рассказывается в разделе 26.5. Можете ли вы ее обнаружить? (См. упражнение 26.5.)
Передача аргументов новым потокам
Мы уже упомянули, что в листинге 26.2 мы преобразуем целочисленную переменную
connfd
к указателю на неопределенный тип (
void
), но этот способ не работает в некоторых системах. Для корректной обработки данной ситуации требуются дополнительные усилия.
В первую очередь, заметим, что мы не можем просто передать адрес
connfd
нового потока, то есть следующий код не будет работать:
int main(int argc, char **argv) {
int listenfd, connfd;
...
for (;;) {
len = addrlen;
connfd = Accept(listenfd, cliaddr, &len);
Pthread_create(&tid, NULL, &doit, &connfd);
}
}
static void* doit(void *arg) {
int connfd;
connfd = *((int*)arg);
Pthread_detach(pthread_self);
str_echo(connfd); /* та же функция, что и прежде */
Close(connfd); /* мы закончили с присоединенным сокетом */
return(NULL);
}
С точки зрения ANSI С здесь все в порядке: мы гарантированно можем преобразовать целочисленный указатель к типу
void*
и затем обратно преобразовать получившийся указатель на неопределенный тип к целочисленному указателю. Проблема заключается в другом — на что именно он будет указывать?
В главном потоке имеется одна целочисленная переменная
connfd
, и при каждом вызове функции
accept
значение этой переменной меняется на новое (в соответствии с новым присоединенным сокетом). Может сложиться следующая ситуация:
Функция
accept
возвращает управление, записывается новое значение переменной
connfd
(допустим, новый дескриптор равен 5) и в главном потоке вызывается функция
pthread_create
. Указатель на
connfd
(а не фактическое его значение!) является последним аргументом функции
pthread_create
.
Создается новый поток, и начинает выполняться функция
doit
.
Готово другое соединение, и главный поток снова начинает выполняться (прежде, чем начнется выполнение вновь созданного потока). Завершается функция
accept
, записывается новое значение переменной
connfd
(например, значение нового дескриптора равно 6) и главный поток вновь вызывает функцию
pthread_create
.
Хотя созданы два новых потока, оба они будут работать с одним и тем же последним значением переменной
connfd
, которое, согласно нашему предположению, равно 6. Проблема заключается в том, что несколько потоков получают доступ к совместно используемой переменной (целочисленному значению, хранящемуся в
connfd
) при отсутствии синхронизации. В листинге 26.2 мы решаем эту проблему, передавая значение переменной
connfd
функции
pthread_create
, вместо того чтобы передавать указатель на это значение. Этот метод работает благодаря тому способу, которым целочисленные значения в С передаются вызываемой функции (копия значения помещается в стек вызванной функции).
В листинге 26.3 показано более удачное решение описанной проблемы.
Листинг 26.3. Эхо-сервер TCP, использующий потоки с более переносимой передачей аргументов
//threads/tcpserv02.c
1 #include "unpthread.h"
2 static void *doit(void*); /* каждый поток выполняет эту функцию */