Лекция 2 Создание нити и идеология posix api




Дата канвертавання25.04.2016
Памер181.17 Kb.

Лекция 2

Создание нити и идеология POSIX API


При выбранном нами для изучения низкоуровневом подходе к поддержке нитей в языке все операции связанные с ними выражаются явно через вызовы функций. Соответственно теперь, когда мы получили общее представление о том, что такое нить, пора рассмотреть вопрос каким же образом мы можем создавать нити и управлять ими в наших программах. Напомню, что мы говорим о программах на языке C и интерфейсе поддержки нитей соответствующему стандарту POSIX. Согласно нему нить создается при помощи следующего вызова:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,

void* (*start)(void *), void *arg)


Упрощенно вызов pthread_create(&thr,NULL,start,NULL) создаст нить которая начнет выполнять функцию start и запишет в переменную thr идентификатор созданной нити. На примере этого вызова мы подробно рассмотрим несколько вспомогательных концепций POSIX API с тем, чтобы не останавливаться на них дальше.

Первый аргумент этой функции thread - это указатель на переменную типа pthread_t, в которую будет записан идентификатор созданной нити, который в последствии можно будет передавать другим вызовам, когда мы захотим сделать что-либо с этой нитью. Здесь мы сталкиваемся с первой особенностью POSIX API а именно с непрозрачностью базовых типов. Дело в том, что мы практически ничего не можем сказать про тип pthread_t. Мы не знаем целое ли это или указатель? Мы не можем сказать существует ли упорядоченность между значениями этого типа, то есть можно ли выстроить из них неубывающую цепочку. Единственное что сказано в стандарте это что эти значения можно копировать, и что используя вызов int pthread_equal(pthread_t thr1, pthread_t thr2) мы можем установить что оба идентификатора thr1 и thr2 идентифицируют одну и ту же нить (при этом они вполне могут быть неравны в смысле оператора равенства). Подобными свойствами обладает большинство типов используемых в данном стандарте, более того, как правило, значения этих типов даже нельзя копировать!

Второй аргумент этой функции attr – это указатель на переменную типа pthread_attr_t которая задает набор некоторых свойств создаваемой нити. Здесь мы сталкиваемся со второй особенностью POSIX API, а именно с концепцией атрибутов. Дело в том, что в этом API во всех случаях, когда при создании или инициализации некоторого объекта необходимо задать набор неких дополнительных его свойств, вместо указания этого набора при помощи набора параметров вызова используется передача предварительно сконструированного объекта представляющего этот набор атрибутов. Такое решение имеет, по крайней мере, два преимущества. Во-первых, мы можем зафиксировать набор параметров функции без угрозы его изменения в дальнейшем, когда у этого объекта появятся новые свойства. Во-вторых, мы можем многократно использовать один и тот же набор атрибутов для создания множества объектов.

Третий аргумент вызова pthread_create это указатель на функцию типа void* ()(void *). Именно эту функцию и начинает выполнять вновь созданная нить, при этом в качестве параметра этой функции передается четвертый аргумент вызова pthread_create. Таким образом можно с одной стороны параметризовать создаваемую нить кодом который она будет выполнять, с другой стороны параметризовать ее различными данными передаваемыми коду.

Функция pthread_create возвращает нулевое значение в случае успеха и ненулевой код ошибки в случае неудачи. Это также одна из особенностей POSIX API, вместо стандартного для Unix подхода когда функция возвращает лишь некоторый индикатор ошибки а код ошибки устанавливает в переменной errno, функции Pthreads API возвращают код ошибки в результате своего аргумента. Очевидно, это связано с тем что с появлением в программе нескольких нитей вызывающих различные функции возвращающие код ошибки в одну и ту же глобальную переменную errno, наступает полная неразбериха, а именно нет никакой гарантии что код ошибки который сейчас находится в этой переменной является результатом вызова произошедшего в этой а не другой нити. И хотя из-за огромного числа функций уже использующих errno библиотека нитей и обеспечивает по экземпляру errno для каждой нити, что в принципе можно было бы использовать и в самой библиотеке нитей, однако создатели стандарта выбрали более правильный а главное более быстрый подход при котором функции API просто возвращают коды ошибки.

Завершение нити, особенности главной нити


Нить завершается когда происходит возврат из функции start. При этом если мы хотим получить возвращаемое значение функции то мы должны воспользоваться функцией:
int pthread_join(pthread_t thread, void** value_ptr)
Эта функция дожидается завершения нити с идентификатором thread, и записывает ее возвращаемое значение в переменную на которую указывает value_ptr. При этом освобождаются все ресурсы связанные с нитью, и следовательно эта функция может быть вызвана для данной нити только один раз. На самом деле ясно, что многие ресурсы, например, стек и данные специфичные для нити, могут быть уже освобождены при возврате из функции нити, а для возможности выполнения функции pthread_join достаточно хранить идентификатор нити и возвращаемое значение. Однако стандарт говорит лишь о том что ресурсы связанные с нитью будут освобождаться после вызова функции pthread_join.

В случае если нас чем-то не устраивает возврат значения через pthread_join, например, нам необходимо получить данные в нескольких нитях, то следует воспользоваться каким либо другим механизмом, например, можно организовать очередь возвращаемых значений, или возвращать значение в структуре указатель на которую передают в качестве параметра нити. То есть использование pthread_join это вопрос удобства, а не догма, в отличие от случая пары fork() – wait(). Дело тут в том, что в случае если мы хотим использовать другой механизм возврата или нас просто не интересует возвращаемое значение то мы можем отсоединить (detach) нить сказав тем самым что мы хотим освободить ресурсы связанные с нитью сразу по завершению функции нити. Сделать это можно несколькими способами. Во-первых, можно сразу создать нить отсоединенной, задав соответствующий объект атрибутов при вызове pthread_create. Во-вторых, любую нить можно отсоединить вызвав в любой момент ее жизни (то есть до вызова pthread_join) функцию


int pthread_detach(pthread_t thread)
и указав ей в качестве параметра идентификатор нити. При этом нить вполне может отсоединить саму себя получив свой идентификатор при помощи функции pthread_t pthread_self(void). Следует подчеркнуть, что отсоединение нити никоим образом не влияет на процесс ее выполнения, а просто помечает нить как готовую по своем завершении к освобождению ресурсов. Фактически тот же pthread_join, всего лишь получает возвращаемое значение и отсоединяет нить.

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

Помимо возврата из функции нити существует еще один способ завершить ее, а именно вызов аналогичный вызову exit() для процессов:
int pthread_exit(void *value_ptr)
Этот вызов завершает выполняемую нить, возвращая в качестве результата ее выполнения value_ptr. Реально при вызове этой функции нить из нее просто не возвращается. Надо обратить также внимание на тот факт, что функция exit() по-прежнему завершает процесс, то есть в том числе уничтожает все потоки.

Как известно, программа на Си начинается с выполнения функции main(). Нить, в которой выполняется данная функция, называется главной или начальной (так как это первая нить в приложении). С одной стороны это нить обладает многими свойствами обычной нити, для нее можно получить идентификатор, она может быть отсоединена, для нее можно вызвать pthread_join из какой-либо другой нити. С другой стороны она обладает некоторыми особенностями, отличающих ее о других нитей. Во-первых, возврат из этой нити завершает весь процесс, что бывает иногда удобно, так как не надо явно заботиться о завершении остальных нитей. Если мы не хотим чтобы по завершении этой нити остальные нити были уничтожены, то следует воспользоваться функцией pthread_exit. Во-вторых, у функции этой нити не один параметр типа void* как у остальных, а пара argc-argv. Строго говоря функция main не является функцией нити так как в большинстве ОС, она сама вызывается некими функциями которые подготавливают ее выполнение автоматически формируемыми компилятором. В-третьих, многие реализации отводят на стек начальной нити гораздо больше памяти чем на стеки остальных нитей. Очевидно, это связано с тем что уже существует много однониточных приложений (то есть традиционных приложений) требующих значительного объема стека, а от автора нового многониточного приложения можно потребовать ограниченности аппетитов.


Жизненный цикл нити.


Рассмотрим теперь жизненный цикл нити, а именно последовательность состояний в которых пребывает нить за время своего существования. В целом можно выделить четыре таких состояния:


Название состояния нити

Что означает

Готова (Ready)

Нить готова к выполнению, но ожидает процессора. Возможно она только что была создана, была вытеснена с процессора другой нитью, или только что была разблокирована (вышла из соответствующего состояния).

Выполняется (Running)

Нить сейчас выполняется. Следует заметить, что на многопроцессорной машине может быть несколько нитей в таком состоянии.

Заблокирована (Blocked)

Нить не может выполняться, так как ожидает чего-либо. Например, окончания операции ввода-вывода, сигнала от условной переменной, получения mutex и т.п.

Завершена (Terminated)

Нить была завершена, например, вследствие возврата из функции нити, вызова pthread_exit, прерывания выполнения нити (cancellation). Нить при этом еще не была отсоединена и для нее не была вызвана функция pthread_join. Как только происходит одно из этих событий, нить перестает существовать.

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




Нить создается





Ожидание закончено

Ready


Blocked






Вытеснена

с процессора


Взята на

процессор

Ожидание чего либо


Running


Terminated


Завершение или прерывание нити


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

Выполняющаяся нить, скорее всего, рано или поздно либо перейдет в состояние «заблокирована», вызвав операцию ожидающую чего-то, например, окончания ввода-вывода, прихода сигнала или поднятия семафора, либо перейдет в состояние «готова» будучи снята с процессора или более высокоприоритетной нитью или просто потому что исчерпала свой квант времени. Здесь надо подчеркнуть разницу между вытеснением (preemption) то есть снятием с процессора вследствие появления готовой более приоритетной задачи, и снятием нити вследствие истечения ее кванта времени. Дело в том, что типичная ошибка предполагать что первое подразумевает второе. Существуют политики планирования которые просто не поддерживают понятие кванта времени. Такова, например политика планирования по умолчанию для нитей в ОС Solaris. Такова одна из стандартных (в смысле POSIX) политик планирования реального времени SCHED_FIFO.

Заблокированная нить, дождавшись события которого она ожидала, переходит в состояние «готова» при этом, конечно в случае если есть такая возможность, она сразу перейдет в состояние выполнения.

Наконец выполняющаяся нить может завершиться тем или иным способом. Например в следствии возврата из функции нити, вызова функции pthread_exit или вследствие насильственного прерывания ее выполнения при помощи вызова pthread_cancel. При этом если нить была отсоединена то она сразу освобождает все связанные с ней ресурсы и перестает существовать (На самом деле она, скорее всего, просто будет повторно использована библиотекой поддержки нитей, поскольку создание нити не самая дешевая операция). В случае если нить не была отсоединена то она, возможно, освободит часть ресурсов, после чего перейдет в состояние «завершена», в котором и будет находиться до тех пор, пока не будет отсоединена либо с помощью pthread_detach либо pthread_join. После чего она опять же освободит все ресурсы и прекратит существование.


Простое многонитевое приложение

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


Рассмотрим сначала наивную реализацию эхо-сервера, в которой сервер состоит из одного процесса, который в цикле принимает соединения, далее для открытого соединения он пытается получить порцию данных от клиента и тут же отослать ее обратно, когда клиент закрывает соединение сервер снова переходит в состояние ожидания соединений.
echo-dumb.c
#include

#include

#include

#include

#include

#include

#include

#include


#include "echo-common.h"
int

main(int argc, char **argv)

{

int listenSocket = getServerSocket(5000);


while(1)

{

int dataSocket;



size_t dataSent,dataToSend;

int result;

char answer[256];
if((dataSocket=accept(listenSocket,(struct sockaddr*)NULL, NULL)) < 0)

die("accept()",errno);


printf("Accepted\n");

while(1)


{

result = recv(dataSocket,answer,sizeof(answer),0);


if(result < 0)

{

if(errno==EINTR)



continue;

else


{

bark("recv()",errno);

break;

}

}



else if(result==0)

break;


else

{

dataToSend = result;



dataSent = 0;

while(dataSent < dataToSend)

{

result = send(dataSocket, answer+dataSent, dataToSend-dataSent, 0);



if(result < 0 && errno != EINTR)

{

bark("send()",errno);



break;

}

else



dataSent +=result;

}
if(dataSent < dataToSend)

break;

}

}



printf("Done. Closing\n");

close(dataSocket);

}

return 0;



}
В заголовочном файле echo-common.h включенном в начале этого примера объявлен ряд вспомогательных функции которые мы будем использовать во всех остальных вариантах эхо-сервера. Функция getServerSocket(port) возвращает файловый дескриптор TCP-сокета который привязан ко всем ip адресам данной машины и порту port, кроме того, он переведен в состояние LISTEN, то есть готов принимать соединения от клиентов. Функция bark(func,errcode) выводит сообщение об ошибке errcode предваряя его указанием места где произошла ошибка – func. А функция die(func,errorcode) дополнительно к этой диагностической печати еще и завершает процесс. Вот собственно код этого заголовочного файла, а также файла echo-common.c в котором даны определения этих функций.
echo-common.h
#ifndef __ECHO_COMMON_H__

#define __ECHO_COMMON_H__


extern void die(const char *function, int errcode);

extern void bark(const char *function, int errcode);

extern int getServerSocket(unsigned short int port);
#endif //__ECHO_COMMON_H__
echo-common.c
#include

#include

#include

#include

#include

#include

#include

#include

void

die(const char *func, int err)



{

fprintf(stderr,"%s: %s\n",strerror(err));

abort();

}
void

bark(const char *func, int err)

{

fprintf(stderr,"%s: %s\n",strerror(err));



}

int


getServerSocket(unsigned short int port)

{

int listenSocket;



struct sockaddr_in listenSockaddr;

if((listenSocket=socket(PF_INET,SOCK_STREAM,0))<0)

die("socket()",errno);
memset(&listenSockaddr, 0, sizeof(listenSockaddr));

listenSockaddr.sin_family = PF_INET;

listenSockaddr.sin_port = htons(port);

listenSockaddr.sin_family = INADDR_ANY;


if(bind(listenSocket,(struct sockaddr*)&listenSockaddr, sizeof(listenSockaddr)) < 0)

die("bind()",errno);


if(listen(listenSocket,5)<0)

die("listen()",errno);


return listenSocket;

}
Основной недостаток данной реализации заключается в его неспособности поддерживать одновременно несколько соединений и обрабатывать поступающую по ним информацию. Действительно, как только данный сервер принимает при помощи accept новое соединение, он становится не способен принимать другие соединения до окончания работы с данным. Единственное что могут делать такие не обслуженные соединения это находиться в очереди запросов на соединение(backlog queue). Но, во-первых, размер этой очереди огранен, во-вторых, соединение может находиться в ней ограниченное время, и, наконец, в-третьих, никому не понравится сервер заставляющий ждать своих клиентов долгое время. Заметим также что в силу уже упоминавшейся асинхронности реального мира приход запросов от нескольких клиентов одновременно вещь обычная. Соответственно, все эти факторы делают данный вариант эхо-сервера неприменимым в реальности.


Пытаясь противопоставить что-нибудь асинхронности реального мира мы приходим к реализации эхо-сервера эксплуатирующей возможности concurrency за счет использования нескольких процессов. Более конкретно к модели в которой на каждое новое соединение создается новый процесс для его обработки.
echo-fork.c
#include

#include

#include

#include

#include

#include

#include

#include

#include
#include "echo-common.h"
int

main(int argc, char **argv)

{

int listenSocket = getServerSocket(5000);


while(1)

{

int dataSocket;



pid_t childPid;

int result;


if((dataSocket=accept(listenSocket,(struct sockaddr*)NULL,NULL)) < 0)

die("accept()",errno);


printf("Accepted\n");
while((result=waitpid(-1,NULL,WNOHANG) ) > 0);
if(result < 0 && errno != ECHILD)

die("wait()",errno);


if((childPid = fork())==(pid_t)-1)

die("fork()",errno);


if(childPid == 0)

{

size_t dataSent,dataToSend;



char answer[256];
close(listenSocket);

while(1)


{

result = recv(dataSocket,answer,sizeof(answer),0);

if(result < 0)

{

if(errno==EINTR)



continue;

else


{

bark("recv()",errno);

break;

}

}



else if(result==0)

break;


else

{

dataToSend = result;



dataSent = 0;

while(dataSent < dataToSend)

{

result = send(dataSocket, answer+dataSent, dataToSend-dataSent, 0);



if(result < 0 && errno != EINTR)

{

bark("send()",errno);



break;

}

else



dataSent +=result;

}

if(dataSent < dataToSend)



break;

}

}



printf("Done. Closing\n");

close(dataSocket);

exit(0);

}

else



{

close(dataSocket);

}

}

return 0;



}
В принципе данная реализация уже вполне удовлетворительна. Однако она, во-первых, не способна выдерживать большую нагрузку (в смысле числа одновременных соединений) из-за большой стоимости создания и поддержания процессов, а во-вторых, в случае если мы захотим чтобы информация поступающая по каждому соединению как-то воздействовала на общее состояние сервера (что является обычной ситуацией в реальности) то нам придется воспользоваться средствами межпроцессного взаимодействия такими как трубопроводы, очереди сообщений или общая память с семафорами.
Некоторый шаг вперед по сравнению с этим подходом, особенно в смысле претензии касающейся сложности взаимодействия между различными соединениями представляет собой подход при котором для обслуживания нового соединения используется новая нить.
echo-thread.c
#include

#include

#include

#include

#include

#include

#include

#include

#include
#include "echo-common.h"

void*


worker(void *sock)

{

int dataSocket = *((int*)sock);



size_t dataSent,dataToSend;

char answer[256];

int result;
free(sock);

while(1)


{

result = recv(dataSocket,answer,sizeof(answer),0);

if(result < 0)

{

if(errno==EINTR)



continue;

else


{

bark("recv()",errno);

break;

}

}



else if(result==0)

break;


else

{

dataToSend = result;



dataSent = 0;

while(dataSent < dataToSend)

{

result = send(dataSocket, answer+dataSent, dataToSend-dataSent,0);



if(result < 0 && errno != EINTR)

{

bark("send()",errno);



break;

}

else



dataSent +=result;

}

if(dataSent < dataToSend)



break;

}

}



printf("Done. Closing\n");

close(dataSocket);


return NULL;

}
int

main(int argc, char **argv)

{

int listenSocket = getServerSocket(5000);



pthread_attr_t workerAttr;
pthread_attr_init(&workerAttr);
pthread_attr_setdetachstate(&workerAttr,PTHREAD_CREATE_DETACHED);

while(1)


{

int *dataSocket = malloc(sizeof(int));

struct sockaddr_in dataSockaddr;

socklen_t dataSockaddrSize = sizeof(dataSockaddr);

pthread_t workerThr;

int errcode;


if((*dataSocket=accept(listenSocket,(struct sockaddr*)&dataSockaddr, &dataSockaddrSize)) < 0)

die("accept()",errno);

printf("Accepted\n");
if((errcode=pthread_create(&workerThr,&workerAttr,worker,dataSocket)!=0))

die("pthread_create()",errcode);

}

return 0;



}
Действительно при таком подходе мы легко можем организовать взаимодействие между различными соединениями, поскольку все нити работают в общей памяти. Надо только правильно организовать доступ к общим данным, но об этом мы поговорим в следующей главе (на следующем занятии). Что касается требования касающегося высокой производительности, то степень, в которой данная реализация ему удовлетворяет, сильно зависит от похода к реализации поддержки нитей применяемых в данной библиотеке нитей или ОС.
Решением которое, с одной стороны безусловно удовлетворит требованию производительности, а с другой стороны обеспечит простоту работы с общими данными, является реализация основанная на использовании неблокирующего ввода-вывода и конечных автоматов, когда мы в одном процессе поддерживаем одновременно несколько вызовов и используем вызов select для того чтобы определить по какому из соединений можно сечас производить обмен.
echo-nb.c
#include

#include

#include

#include

#include

#include

#include

#include

#include

#include


#include "echo-common.h"
struct connection_cb

{

int dataSocket;



char data[256];

int dataSent;

int dataToSend;

int isReading;

struct connection_cb *next;

};
struct connection_cb *connections = NULL;


int

main(int argc, char **argv)

{

int listenSocket = getServerSocket(5000);



if(fcntl(listenSocket,F_SETFL,O_NONBLOCK)<0)

die("fcntl()",errno);

while(1)

{

fd_set readFdSet;



fd_set writeFdSet;

struct connection_cb *currentConn, **currentConnPtr, *tempConn;

int maxFdNum;
FD_ZERO(&readFdSet);

FD_ZERO(&writeFdSet);


FD_SET(listenSocket,&readFdSet);

maxFdNum = listenSocket;

for(currentConn = connections;

currentConn!=NULL;

currentConn = currentConn->next

)

{



if(currentConn->isReading)

FD_SET(currentConn->dataSocket,&readFdSet);

else

FD_SET(currentConn->dataSocket,&writeFdSet);



maxFdNum = currentConn->dataSocket > maxFdNum ?currentConn->dataSocket : maxFdNum;

}

if(select(maxFdNum+1,&readFdSet,&writeFdSet,NULL,NULL) < 0)



{

if(errno == EINTR)

continue;

die("select()",errno);

}

currentConnPtr=&connections;



while(*currentConnPtr!=NULL)

{

if((*currentConnPtr)->isReading &&



FD_ISSET((*currentConnPtr)->dataSocket,&readFdSet))

{

int result = recv((*currentConnPtr)->dataSocket, (*currentConnPtr)->data, sizeof((*currentConnPtr)->data),0);


if(result < 0)

{

if(errno!=EINTR && errno!=EAGAIN && errno!=EWOULDBLOCK)



{

bark("recv()",errno);

close((*currentConnPtr)->dataSocket);

tempConn = *currentConnPtr;

*currentConnPtr = (*currentConnPtr)->next;

free(tempConn);

continue;

}

}



else if(result==0)

{

close((*currentConnPtr)->dataSocket);



tempConn = *currentConnPtr;

*currentConnPtr = (*currentConnPtr)->next;

free(tempConn);

continue;

}

else


{

(*currentConnPtr)->dataToSend = result;

(*currentConnPtr)->dataSent = 0;

(*currentConnPtr)->isReading = 0;

}

}

else if(FD_ISSET((*currentConnPtr)->dataSocket,&writeFdSet))



{

int result = send((*currentConnPtr)->dataSocket, (*currentConnPtr)->data+(*currentConnPtr)->dataSent, (*currentConnPtr) ->dataToSend-(*currentConnPtr)->dataSent, 0);

if(result < 0)

{

if(errno!=EINTR && errno!=EAGAIN)



{

bark("write()",errno);

close((*currentConnPtr)->dataSocket);

tempConn = *currentConnPtr;

*currentConnPtr = (*currentConnPtr)->next;

free(tempConn);

continue;

}

}



else

{

(*currentConnPtr)->dataSent +=result;


if((*currentConnPtr)->dataSent >= (*currentConnPtr)->dataToSend)

(*currentConnPtr)->isReading = 1;

}

}
currentConnPtr = &((*currentConnPtr)->next);



}

if(FD_ISSET(listenSocket,&readFdSet))

{

while(1)


{

int result = accept(listenSocket,(struct sockaddr*)NULL,NULL);

if(result < 0)

{

if(errno==EAGAIN || errno == EWOULDBLOCK)



break;

die("accept()",errno);

}

else


{

*currentConnPtr = malloc(sizeof(struct connection_cb));


if(*currentConnPtr==NULL)

die("malloc()",0);


if(fcntl(result,F_SETFL,O_NONBLOCK)<0)

die("fcntl()",errno);


(*currentConnPtr)->dataSocket = result;

(*currentConnPtr)->isReading = 1;

(*currentConnPtr)->next = 0;

currentConnPtr = &((*currentConnPtr)->next);

}

}

}



}

return 0;

}
Если мы внимательно приглядимся к этой программе, то увидим что такие данные как data, dataSocket, dataToSend, dataSent, которые раньше находились либо в стеке нового процесса, либо нити, теперь находятся в отдельной структуре. Фактически эта структура представляет некий контекст псевдонити или псевдопроцесса в котором с одной стороны хранятся уже упомянутые данные ассоциированные с данным соединением, с другой стороны член isReading представляющий собой аналог счетчика команд (он говорит нам в каком состоянии мы сейчас находимся то есть должны ли мы читать или писать).

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



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


База данных защищена авторским правом ©shkola.of.by 2016
звярнуцца да адміністрацыі

    Галоўная старонка