Книга: UNIX: разработка сетевых приложений

30.11. Сервер TCP с предварительным порождением потоков, каждый из которых вызывает accept

30.11. Сервер TCP с предварительным порождением потоков, каждый из которых вызывает accept

Ранее в этой главе мы обнаружили, что версии, в которых заранее создается пул дочерних процессов, работают быстрее, чем те, в которых для каждого клиентского запроса приходится вызывать функцию fork. Для систем, поддерживающих потоки, логично предположить, что имеется та же закономерность: быстрее сразу создать пул потоков при запуске сервера, чем создавать по одному потоку по мере поступления запросов от клиентов. Основная идея такого сервера заключается в том, чтобы создать пул потоков, каждый из которых вызывает затем функцию accept. Вместо того чтобы блокировать потоки в вызове accept, мы используем взаимное исключение, как в разделе 30.8. Это позволяет вызывать функцию accept только одному потоку в каждый момент времени. Использовать блокировку файла для защиты accept в таком случае бессмысленно, так как при наличии нескольких потоков внутри данного процесса можно использовать взаимное исключение.

В листинге 30.21 показан заголовочный файл pthread07.h, определяющий структуру Thread, содержащую определенную информацию о каждом потоке.

Листинг 30.21. Заголовочный файл pthread07.h

//server/pthread07.h
1 typedef struct {
2  pthread_t thread_tid; /* идентификатор потока */
3  long thread_count; /* количество обработанных запросов */
4 } Thread;
5 Thread *tptr; /* массив структур Thread */
6 int listenfd, nthreads;
7 socklen_t addrlen;
8 pthread_mutex_t mlock;

Мы также объявляем несколько глобальных переменных, таких как дескриптор прослушиваемого сокета и взаимное исключение, которые должны совместно использоваться всеми потоками.

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

Листинг 30.22. Функция main для сервера TCP с предварительным порождением потоков

//server/serv07.c
 1 #include "unpthread.h"
 2 #include "pthread07.h"
 3 pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;
 4 int
 5 main(int argc, char **argv)
 6 {
 7  int i;
 8  void sig_int(int), thread_make(int);
 9  if (argc == 3)
10   listenfd = Tcp_listen(NULL, argv[1], &addrlen);
11  else if (argc == 4)
12   listenfd = Tcp_1isten(argv[1], argv[2], &addrlen);
13  else
14   err_quit("usage: serv07 [ <host> ] <port#> <#threads>");
15  nthreads = atoi(argv[argc - 1]);
16  tptr = Calloc(nthreads, sizeof(Thread));
17  for (i = 0; i < nthreads; i++)
18  thread_make(i); /* завершается только основной поток */
19  Signal(SIGINT, sig_int);
20  for (;;)
21   pause(); /* потоки все выполнили */
22 }

Функции thread_make и thread_main показаны в листинге 30.23.

Листинг 30.23. Функции thread_make и thread_main

//server/pthread07.c
 1 #include "unpthread.h"
 2 #include "pthread07.h"
 3 void
 4 thread_make(int i)
 5 {
 6  void *thread_main(void*);
 7  Pthread_create(&tptr[i].thread_tid, NULL, &thread_main, (void*)i);
 8  return; /* завершается основной поток */
 9 }
10 void*
11 thread_main(void *arg)
12 {
13  int connfd;
14  void web_child(int);
15  socklen_t clilen;
16  struct sockaddr *cliaddr;
17  cliaddr = Malloc(addrlen);
18  printf("thread %d startingn", (int)arg);
19  for (;;) {
20   clilen = addrlen;
21   Pthread_mutex_lock(&mlock);
22   connfd = Accept(listenfd, cliaddr, &clilen);
23   Pthread_mutex_unlock(&mlock);
24   tptr[(int)arg].thread_count++;
25   web_child(connfd); /* обработка запроса */
26   Close(connfd);
27  }
28 }

Создание потоков

7 Создаются потоки, каждый из которых выполняет функцию pthread_main. Единственным аргументом этой функции является порядковый номер потока.

21-23 Функция thread_main вызывает функции pthread_mutex_lock и pthread_mutex_unlock соответственно до и после вызова функции accept.

Сравнивая строки 6 и 7 в табл. 30.1, можно заметить, что эта последняя версия нашего сервера быстрее, чем версия с созданием нового потока для каждого клиентского запроса. Этого можно было ожидать, так как в данной версии мы сразу создаем пул потоков и не тратим время на создание новых потоков по мере поступления клиентских запросов. На самом деле эта версия сервера — самая быстродействующая для всех операционных систем, которые мы испытывали.

В табл. 30.2 показано распределение значений счетчика thread_count структуры Thread, которые мы выводим с помощью обработчика сигнала SIGINT по завершении работы сервера. Равномерность этого распределения объясняется тем, что при выборе потока, который будет блокировать взаимное исключение, алгоритм планирования загрузки потоков последовательно перебирает все потоки в цикле.

ПРИМЕЧАНИЕ

В Беркли-ядрах нам не нужна блокировка при вызове функции accept, так что мы можем использовать версию, представленную в листинге 30.23, без взаимных исключений. Но в результате этого время, затрачиваемое центральным процессором, увеличится. Если рассмотреть два компонента, из которых складывается время центрального процессора — пользовательское и системное время — то окажется, что первый компонент уменьшается при отсутствии блокировки (поскольку блокирование осуществляется в библиотеке потоков, входящей в пользовательское пространство), но системное время возрастает (за счет эффекта «общей побудки», возникающего, когда все потоки, блокированные в вызове функции accept, выходят из состояния ожидания при появлении нового клиентского соединения). Для того чтобы каждое соединение передавалось только одному потоку, необходима некая разновидность взаимного исключения, и оказывается, что быстрее это делают сами потоки, а не ядро.

Оглавление книги


Генерация: 1.378. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз