线程池
“池”,我把他归结为一大堆的资源,
比如说,内存,线程,网络连接,数据库的连接等一系列的资源,
因而与之相对应的就有,内存池,线程池,网络连接池,数据库连接池等。
线程池也就是一大堆的线程资源,
它们在程序刚开始运行的时候就存在了,
等需要它们的时候呢,就可以使用它们,
而不必向操作系统去申请。
那么我们为何不在需要的时候再向操作系统申请呢?
这是由于动态的申请线程资源的话,要从应用层转到内核层,
这期间花费的时间是非常慢的,
如果我们要经常的使用线程,而采用动态申请就需要非常多的时间,
这会大大降低程序的效率。
因此,针对这种情况,我们就在程序运行开始时,
就申请一大堆线程,需要的时候就从线程池里边拿,不需熬的时候就让它们挂起。
MSDN提供了CreateThreadPool这个API,用于创建一个线程池,
PTP_POOL WINAPI CreateThreadpool (
_Reserved_ PVOID reserved
) ;
第二个是SetThreadpoolThreadMinimum,用于设置线程池的最小线程数,
BOOL WINAPI SetThreadpoolThreadMinimum (
PTP_POOL ptpp,
DWORD cthrdMic
) ;
第三个是SetThreadpoolThreadMaximum,用于设置最大的线程数,
VOID WINAPI SetThreadpoolThreadMaximum (
PTP_POOL ptpp,
DWORD cthrdMost
) ;
当我们设置完最小和最大线程数的时候,就可以初始化一下线程池的回调环境了,
回调环境是一个TP_CALLBACK_ENVIRON结构体,
我们用InitializeThreadpoolEnvironment来初始化该环境,
VOID InitializeThreadpoolEnvironment (
PTP_CALLBACK_ENVIRON pcbe
) ;
回调环境必须指明我们的工作任务由哪个线程池来处理,
而SetThreadpoolCallbackPool这个API就是来干这个滴,
VOID SetThreadpoolCallbackPool (
PTP_CALLBACK_ENVIRON pcbe,
PTP_POOL ptpp
) ;
最后呢,将一个工作任务添加到线程池的队列中呢,
需要调用TrySubmitThreadpoolCallback,
BOOL WINAPI TrySubmitThreadpoolCallback (
PTP_SIMPLE_CALLBACK pfns,
PVOID pv,
PTP_CALLBACK_ENVIRON pcbe
) ;
为了让线程池一异步的方式执行我们的工作任务函数呢,
我们需要定义一个具有以下原型的函数,格式不能改,名字可改:
VOID CALLBACK SimpleCallback (
PTP_CALLBACK_INSTANCE Instance,
PVOID Context
) ;
测试代码:
# include <windows.h>
void CALLBACK test1 (
PTP_CALLBACK_INSTANCE instance,
PVOID context)
{
printf ( "Pool thread %d is running test1...\n" , GetCurrentThreadId ( ) ) ;
return ;
}
void CALLBACK test2 (
PTP_CALLBACK_INSTANCE instance,
PVOID context)
{
printf ( "Pool thread %d is running test2...\n" , GetCurrentThreadId ( ) ) ;
return ;
}
int _tmain ( int argc, _TCHAR* argv[ ] )
{
PTP_POOL pool = CreateThreadpool ( 0 ) ;
SetThreadpoolThreadMinimum ( pool, 1 ) ;
SetThreadpoolThreadMaximum ( pool, 10 ) ;
TP_CALLBACK_ENVIRON env;
InitializeThreadpoolEnvironment ( & env) ;
SetThreadpoolCallbackPool ( & env, pool) ;
TrySubmitThreadpoolCallback ( test1, 0 , & env) ;
TrySubmitThreadpoolCallback ( test2, 0 , & env) ;
system ( "pause" ) ;
return 0 ;
}
select网络模型
我们在TCP的服务端里边,接收一个客户端的时候,我们调用accept函数,
这个函数会返回一个客户端的socket,我们在主线程里边不停的接收客户的连接,
每当有客户连接时,我们就会在开一个线程,用于对客户的服务。
因此,如果有N个的客户进行连接的话,那么线程数量就会有N+1个(N个服务线程+主线程),
若N比较大,则线程就会非常多,以至于将整个电脑都给拖垮掉。
而我们的select模型呢,就是为了解决这个问题而设计的。
TCP的服务端一样,它需要初始化环境,然后执行绑定,监听等操作。
但是之后,我们会直接开一个线程来对客户进行服务,
然后才是我们原来的一个循环,来接待客户的连接。
typedef struct fd_set
{
u_int fd_count;
SOCKET fd_array[ FD_SETSIZE] ;
} fd_set;
当我们调用accept来获取客户端的连接之后,会调用FD_SET这个宏,
它实际上是会将我们的客户那个socket保存到fd_array这个数组里边去,
因为这个数组最大为64个,所以最多只能有64个客户端进行连接。
我们把服务客户的那个线程叫做工作者线程,在里边我们会调用一个函数叫做select,
int select (
int nfds,
fd_set FAR * readfds,
fd_set FAR * writefds,
fd_set FAR * exceptfds,
const struct timeval FAR * timeout
) ;
这个函数会检查fd_array这个数组里边所有的socket是否有信号到来,
如果有就成功返回,否则会阻塞在这里,不过我们在最后一个参数那里,传一个等待时间。
调用完select之后,
我们可以在调用FD_ISSET这个宏来判断是fd_array这个数组里边的那个socket有信号了。
之后我们就可以进行数据收发了。
示例代码:
# include <winsock2.h>
# include <stdio.h>
# define PORT 6000
# pragma comment ( lib, "Ws2_32.lib" )
fd_set g_fdClientSock;
int clientNum = 0 ;
BOOL WinSockInit ( )
{
WSADATA data = { 0 } ;
if ( WSAStartup ( MAKEWORD ( 2 , 2 ) , & data) )
{
return FALSE;
}
if ( LOBYTE ( data. wVersion) != 2
|| HIBYTE ( data. wVersion) != 2 )
{
WSACleanup ( ) ;
return FALSE;
}
return TRUE;
}
DWORD WINAPI WorkThreadProc ( LPARAM lparam)
{
fd_set fdRead;
FD_ZERO ( & fdRead ) ;
int nRet = 0 ;
char * recvBuffer = ( char * ) malloc ( sizeof ( char ) * 1024 ) ;
if ( recvBuffer == NULL )
{
return - 1 ;
}
memset ( recvBuffer, 0 , sizeof ( char ) * 1024 ) ;
while ( true )
{
fdRead = g_fdClientSock;
timeval tv;
tv. tv_sec = 0 ;
tv. tv_usec = 10 ;
nRet = select ( 0 , & fdRead, NULL , NULL , & tv ) ;
if ( nRet != SOCKET_ERROR )
{
for ( int i = 0 ; i < g_fdClientSock. fd_count; i++ )
{
if ( FD_ISSET ( g_fdClientSock. fd_array[ i] , & fdRead) )
{
memset ( recvBuffer, 0 , sizeof ( char ) * 1024 ) ;
nRet = recv ( g_fdClientSock. fd_array[ i] , recvBuffer, 1024 , 0 ) ;
if ( nRet == SOCKET_ERROR )
{
closesocket ( g_fdClientSock. fd_array[ i] ) ;
clientNum-- ;
FD_CLR ( g_fdClientSock. fd_array[ i] , & g_fdClientSock ) ;
}
else if ( nRet == 0 )
{
closesocket ( g_fdClientSock. fd_array[ i] ) ;
clientNum-- ;
FD_CLR ( g_fdClientSock. fd_array[ i] , & g_fdClientSock ) ;
}
else
{
printf ( "Recv msg:%s\n" , recvBuffer) ;
send (
g_fdClientSock. fd_array[ i] ,
recvBuffer,
strlen ( recvBuffer) ,
0 ) ;
}
}
}
}
}
if ( recvBuffer != NULL )
{
free ( recvBuffer ) ;
}
return 0 ;
}
int main ( )
{
WinSockInit ( ) ;
SOCKET listenSock = socket ( AF_INET, SOCK_STREAM, IPPROTO_TCP) ;
struct sockaddr_in server;
server. sin_family = AF_INET;
server. sin_addr. s_addr = htonl ( INADDR_ANY) ;
server. sin_port = htons ( PORT) ;
int ret = bind ( listenSock, ( sockaddr* ) & server, sizeof ( server) ) ;
ret = listen ( listenSock, 4 ) ;
sockaddr_in clientAddr;
int nameLen = sizeof ( clientAddr ) ;
CreateThread ( NULL , NULL , ( LPTHREAD_START_ROUTINE) WorkThreadProc, NULL , NULL , NULL ) ;
while ( clientNum < FD_SETSIZE )
{
SOCKET clientSock = accept ( listenSock, ( sockaddr* ) & clientAddr, & nameLen ) ;
FD_SET ( clientSock, & g_fdClientSock) ;
clientNum++ ;
}
closesocket ( listenSock) ;
WSACleanup ( ) ;
return 0 ;
}
EventSelect网络模型
select网络模型,它解决了有N个客户端连接,就有N+1个线程的问题,
现在我们只需要2个线程就能搞定了。
但是,我们的服务端仅仅是这样的话,它的效率还是非常低的。
在select模型中,我们在工作者线程里边调用了select函数来判断是否有数据到来了(有信号了),
如果没有信号,select就会卡在这里,虽然我们可以给它一个超时时间,
但是它在底层还是调用了sleep函数。所以它浪费了CPU的时钟周期,
由于我们不知道客户是什么发送数据过来,可能是1秒,可能是1小时,也有可能是1天,
所有等待数据到来占了非常大的一部分时间,效率是比较低的。
接下来我们来看下EventSelect模型,它要解决的问题就是将这个等待数据到来的时间给节省掉了。
首先,初始化网络环境,创建一个监听的socket,然后进行bind,listen操作。
接下来我们会创建一个网络事件对象,
它和我们讲内核态下线程同步里边事件对象很类似,我们可以调用WSACreateEvent来创建它,
WSAEVENT WSACreateEvent ( void ) ;
然后我们再调用WSAEventSelect,来将监听的socket与该事件进行一个关联,
int WSAEventSelect (
SOCKET s,
WSAEVENT hEventObject,
long lNetworkEvents
) ;
因为我们这里是将监听的socket与事件对象进行关联,
因此我们只需要关注两个事件,
一个是客户端的连接,一个是socket关闭这两个事件。
然后,我们可以创建一个工作者线程了。
最后将我们监听的socket和我们创建的那个网络事件对象保存到各自的全局数组里边去。
这时候我们的主线程就结束了。
然后我们来看下我们的工作者线程做了哪些工作。
在工作者线程里边,会有一个死循环,
在循环刚开始的时候,会调用WSAWaitForMultipleEvents函数,
来查看我们那个全局事件对象数组里边是否至少有一个有信号到来,
DWORD WSAWaitForMultipleEvents (
DWORD cEvents,
const WSAEVENT FAR * lphEvents,
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable
) ;
如果该函数执行成功,就返回一个索引值,这个值代表了那个有信号的事件对象的索引值。
我们在主线程里边关注了两个事件,一个是客户端的连接,一个是关闭事件,
那我们如何知道出现了哪个网络事件呢,我需要调用WSAEnumNetworkEvents,
来检测指定的socket上的网络事件。
int WSAEnumNetworkEvents
(
SOCKET s,
WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents
) ;
当我们调用这个函数成功后,
它会将我们指定的socket和事件对象所关联的网络事件的信息保存到
WSANETWORKEVENTS这个结构体里边去,
typedef struct _WSANETWORKEVENTS
{
long lNetworkEvents;
int iErrorCodes[ FD_MAX_EVENTS] ;
} WSANETWORKEVENTS, * LPWSANETWORKEVENTS;
根据这个结构体我们就可以判断是否是我们所关注的网络事件已经发生了。
如果是我们那个客户端连接的事件发生了,
我们就调用accept函数将客户端和服务端进行连接。
连接完成后,我们再次创建一个网络事件对象,然后继续调用WSAEventSelect函数,
将这个事件对象和客户端的那个socket进行一个关联,
此时我们需要关注的网络事件,数据的读和写,还有关闭这三个事件。
然后我们就把客户端的socket和新建的事件对象保存到各自的全局数组里边去。
如果是我们的读的网络事件发生了,那么我们就调用recv函数进行操作。
如果是写的网络事件发生了,我们就可以做一些日志等操作。
若是关闭的事件发生了,就调用closesocket将socket关掉,在数组里将其置零等操作。
最后,在提一下另一个网络模型---异步选择模型,这个模型呢它和我们的事件选择模型很像,
不过它是基于windows消息的,这就说明了我们只能在窗口程序里边来使用,
而事件选择是以事件对象为基础,它不管是控制台还是窗口程序都可以使用,
因此用的较多的也是我们事件选择模型。
# include <winsock2.h>
# include <stdio.h>
# define PORT 6000
# pragma comment ( lib, "Ws2_32.lib" )
SOCKET ArrSocket[ 64 ] = { 0 } ;
WSAEVENT ArrEvent[ 64 ] = { 0 } ;
DWORD dwTotal = 0 ;
DWORD dwIndex = 0 ;
BOOL WinSockInit ( )
{
WSADATA data = { 0 } ;
if ( WSAStartup ( MAKEWORD ( 2 , 2 ) , & data) )
{
return FALSE;
}
if ( LOBYTE ( data. wVersion) != 2
|| HIBYTE ( data. wVersion) != 2 )
{
WSACleanup ( ) ;
return FALSE;
}
return TRUE;
}
DWORD WINAPI WorkThreadProc ( LPARAM lparam)
{
char buf[ 1024 ] = { 0 } ;
SOCKET sockClient = INVALID_SOCKET;
WSANETWORKEVENTS NetWorkEvent = { 0 } ;
while ( TRUE)
{
dwIndex = WSAWaitForMultipleEvents (
dwTotal,
ArrEvent,
FALSE,
100 ,
FALSE) ;
if ( dwIndex == WSA_WAIT_TIMEOUT)
{
continue ;
}
WSAEnumNetworkEvents (
ArrSocket[ dwIndex - WSA_WAIT_EVENT_0] ,
ArrEvent[ dwIndex - WSA_WAIT_EVENT_0] ,
& NetWorkEvent) ;
if ( NetWorkEvent. lNetworkEvents & FD_ACCEPT)
{
if ( NetWorkEvent. iErrorCode[ FD_ACCEPT_BIT] != 0 )
{
continue ;
}
sockClient = accept ( ArrSocket[ dwIndex - WSA_WAIT_EVENT_0] , NULL , NULL ) ;
if ( sockClient == INVALID_SOCKET)
{
continue ;
}
WSAEVENT newEvent = WSACreateEvent ( ) ;
WSAEventSelect (
sockClient,
newEvent,
FD_READ | FD_WRITE | FD_CLOSE) ;
ArrSocket[ dwTotal] = sockClient;
ArrEvent[ dwTotal] = newEvent;
++ dwTotal;
}
if ( NetWorkEvent. lNetworkEvents & FD_READ)
{
if ( NetWorkEvent. iErrorCode[ FD_READ_BIT] != 0 )
{
continue ;
}
int len = recv ( ArrSocket[ dwIndex - WSA_WAIT_EVENT_0] , buf, sizeof ( buf) , 0 ) ;
printf ( "Recv: %s\n" , buf) ;
send ( ArrSocket[ dwIndex - WSA_WAIT_EVENT_0] , buf, strlen ( buf) , 0 ) ;
}
if ( NetWorkEvent. lNetworkEvents & FD_WRITE)
{
if ( NetWorkEvent. iErrorCode[ FD_WRITE_BIT] != 0 )
{
continue ;
}
printf ( "Send something\n" ) ;
}
if ( NetWorkEvent. lNetworkEvents & FD_CLOSE)
{
if ( NetWorkEvent. iErrorCode[ FD_CLOSE_BIT] != 0 )
{
continue ;
}
closesocket ( ArrSocket[ dwIndex - WSA_WAIT_EVENT_0] ) ;
ArrSocket[ dwIndex - WSA_WAIT_EVENT_0] = 0 ;
}
}
}
int main ( )
{
WinSockInit ( ) ;
SOCKET sockListen = INVALID_SOCKET;
sockListen = socket ( AF_INET, SOCK_STREAM, IPPROTO_TCP) ;
if ( sockListen == INVALID_SOCKET)
{
printf ( "Create socket error" ) ;
return - 1 ;
}
sockaddr_in service;
service. sin_family = AF_INET;
service. sin_addr. S_un. S_addr = INADDR_ANY;
service. sin_port = htons ( PORT) ;
if ( bind ( sockListen, ( sockaddr* ) & service, sizeof ( service) ) == SOCKET_ERROR)
{
printf ( "bind failed\n" ) ;
return - 1 ;
}
if ( listen ( sockListen, SOMAXCONN) == SOCKET_ERROR)
{
printf ( "listen error\n" ) ;
return - 1 ;
}
WSAEVENT ListenEvent = WSACreateEvent ( ) ;
WSAEventSelect ( sockListen, ListenEvent, FD_ACCEPT | FD_CLOSE) ;
CreateThread ( NULL , NULL , ( LPTHREAD_START_ROUTINE) WorkThreadProc, NULL , NULL , NULL ) ;
ArrSocket[ dwTotal] = sockListen;
ArrEvent[ dwTotal] = ListenEvent;
++ dwTotal;
system ( "pause" ) ;
if ( sockListen != INVALID_SOCKET)
{
closesocket ( sockListen) ;
}
WSACleanup ( ) ;
return 0 ;
}
重叠IO模型
EventSelect网络模型,它已经解决了等待数据到来的这一大部分时间,
但是它还有一小部分时间没有节省下来。
那就是把数据从网卡的缓冲区拷贝到我们应用程序的缓冲区里边。
而这一篇的重叠IO模型就是将这一小部分的时间也给节省了下来。
首先,我们在主线程里边初始化网络环境,然后创建监听的socket,
接下来,执行绑定,监听的操作,
然后,创建一个工作者线程来对客户进行服务。
执行以上操作之后呢,是一个死循环。
在这个循环里边,我们首先调用accept函数来对一个客户进行连接操作。
然后将该函数返回的客户端的socket保存到我们定义的一个全局socket数组里边进去。
然后对我们自定义的结构体单IO操作分配一个空间,其声明如下:
typedef struct
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
char szMessage[ MSGSIZE] ;
DWORD NumberOfBytesRecvd;
DWORD Flags;
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;
注意,该结构体是我们自己定义的,
其中第一个参数overlapped结构体是最重要的,必须把它放在第一个声明处。
因为我们要的是overlapped的地址,而其它的则是我们自己进行扩展的,
大家也可以进行自己的扩展。
那什么叫做单IO操作呢,比如说你请了一个人来专门打理你的店铺,
你每天呢都会发一个信息给这个人,
信息的内容就是叫他每天干一些指定的的事情,
而这个信息就是我们这个单IO操作这个数据结构所指定的内容。
我们在堆上给我们的单IO结构分配完空间之后呢,
我们来对它进行初始化,
我们把Buffer里面的指针指向szMessage,把里面的大小指定为szMessage的大小。
然后,调用WSACreateEvent创建一个事件对象,将这个事件对象赋予给overlappped里边的事件对象。
最后,在循环的末尾我们调用WSARecv,来对数据进行接收,其声明如下:
int WSARecv (
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
) ;
注意,该函数是异步的,也就是说它调用完就直接进行返回了,不用等待。
所以整个循环,只有刚开始接收到一个客户的连接之后,
我们就畅通无阻的往下执行一遍,然后再回到循环的开始继续等待客户的连接。
接下来,看下工作者线程。
工作者线程,也是一个死循环,它和我们的EventSelect一样,
刚开始也是调用了WSAWaitForMultipleEvents,来监控我们的socket数组哪一个有信号了,
由于这个函数最多只能监控64个socket,
所以我们的服务端只能同时进行64个客户的数据收发。
调用完该函数之后,它会返回一个索引值,
我们将调用WSAResetEvent,将我们全局event数组里边那个有信号的手动重置为无信号状态。
因为我们用WSACreateEvent创建的事件对象是以手动重置的方式创建的。
如果,不重置成无信号的状态,
那么就像上面我们举得那个例子一样,我们请的那个人,
他第二天查看信息的时候,还会继续的执行昨天的工作。
接下来,是我们重叠IO模型里边和EventSelect里边最大的不同点,
我们会调用WSAGetOverlappedResult,来判断重叠IO调用是否成功,其声明如下:
BOOL WSAGetOverlappedResult (
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer,
BOOL fWait,
LPDWORD lpdwFlags
) ;
这个函数的第三个参数和我们的WSARecv的第四个参数是一样的,操作系统会改写这个值,
若该值为0表示客户端断开连接或该数据传输失败了。
如果没有失败,我们就将数据保存到我们那个单IO结构里边的szMessage数组里边。
然后再次调用WSARecv,告诉操作系统继续帮我们监控这个socket。
实例代码:
# include <winsock2.h>
# include <stdio.h>
# define PORT 6000
# define MSGSIZE 1024
# pragma comment ( lib, "Ws2_32.lib" )
BOOL WinSockInit ( )
{
WSADATA data = { 0 } ;
if ( WSAStartup ( MAKEWORD ( 2 , 2 ) , & data) )
{
return FALSE;
}
if ( LOBYTE ( data. wVersion) != 2
|| HIBYTE ( data. wVersion) != 2 )
{
WSACleanup ( ) ;
return FALSE;
}
return TRUE;
}
typedef struct
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
char szMessage[ MSGSIZE] ;
DWORD NumberOfBytesRecvd;
DWORD Flags;
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;
int g_iTotalConn = 0 ;
SOCKET g_CliSocketArr[ MAXIMUM_WAIT_OBJECTS] ;
WSAEVENT g_CliEventArr[ MAXIMUM_WAIT_OBJECTS] ;
LPPER_IO_OPERATION_DATA g_pPerIODataArr[ MAXIMUM_WAIT_OBJECTS] ;
DWORD WINAPI WorkerThread ( LPVOID) ;
void Cleanup ( int ) ;
int main ( )
{
SOCKET sListen, sClient;
SOCKADDR_IN local, client;
DWORD dwThreadId;
int iaddrSize = sizeof ( SOCKADDR_IN) ;
WinSockInit ( ) ;
sListen = socket ( AF_INET, SOCK_STREAM, IPPROTO_TCP) ;
local. sin_addr. S_un. S_addr = htonl ( INADDR_ANY) ;
local. sin_family = AF_INET;
local. sin_port = htons ( PORT) ;
bind ( sListen, ( struct sockaddr * ) & local, sizeof ( SOCKADDR_IN) ) ;
listen ( sListen, 3 ) ;
CreateThread ( NULL , 0 , WorkerThread, NULL , 0 , & dwThreadId) ;
while ( TRUE)
{
sClient = accept ( sListen, ( struct sockaddr * ) & client, & iaddrSize) ;
printf (
"Accepted client:%s:%d\n" ,
inet_ntoa ( client. sin_addr) ,
ntohs ( client. sin_port) ) ;
g_CliSocketArr[ g_iTotalConn] = sClient;
g_pPerIODataArr[ g_iTotalConn]
= ( LPPER_IO_OPERATION_DATA) HeapAlloc (
GetProcessHeap ( ) ,
HEAP_ZERO_MEMORY,
sizeof ( PER_IO_OPERATION_DATA) ) ;
g_pPerIODataArr[ g_iTotalConn] -> Buffer. len = MSGSIZE;
g_pPerIODataArr[ g_iTotalConn] -> Buffer. buf =
g_pPerIODataArr[ g_iTotalConn] -> szMessage;
g_CliEventArr[ g_iTotalConn] =
g_pPerIODataArr[ g_iTotalConn] -> overlap. hEvent = WSACreateEvent ( ) ;
WSARecv (
g_CliSocketArr[ g_iTotalConn] ,
& g_pPerIODataArr[ g_iTotalConn] -> Buffer,
1 ,
& g_pPerIODataArr[ g_iTotalConn] -> NumberOfBytesRecvd,
& g_pPerIODataArr[ g_iTotalConn] -> Flags,
& g_pPerIODataArr[ g_iTotalConn] -> overlap,
NULL ) ;
g_iTotalConn++ ;
}
closesocket ( sListen) ;
WSACleanup ( ) ;
return 0 ;
}
DWORD WINAPI WorkerThread ( LPVOID lpParam)
{
int ret, index;
DWORD cbTransferred;
while ( TRUE)
{
ret = WSAWaitForMultipleEvents (
g_iTotalConn,
g_CliEventArr,
FALSE,
1000 ,
FALSE) ;
if ( ret == WSA_WAIT_FAILED || ret == WSA_WAIT_TIMEOUT)
{
continue ;
}
index = ret - WSA_WAIT_EVENT_0;
WSAResetEvent ( g_CliEventArr[ index] ) ;
WSAGetOverlappedResult (
g_CliSocketArr[ index] ,
& g_pPerIODataArr[ index] -> overlap,
& cbTransferred,
TRUE,
& g_pPerIODataArr[ g_iTotalConn] -> Flags) ;
if ( cbTransferred == 0 )
{
Cleanup ( index) ;
}
else
{
g_pPerIODataArr[ index] -> szMessage[ cbTransferred] = '\0' ;
send (
g_CliSocketArr[ index] ,
g_pPerIODataArr[ index] -> szMessage,
cbTransferred,
0 ) ;
WSARecv (
g_CliSocketArr[ index] ,
& g_pPerIODataArr[ index] -> Buffer,
1 ,
& g_pPerIODataArr[ index] -> NumberOfBytesRecvd,
& g_pPerIODataArr[ index] -> Flags,
& g_pPerIODataArr[ index] -> overlap,
NULL ) ;
}
}
return 0 ;
}
void Cleanup ( int index)
{
closesocket ( g_CliSocketArr[ index] ) ;
WSACloseEvent ( g_CliEventArr[ index] ) ;
HeapFree ( GetProcessHeap ( ) , 0 , g_pPerIODataArr[ index] ) ;
if ( index < g_iTotalConn - 1 )
{
g_CliSocketArr[ index] = g_CliSocketArr[ g_iTotalConn - 1 ] ;
g_CliEventArr[ index] = g_CliEventArr[ g_iTotalConn - 1 ] ;
g_pPerIODataArr[ index] = g_pPerIODataArr[ g_iTotalConn - 1 ] ;
}
g_pPerIODataArr[ -- g_iTotalConn] = NULL ;
}
完成端口模型
重叠IO模型,
它已经把我们的等待数据到来和拷贝数据到我们程序的缓冲区这个时间全部交给了操作系统去完成了,
它已经很完善了。
但是,如果我们想要把服务端的性能做的更好一点的话,它还是有点不足的,
比如说,重叠IO它只有一个工作者线程在工作,要想获得更高的效率,我们应该增加一些线程。
而且,重叠IO它只能同时进行64个客户端的数据收发,这也是一个很大的问题。
为此,就产生了完成端口模型,它就解决了这些问题。
可以说,在windows下做大型服务端的开发,完成端口模型是不二之选。
前面几种模型呢,我们都是在main函数里面直接写的,由于这个完成端口比较重要,
我们就给它稍微封装一下。
首先,新建一个完成端口的类,
在构造函数里面我们对它的一些数据成员进行初始化和对网络环境的初始化。
其次它有几个公有成员函数,第一个是Initialize初始化函数,来看下它的声明:
bool Initialize (
NOTIFYPROC pNotifyProc,
int nPort
) ;
第一个参数的的类型是我们自定义的一个函数指针,它的格式如下:
typedef void ( CALLBACK* NOTIFYPROC) ( LPVOID pContext, LPBYTE lpBuf, DWORD dwSize) ;
在Initialize这个函数里面我们做了哪些事情呢,
首先,我们调用WSASocket来建立一个监听的udp的socket,
然后进行绑定,因为是udp所以不用进行监听了。
接下来,我们调用CreateIoCompletionPort创建一个完成端口对象,其声明如下:
HANDLE CreateIoCompletionPort (
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
) ;
我们给这些参数赋值为无效值或空,这样我们就新建了一个完成端口了。
接下来,我们再次调用CreateIoCompletionPort函数,
这是我们给的参数与上面的不一样了,
第一个参数我们把监听的那个socket传进去,因为socket也是一个特殊的文件句柄。
第二个参数我们把刚创建的完成端口传进去。
第三个参数我们也把监听的socket传过去,
第四个参数我们一般把它赋值为CPU核心数的2倍左右的个数。
调用完该函数之后,我们就已经将一个socket与我们的完成端口进行了一个绑定。
与完成端口绑定好了之后,
我们就创建一些工作者线程,创建线程的数量与上面那个函数的第四个参数的个数就行了。
创建好线程之后我们会调用另外一个成员函数PostRecv函数,来投递一个接收请求。
至此,Initialize成员函数就到此为止了。
接下来看下工作者线程是如何工作的。
在工作者线程函数里,有一个循环,我们调用了GetQueuedCompletionStatus这么一个函数,
它和重叠IO模型l里面的WSAWaitForMultipleEvents函数的功能是相似的,
这个函数呢是用于监控一个完成端口,看这个完成端口上所投递的任务是否完成,
如果完成就返回,否则继续监控。
BOOL GetQueuedCompletionStatus (
HANDLE CompletionPort,
LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED * lpOverlapped,
DWORD dwMilliseconds
) ;
我们在最后一个参数传了一个INFINITE,表示无限的等待。
第三个参数overlapped结构我们也和重叠IO的单IO结构一样,对OVERLAPPED结构进行了扩展。
该类声明如下:
class OVERLAPPEDPLUS
{
public:
OVERLAPPED m_ol;
WSABUF DataBuf;
CHAR Buffer[ BUFSIZE] ;
unsigned long recvBytes;
SOCKADDR_IN remoteAddr;
int remoteAddrLen;
OVERLAPPEDPLUS ( )
{
ZeroMemory ( this, sizeof ( OVERLAPPEDPLUS) ) ;
}
} ;
当GetQueuedCompletionStatus成功返回的时候,
我们可以来调用成员函数OnRecv来处理接收到的数据。
至此,我们的工作者线程函数就完成了。
我们在Initialize函数最后投递了一个接收请求,我们来看下这个成员函数的实现。
在这个函数的开始处,
我们调用AllocateContext成员函数为OVERLAPPEDPLUS申请了一段内存,
然后,我们调用WSARecvFrom来接收数据,由于该函数是异步的,它会立即返回,
其声明如下:
int WSARecvFrom (
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
struct sockaddr FAR * lpFrom,
LPINT lpFromlen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
) ;
当这个函数成功返回的时候,
我们的工作者线程函数里面的GetQueuedCompletionStatus就会返回,
我们给GetQueuedCompletionStatus里面传了一个overlapped结构,
返回时它也会返回overlapped结构的地址,找到了地址,我们自然也知道了里面的一些信息了。
PostRecv完了之后,我们来看下OnRecv成员函数,
在这个函数里面我们定义的一个回调函数,
在里面我们来真正的处理接收到的数据,
然后调用成员函数MoveToFreePool来释放内存,
最后再调用PostRecv来继续投递一个接收请求。
我们的OnRecv就做这么多。
在PostRecv开始处我们为OVERLAPPED结构申请了一段内存,
调用AllocateContext来实现,
这个函数里,我们调用Interlocked系列来做了一个自旋锁,
因为有多个线程访问关键数据,我们必须做线程同步操作。
然后我们又做了一个最简单的内存池。
我们用两个链表来保存OVERLAPPED结构的指针,
一个是保存正在使用的,一个是空闲的,
当我们需要申请内存的时候,我们就查看空闲链表里面是否为空,
不为空我们就直接拿来用,否则,就申请。然后初始化一下OVERLAPPEDPLUS的成员。
在MoveToFreePool里面,我们也会做个自旋锁来进行线程同步,
然后遍历正在使用的链表,找到要释放的指针,将其移除链表,然后加到空闲链表里边去。
我们来看下资源释放的成员函数CloseCompletionPort,
我们在Initialize函数里创建了一些线程,
我们需要调用PostQueuedCompletionStatus这个函数将其释放,其声明如下:
BOOL PostQueuedCompletionStatus (
HANDLE CompletionPort,
DWORD dwNumberOfBytesTransferred,
ULONG_PTR dwCompletionKey,
LPOVERLAPPED lpOverlapped
) ;
我们将后三个参数全部给0,
特别是第三个,
我们会在工作者线程函数调用GetQueuedCompletionStatus之后,检查一下第三个参数是否为0,
若是则表示要关闭了,就退出该工作者线程,线程就自然结束了。
至于剩下的操作就是删除两个链表里面的指针了。
示例代码:
# ifndef AFX_IOCPSERVER_H__75B80E90_FD25_4FFB_B273_0090AA43BBDF__INCLUDED_
# define AFX_IOCPSERVER_H__75B80E90_FD25_4FFB_B273_0090AA43BBDF__INCLUDED_
# include <WINSOCK2.H>
# include <windows.h>
# include <algorithm>
# include <list>
using namespace std;
# pragma comment ( lib, "ws2_32.lib" )
# define BUFSIZE 512
class OVERLAPPEDPLUS
{
public:
OVERLAPPED m_ol;
WSABUF DataBuf;
CHAR Buffer[ BUFSIZE] ;
unsigned long recvBytes;
SOCKADDR_IN remoteAddr;
int remoteAddrLen;
OVERLAPPEDPLUS ( )
{
ZeroMemory ( this, sizeof ( OVERLAPPEDPLUS) ) ;
}
} ;
typedef void ( CALLBACK* NOTIFYPROC) ( LPVOID pContext, LPBYTE lpBuf, DWORD dwSize) ;
typedef list< OVERLAPPEDPLUS* > ContextList;
class CIOCPServer
{
public:
CIOCPServer ( ) ;
virtual ~ CIOCPServer ( ) ;
NOTIFYPROC m_pNotifyProc;
bool Initialize ( NOTIFYPROC pNotifyProc, int nPort) ;
static unsigned __stdcall WorkerThreadFunc ( LPVOID WorkContext) ;
bool PostSend ( OVERLAPPEDPLUS* pContext, LPBYTE lpData, UINT nSize) ;
bool PostRecv ( ) ;
volatile LONG g_fResourceInUse ;
void Shutdown ( ) ;
LONG m_nCurrentThreads;
LONG m_nBusyThreads;
ContextList m_listContexts;
ContextList m_listFreePool;
protected:
void MoveToFreePool ( OVERLAPPEDPLUS * pContext) ;
OVERLAPPEDPLUS* AllocateContext ( ) ;
bool m_bInit;
void CloseCompletionPort ( ) ;
void Stop ( ) ;
SOCKET m_socListen;
HANDLE m_hCompletionPort;
bool m_bTimeToKill;
string GetHostName ( SOCKET socket) ;
bool OnRecv ( OVERLAPPEDPLUS* pContext, DWORD dwSize = 0 ) ;
} ;
# endif
# include "IOCPServer.h"
CIOCPServer:: CIOCPServer ( )
{
printf ( "CIOCPServer=%p\n" , this) ;
WSADATA wsaData;
WSAStartup ( MAKEWORD ( 2 , 2 ) , & wsaData) ;
m_socListen = NULL ;
m_bTimeToKill = false;
m_hCompletionPort = NULL ;
m_bInit = false;
m_nCurrentThreads = 0 ;
m_nBusyThreads = 0 ;
m_nSendKbps = 0 ;
m_nRecvKbps = 0 ;
g_fResourceInUse = FALSE;
}
CIOCPServer:: ~ CIOCPServer ( )
{
printf ( "IOCPServer[%p] is shuting down\n" , this) ;
Shutdown ( ) ;
WSACleanup ( ) ;
}
bool CIOCPServer:: Initialize ( NOTIFYPROC pNotifyProc, int nPort)
{
m_pNotifyProc = pNotifyProc;
DWORD Flags = 0 ;
m_socListen = WSASocket (
AF_INET,
SOCK_DGRAM,
IPPROTO_IP,
NULL ,
0 ,
WSA_FLAG_OVERLAPPED) ;
if ( m_socListen == INVALID_SOCKET)
{
printf (
"Could not create listen socket %ld\n" ,
WSAGetLastError ( ) ) ;
return false;
}
SOCKADDR_IN saServer;
saServer. sin_port = htons ( nPort) ;
saServer. sin_family = AF_INET;
saServer. sin_addr. s_addr = INADDR_ANY;
nRet = bind (
m_socListen,
( LPSOCKADDR) & saServer,
sizeof ( struct sockaddr ) ) ;
if ( nRet == SOCKET_ERROR)
{
printf ( "bind() error %ld\n" , WSAGetLastError ( ) ) ;
closesocket ( m_socListen) ;
return false;
}
printf ( "CIOCPServer[%p] is binded on port %d now\n" , this, nPort) ;
m_hCompletionPort = CreateIoCompletionPort ( INVALID_HANDLE_VALUE, NULL , 0 , 0 ) ;
if ( m_hCompletionPort == NULL )
{
printf ( "CIOCPServer[%p] CreateIoCompletionPort failed\n" , this) ;
closesocket ( m_socListen ) ;
return false;
}
SYSTEM_INFO systemInfo;
GetSystemInfo ( & systemInfo ) ;
UINT nWorkerCnt = systemInfo. dwNumberOfProcessors * 2 ;
CreateIoCompletionPort (
( HANDLE) m_socListen,
m_hCompletionPort,
( DWORD) m_socListen,
nWorkerCnt) ;
HANDLE hWorker;
for ( UINT i = 0 ; i < nWorkerCnt; i++ )
{
UINT tmp= i+ 1 ;
hWorker= CreateThread (
NULL ,
0 ,
( LPTHREAD_START_ROUTINE) WorkerThreadFunc,
this,
0 ,
( LPDWORD) & tmp) ;
if ( hWorker == NULL )
{
CloseHandle ( m_hCompletionPort ) ;
return false;
}
PostRecv ( ) ;
m_nCurrentThreads++ ;
CloseHandle ( hWorker) ;
}
m_bInit = true;
return true;
}
unsigned CIOCPServer:: WorkerThreadFunc ( LPVOID thisContext)
{
CIOCPServer* pThis = reinterpret_cast< CIOCPServer* > ( thisContext) ;
HANDLE hCompletionPort = pThis-> m_hCompletionPort;
DWORD dwIoSize;
OVERLAPPEDPLUS* pOverlapPlus;
bool bError;
DWORD nSocket;
InterlockedIncrement ( & pThis-> m_nBusyThreads) ;
while ( true)
{
pOverlapPlus = NULL ;
bError = false;
InterlockedDecrement ( & pThis-> m_nBusyThreads) ;
BOOL bIORet = GetQueuedCompletionStatus (
hCompletionPort,
& dwIoSize,
( LPDWORD) & nSocket,
( LPOVERLAPPED * ) & pOverlapPlus,
INFINITE) ;
if ( nSocket == 0 )
{
break ;
}
int nBusyThreads = InterlockedIncrement ( & pThis-> m_nBusyThreads) ;
DWORD dwIOError = GetLastError ( ) ;
if ( dwIoSize == 0 || dwIOError == SOCKET_ERROR || bIORet == 0 )
{
pThis-> MoveToFreePool ( pOverlapPlus) ;
continue ;
}
pThis-> OnRecv ( pOverlapPlus, dwIoSize) ;
}
InterlockedDecrement ( & pThis-> m_nCurrentThreads) ;
InterlockedDecrement ( & pThis-> m_nBusyThreads) ;
return 0 ;
}
string CIOCPServer:: GetHostName ( SOCKET socket)
{
sockaddr_in sockAddr;
memset ( & sockAddr, 0 , sizeof ( sockAddr) ) ;
int nSockAddrLen = sizeof ( sockAddr) ;
BOOL bResult = getpeername ( socket, ( SOCKADDR* ) & sockAddr, & nSockAddrLen) ;
return bResult != INVALID_SOCKET ? inet_ntoa ( sockAddr. sin_addr) : "" ;
}
bool CIOCPServer:: PostRecv ( )
{
OVERLAPPEDPLUS * PerIoData = AllocateContext ( ) ;
if ( PerIoData == NULL )
{
printf ( "GlobalAlloc() failed with error %d\n" , GetLastError ( ) ) ;
return false;
}
DWORD RecvBytes= 0 ;
DWORD Flags= 0 ;
if ( WSARecvFrom (
m_socListen,
& ( PerIoData-> DataBuf) ,
1 ,
& RecvBytes,
& Flags,
( sockaddr* ) & ( PerIoData-> remoteAddr) ,
& PerIoData-> remoteAddrLen,
& ( PerIoData-> m_ol) , NULL ) == SOCKET_ERROR)
{
if ( WSAGetLastError ( ) != ERROR_IO_PENDING)
{
printf ( "WSARecvFrom() failed with error %d\n" , WSAGetLastError ( ) ) ;
return false;
}
}
return true;
}
bool CIOCPServer:: PostSend (
OVERLAPPEDPLUS* pContext,
LPBYTE lpData,
UINT nSize)
{
if ( pContext == NULL )
{
return false;
}
sendto (
m_socListen,
( char * ) lpData,
nSize,
0 ,
( sockaddr* ) & ( pContext-> remoteAddr) ,
sizeof ( pContext-> remoteAddr) ) ;
return true;
}
bool CIOCPServer:: OnRecv ( OVERLAPPEDPLUS* pContext, DWORD dwIoSize)
{
if ( dwIoSize == 0 )
{
return false;
}
printf (
"BusyThread: %d\t- %s\n" ,
m_nBusyThreads,
pContext-> Buffer) ;
m_pNotifyProc ( pContext, ( LPBYTE) pContext-> Buffer, dwIoSize) ;
MoveToFreePool ( pContext) ;
PostRecv ( ) ;
return true;
}
void CIOCPServer:: CloseCompletionPort ( )
{
while ( m_nCurrentThreads)
{
PostQueuedCompletionStatus ( m_hCompletionPort, 0 , ( DWORD) NULL , NULL ) ;
Sleep ( 100 ) ;
}
CloseHandle ( m_hCompletionPort) ;
OVERLAPPEDPLUS* pContext = NULL ;
list< OVERLAPPEDPLUS* > :: iterator iter;
for ( iter= m_listContexts. begin ( ) ; iter!= m_listContexts. end ( ) ; )
{
pContext= * iter++ ;
delete pContext;
}
m_listContexts. clear ( ) ;
for ( iter= m_listFreePool. begin ( ) ; iter!= m_listFreePool. end ( ) ; )
{
pContext= * iter++ ;
delete pContext;
}
m_listFreePool. clear ( ) ;
}
void CIOCPServer:: Shutdown ( )
{
if ( m_bInit == false)
{
return ;
}
m_bInit = false;
m_bTimeToKill = true;
CloseCompletionPort ( ) ;
closesocket ( m_socListen) ;
}
void CIOCPServer:: MoveToFreePool ( OVERLAPPEDPLUS * pContext)
{
while ( InterlockedExchange ( & g_fResourceInUse, TRUE) == TRUE)
{
Sleep ( 1 ) ;
}
ContextList:: iterator iter;
iter= find ( m_listContexts. begin ( ) , m_listContexts. end ( ) , pContext) ;
if ( iter!= m_listContexts. end ( ) )
{
m_listContexts. remove ( pContext) ;
m_listFreePool. push_back ( pContext) ;
}
InterlockedExchange ( & g_fResourceInUse, FALSE) ;
}
OVERLAPPEDPLUS* CIOCPServer:: AllocateContext ( )
{
OVERLAPPEDPLUS* pContext = NULL ;
while ( InterlockedExchange ( & g_fResourceInUse, TRUE) == TRUE) Sleep ( 1 ) ;
if ( ! m_listFreePool. empty ( ) )
{
pContext = m_listFreePool. front ( ) ;
m_listFreePool. remove ( pContext) ;
}
else
{
pContext = new OVERLAPPEDPLUS ( ) ;
}
ZeroMemory ( pContext, sizeof ( OVERLAPPEDPLUS) ) ;
pContext-> DataBuf. len = BUFSIZE;
pContext-> DataBuf. buf = pContext-> Buffer;
pContext-> recvBytes = 24 ;
pContext-> remoteAddrLen = sizeof ( pContext-> remoteAddr) ;
m_listContexts. push_back ( pContext) ;
InterlockedExchange ( & g_fResourceInUse, FALSE) ;
return pContext;
}
# include <conio.h>
# include "IOCPServer.h"
void CALLBACK cb ( LPVOID pContext, LPBYTE lpBuf, DWORD dwSize)
{
printf ( "%s\n" , lpBuf) ;
return ;
}
int main ( int argc, char * argv[ ] )
{
CIOCPServer * server= new CIOCPServer ( ) ;
server-> Initialize ( cb, 6000 ) ;
_getch ( ) ;
return 0 ;
}
参考
https://blog.csdn.net/Timmiy/article/details/52167022
https://blog.csdn.net/Timmiy/article/details/52201534