一、线程的概念
和多进程相比,多线程是一种比较节省资源的多任务操作方式。启动一个新的进程必须分配给它独立的地址空间,每个进程都有自己的堆栈段和数据段,系统开销比较高,进行数据的传递只能通过进行间通信的方式进行。在同一个进程中,可以运行多个线程,运行于同一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享全局变量和对象,启动一个线程所消耗的资源比启动一个进程所消耗的资源要少。
二、线程的使用
1、创建线程
在Linux下,采用pthread_create函数来创建一个新的线程,函数声明:
函数声明:
1
| int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
|
参数thread为为指向线程标识符的地址。
参数attr用于设置线程属性,一般为空,表示使用默认属性。
参数start_routine是线程运行函数的地址,填函数名就可以了。
参数arg是线程运行函数的参数。新创建的线程从start_routine函数的地址开始运行,该函数只有一个无类型指针参数arg。若要想向start_routine传递多个参数,可以将多个参数放在一个结构体中,然后把结构体的地址作为arg参数传入,但是要非常慎重,程序员一般不会这么做。
在编译时注意加上-lpthread参数,以调用静态链接库。因为pthread并非Linux系统的默认库。
2、线程的终止
如果进程中的任一线程调用了exit,则整个进程会终止,所以,在线程的start_routine函数中,不能采用exit。
线程的终止有三种方式:
1)线程的start_routine函数代码结束,自然消亡。
2)线程的start_routine函数调用pthread_exit结束。
3)被主进程或其它线程中止。
pthread_exit函数的声明如下:
1
| void pthread_exit(void *retval);
|
参数retval填空,即0。
3、多线程的socket服务端
我们把TCP通信的服务端(book250.cpp)程序改一下,由多进程改为多线程。
示例(book261.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
|
#include <stdio.h> #include <string.h> #include <unistd.h> #include <netdb.h> #include <stdlib.h> #include <signal.h> #include <pthread.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h>
class CTcpServer { public: int m_listenfd; int m_clientfd;
CTcpServer();
bool InitServer(int port);
bool Accept();
int Send(const void *buf,const int buflen); int Recv(void *buf,const int buflen);
~CTcpServer(); };
CTcpServer TcpServer;
void EXIT(int sig) { printf("程序退出,信号值=%d\n",sig);
TcpServer.~CTcpServer();
exit(0); }
void *pth_main(void *arg);
int main() { for (int ii=0;ii<50;ii++) signal(ii,SIG_IGN);
signal(SIGINT,EXIT); signal(SIGTERM,EXIT);
if (TcpServer.InitServer(5051)==false) { printf("服务端初始化失败,程序退出。\n"); return -1; }
while (1) { if (TcpServer.Accept() == false) continue;
pthread_t pthid; if (pthread_create(&pthid,NULL,pth_main,(void*)((long)TcpServer.m_clientfd))!=0) { printf("创建线程失败,程序退出。n"); return -1; }
printf("与客户端通信的线程已创建。\n"); } }
CTcpServer::CTcpServer() { m_listenfd=m_clientfd=0; }
CTcpServer::~CTcpServer() { if (m_listenfd!=0) close(m_listenfd); if (m_clientfd!=0) close(m_clientfd); }
bool CTcpServer::InitServer(int port) { if (m_listenfd!=0) { close(m_listenfd); m_listenfd=0; }
m_listenfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in servaddr; memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(port); if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 ) { close(m_listenfd); m_listenfd=0; return false; }
if (listen(m_listenfd,5) != 0 ) { close(m_listenfd); m_listenfd=0; return false; }
return true; }
bool CTcpServer::Accept() { if ( (m_clientfd=accept(m_listenfd,0,0)) <= 0) return false;
return true; }
int CTcpServer::Send(const void *buf,const int buflen) { return send(m_clientfd,buf,buflen,0); }
int CTcpServer::Recv(void *buf,const int buflen) { return recv(m_clientfd,buf,buflen,0); }
void *pth_main(void *arg) { int clientfd=(long) arg;
char strbuffer[1024];
while (1) { memset(strbuffer,0,sizeof(strbuffer)); if (recv(clientfd,strbuffer,sizeof(strbuffer),0)<=0) break; printf("接收:%s\n",strbuffer);
strcpy(strbuffer,"ok"); if (send(clientfd,strbuffer,strlen(strbuffer),0)<=0) break; printf("发送:%s\n",strbuffer); }
printf("客户端已断开连接。\n");
close(clientfd);
pthread_exit(0); }
|
需要注意几个问题:
1)线程主函数的函数体中,不能使用return;
语句,如果想退出线程,可以用pthread_exit(0);
返回。
2)线程可以共享全局变量,当然也可以共享TcpServer的m_clientfd成员变量,但是,创建线程的时候,为什么要把客户端的socket用参数传给线程主函数,而不是直接获取TcpServer.m_clientfd的值,因为主进程调用pthread_create创建线程后,立即返回循环重新Accept,创建线程需要时间,如果在这段时间内有新的客户端连接上来,TcpServer.m_clientfd的值会发生改变。
3)TcpServer.m_clientfd的强制转换,在创建线程的时候,代码如下:
1
| if (pthread_create(&pthid,NULL,pth_main,(void*)((long)TcpServer.m_clientfd))!=0)
|
线程中的代码如下:
1
| int clientfd=(long) arg;
|
这种数据类型的转换方法可能会让初学者不理解,在学习指针的时候说过,指针是用来存放变量的地址,不能把整数赋给指针,那现在这是怎么回事?这么说吧,C语言很灵活,数据类型可以强制转换,怎么转过去就怎么转回来。举个例子:水桶是用来装水的,特殊情况下用水桶来装板砖其实也可以,但是,板砖放入水桶的方法和从水桶中取出板砖的方法与水不同,怎么放进去就怎么取出来。
4)book261.cpp程序有一个漏洞,没有保存客户端的socket,主程序退出时,没有关闭客户端的socket,资源没有释放,这么说您可能难以理解,没有关系,等您真的需要编写多线程的socket服务端程序的时候就明白了。
三、线程资源的回收
线程有joinable和unjoinable两种状态,如果线程是joinable状态,当线程主函数终止时(自己退出或调用pthread_exit退出)不会释放线程所占用内存资源和其它资源,这种线程被称为“僵尸线程”。创建线程时默认是非分离的,或者称为可连接的(joinable)。
避免僵尸线程就是如何正确的回收线程资源,有四种方法:
1)方法一:创建线程前,调用pthread_attr_setdetachstate将线程设为detached,这样线程退出时,系统自动回收线程资源。
1 2 3 4
| pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); pthread_create(&pthid,&attr,pth_main,(void*)((long)TcpServer.m_clientfd);
|
2)方法二:创建线程后,在创建线程的程序中调用pthread_detach将新创建的线程设置为detached状态。
3)方法3:创建线程后,在创建线程的程序中调用pthread_join等待线程退出,一般不会采用这种方法,因为pthread_join会发生阻塞。
1
| pthread_join(pthid,NULL);
|
4)方法4:在线程主函数中调用pthread_detach改变自己的状态。
1
| pthread_detach(pthread_self());
|
四、查看线程
1)在top命令中,如果加上-H参数,top中的每一行显示的不是进程,而是一个线程。
2)在ps命令中加-xH参数也可以显示线程,加grep可以过滤内容。
1 2
| ps -xH ps -xH|grep book261
|
五、应用经验
Linux没有真正意义上的线程,它的实现是由进程来模拟,属于用户级线程。所以,在Linux系统下,进程与线程在性能和资源消耗方面没有本质的差别。
对我们程序员来说,进程不能共享全局数据,线程可以共享全局数据,各位可以根据应用场景选择采用多进程或多线程。
六、版权声明
C语言技术网原创文章,转载请说明文章的来源、作者和原文的链接。
来源:C语言技术网(www.freecplus.net)
作者:码农有道