线程
编译多线程程序时,要注意参数
g++ -g 源文件.cpp -o 目标文件 -lpthread
[root@localhost coding]# g++ -g test.cpp -o test -lpthread
或者
g++ -g -o 目标文件 源文件.cpp -lpthread
[root@localhost coding]# g++ -g -o test test.cpp -lpthread
关于线程的注意事项(经常忘)
1、虽然能按顺序创建线程,但实际上的线程运行顺序是未知的。
2、主线程一退出,所有子线程都会退出。主线程要给子线程的运行留下时间。
void* pth_foo(void* arg)
{
int i = (long)arg;
printf("thread value : %d \n", i);
}
int main()
{
pthread_t pid[5];
for(long i = 0; i < 5; ++i)
{
if(pthread_create(&pid[i], NULL, pth_foo, (void*)(i + 1)) != 0)
return -1;
}
}
因为主线程创建完子线程后就立即退出,并导致程序结束,使得子线程完全没来得及执行。
解决方法一:
让主线程休眠一段时间。
sleep(1);
解决方法二:
主线程调用pthread_exit
函数退出,等到所有线程都执行完毕后,进程才结束。
pthread_exit(NULL);
[root@localhost coding]# g++ -g test.cpp -o test -lpthread
[root@localhost coding]# ./test
thread value : 2
thread value : 3
thread value : 4
thread value : 5
thread value : 1
在多线程中如果要输出信息,最好还是用printf,如果使用cout,会出现多个线程的输出混杂在一起的情况。
cout是个全局变量,多线程在使用共享资源时没有上锁,所以导致输出混合的情况吧。
创建线程 pthread_create()
#include <pthread.h>
int pthread_create(pthread_t*thread, const pthread_attr_t* attr, void*(*start_routine)(void*), void* arg);
thread
参数是新线程的标识符,为一个整型。
attr
参数用于设置新线程的属性。给传递NULL表示设置为默认线程属性。
start_routine
和arg
参数分别指定新线程将运行的函数和参数。start_routine返回时,这个线程就退出了
返回值:成功返回0,失败返回错误号。
获取当前线程id pthread_self()
线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型有不同的实现,调用pthread_self()
可以获得当前线程的id。
终止线程 pthread_exit() pthread_cancel()
终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程调用
pthread_exit()
终止自己。 - 线程调用
pthread_cancel()
终止同一进程中的另一个线程。
线程返回 pthread_exit()
#include <pthread.h>
void pthread_exit(void *status);
status
:可以指向一个变量或数据结构,用于在线程结束时向外传递数据,由用户决定。
其它线程可以调用pthread_join()
获得这个指针。
注意!status
指针不能指向线程的局部存储对象,因为在线程终止时,所有局部存储对象都会随之销毁。
例子:
#include <pthread.h>
#include <errno.h>
int val = 10;
void* pth_foo(void* arg)
{
int i = 10;
pthread_exit((void*)&val);
// int *i = new int(15);
// pthread_exit((void*)i);
}
int main()
{
pthread_t tid;
int i = 0;
if(pthread_create(&tid, NULL, pth_foo, NULL) != 0)
{
printf("线程创建失败。(%d:%s) \n", errno, strerror(errno));
return -1;
}
// 线程函数传出来的是个指针,所以也用指针接收
int *pi;
// 如果线程函数传出来的是值,只要修改pi的类型就好了,下方代码不用修改
// &pi为(void**),pi为(void*),刚好对应“&val”——线程exit传出的值~
pthread_join(tid, (void**)&pi);
// ~所以*pi就能得到全局变量val的数据
printf("%d \n", *pi);
}
-----------------------------------------
[root@localhost coding]# ./test
10
/* 如果
void* pth_foo(void* arg)
{
int i = 10;
pthread_exit((void*)&i);
}
则输出的结果是0
*/
补充:在本例中,pthread_exit
用return
替代也能正常运行。
二者区别(网络):return会回到调用者,pthread_exit会终止线程。
再补充:return可用于所有函数,而pthread_exit专门用于结束线程。
且return不会自动调用线程清理函数。
线程取消 pthread_cancel()
#include <pthread.h>
int pthread_cancel(pthread_t thread);
thread
:线程的标识符
返回值:成功返回0,失败返回错误码。
!补充
1、pthread_cancel
后,thread
内容不变,至少用printf("%x\n", thread);
输出的结果在取消线程前后都是一致的。
2、对一个已经取消的线程再次调用pthread_cancel
,返回3。(对应的错误码是3(No such process)
)
3、线程对pthread_cancel
的默认响应状态是PTHREAD_CANCEL_DEFERRED
,线程运行到取消点后才退出。
pthread_setcancelstate
(是否响应)
子线程可以通过调用pthread_setcancelstate
,设置对pthread_cancel
请求的响应方式。
int pthread_setcancelstate(int state, int *oldstate);
// 将线程的响应方式设置为state,通过oldstate返回旧状态。
PTHREAD_CANCEL_ENABLE
:响应取消。
PTHREAD_CANCEL_DISABLE
:不响应。
pthread_setcanceltype
(如何响应)
int pthread_setcanceltype(int type, int *oldtype);
pthread_setcanceltype
设置线程的取消方式。
PTHREAD_CANCEL_ASYNCHRONOUS
:异步取消,立即将线程取消;
PTHREAD_CANCEL_DEFERRED
:推迟取消,线程运行到**取消点(一些特定函数)**后才取消。(取消点:APUE P.363)
pthread_testcancel
调用这个函数时,如果有某个取消请求处于挂起状态,且取消没有设置为无效,那么线程就会被取消。
线程等待 pthread_join()
#include <pthread.h>
int pthread_join(pthread_t thread, void **status);
thread
:调用该函数的线程将挂起等待,直到id为thread
的线程终止。
status
:
1、等待的线程通过return返回
status所指向的地址存放的是线程函数的返回值。
2、等待的线程被其它线程调用函数pthread_cancel
停止
status所指向的地址存放的是常数PTHREAD_CANCELED
。
3、等待的线程自己调用pthread_exit
终止
可通过status获取线程终止时保存的数据。如果不需要改数据,传入NULL。
线程函数退出后。exit时传入参数保存了数据,就能通过status获取这个数据。
如果等待的线程调用pthread_exit
时传入参数保存了数据,就能通过status
获取这个数据。
如果有多个线程调用pthread_join
获取同一个线程的执行结果,则只有一个线程能得到结果,其余线程都将执行失败。
(3、)的例子:
...
// 已知线程函数会用pthread_exit保存一个int类型变量
int val;
// 传入变量val的地址
pthread_join(tid, (void**)&val);
// 函数返回后,变量val指向得到pthread_exit保存的数据
printf("%d \n", val);
返回值:成功返回0,失败返回错误码(可以当做int用)。可能出现的错误码:
错误码 | 描述 |
EDEADLK | 可能引起死锁,比如2个线程互相针对对方调用 |
EINVAL | 目标线程是不可回收的(分离状态),或已有其它线程在回收该目标线程 |
ESRCH | 目标线程不存在 |
线程的结合、分离概念(线程资源回收)
在任何一个时间点上,线程是可结合的(joinable)或者是分离的(detached)。
一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)不释放。(默认情况下线程的创建都是可结合的)
一个分离的线程是不能被其他线程回收或杀死,它的存储器资源在它终止时由系统自动释放。
如果一个可结合线程结束运行但没有被join,会导致部分资源没有被回收,所以创建线程者应该调用pthread_join
来等待线程运行结束,并得到线程的退出代码,回收其资源。
在调用pthread_join
后,如果该线程没有运行结束,调用者会被阻塞。如何解决这种情况?
答:将等待的线程设置为分离状态。也就不需要再调用pthread_join
等待该线程。
分离线程 pthread_detach()
#include <pthread.h>
int pthread_detach(pthread_t thread);
thread
:线程id
返回值:函数成功,返回0;失败,返回错误码。
能在主线程中调用,或子线程中调用都可以。重要的是要传入正确的线程id。
线程清理 pthread_cleanup_push/pop
线程可以安排它退出时需要调用的函数。退出函数可以建立多个,记录在栈中。
void pthread_cleanup_push(void (*routine)(void *), void *arg);
routine
:函数名
arg
:参数
void pthread_cleanup_pop(int execute);
execute
:传入非0参数,弹出并执行;传入0,只弹出,不执行。
线程函数调用pthread_exit
时,会按出栈的顺序执行所有清理函数。使用return
退出的线程函数不会执行清理函数。
锁 !
http://c.biancheng.net/thread/vip_8615.html
大概描述:互斥锁、信号量、条件变量、读写锁(还有自旋锁、屏障等)
- 互斥锁:只允许一个线程进入临界区;
- 信号量:允许n个线程进入临界区;
- 条件变量:当某线程满足特定条件后进入临界区,
通常(几乎是必须)和互斥锁配合使用; - 读写锁:大多数线程能读临界区,少数线程能写临界区。允许同时有多个读者进入,只允许1个作者进入;有读者时不能有作者,有作者时不能有读者。
补充:条件变量所谓的
“满足特定条件后进入临界区”
是由代码结构实现的,即它自身并没有提供实现该功能的函数。大概的代码结构:一个线程到进入区调用
pthread_cond_wait
等待,另一个线程达到满足的条件后调用pthread_cond_signal
解锁。所谓的读写锁其实就是提供了2个上锁的方式而已,具体的读和写动作还是得由用户自觉操作。
线程同步(锁)
互斥变量为pthread_mutex_t
类型。初始化时可以设置为常量PTHREAD_MUTEX_INITIALIZER
(只适用于静态分配的互斥量)或用pthread_mutex_init()
初始化。
书上的例子
**1、**一个被互斥锁(作为成员变量)保护的结构foo
,P.322:
#include <stdlib.h>
#include <pthread.h>
struct foo
{
int f_count;
pthread_mutex_t f_lock;
int f_id;
}
foo* foo_alloc(int id)
{
foo *fp;
if(fp = (foo*)malloc(sizeof(foo)) != NULL)
{
fp->f_count = 1;
fp->id = id;
if(pthread_mutex_init(&fp->f_lock, NULL) != 0)
{
free(fp);
return 0;
}
}
// 继续初始化操作
}
void foo_hold(foo* fp)
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
void foo_rele(foo* fp)
{
pthread_mutex_lock(&fp->f_lock);
if(--fp->f_count == 0)
{
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
}
else
pthread_mutex_unlock(&fp->f_lock);
}
**2、**在程序中使用多个foo对象,多个foo对象用哈希表组织,增加一个互斥锁2用于保护哈希表。P.323。
**3、**访问foo结构的成员变量f_cout
也使用互斥锁2进行保护。P.325。两种用途使用相同的锁(增加了锁的粒度),简化了代码结构。
线程与信号
外界信号不会中断子线程的运行(除非是终止进程的信号)。
在多线程程序中,捕获信号的函数放在哪都一样,通常放在主函数中。
多线程程序中,在任一线程中调用signal
或sigaction
都会改变所有信号的信号处理函数。
主线程向子线程发送信号用pthread_kill
函数。
多线程服务器的退出
退出信号(2 和 15)处理函数的流程:
1、关闭监听socket;
2、用pthread_cancel
终止所有子线程;
3、释放资源(IO、文件、内存等);
4、子线程执行清理函数(在里面关闭通信socket);
多线程服务器中,子线程通常都是分离状态的,且通常都是立即取消状态。
I/O复用
《APUE》中的标题是“I/O多路转接”
需求背景
1、一些函数,如read、accept在被调用时,会阻塞调用函数的线程。
read(文件描述符, buf, bufsize);
accept(套接字描述符, socketaddr, socketlen);
2、可用的描述符数量可能会小于需求数量,例如要打开很多个文件、要响应很多个客户端的连接等。
使用I/O多路转接技术,先构造一张感兴趣的描述符列表,然后调用一个函数,直到这些描述符中的一个已经准备好响应操作后,函数返回。
select
#include <sys/select.h>
int select(int maxfdp1, fd_set* restrict readfds, fd_set* restrict writefds, fd_set* restruct execptfds, struct timeval* restruct tvptr);
返回值:准备就绪的描述符数目;超时——0;出错——-1。
从后向前介绍参数
tvptr
:等待时间,timeval
结构,2个成员:time_t tv_sec
、long tv_nsec
分别表示s
和ns
。
struct timeval {
time_t tv_sec; //秒
suseconds_t tv_usec; //微秒 1ms = 10^(-6)s
};
tvptr == NULL
:永久等待,直到一个描述符已经准备好,或捕捉到一个信号。捕捉到信号会使函数返回-1,errno设置为EINTR
tvptr->tv_sec == 0 && tvptr->tv_use c== 0
:不等待,立即测试所有指定的描述符并返回。
tvptr->tv_sec != 0 && tvptr->tv_usec != 0
:等待指定的时间。当指定的任一描述符准备好,或超时时返回。
readfds
、writefds
、execptfds
:指向描述符集的指针。描述符集为fd_set
类型,基本上可认为是一个很大的字节数组。
fd_set相关的操作函数
#include <sys/select.h>
int FD_ISSET(int fd, fd_set *fdset);
返回值:如果fd在字符集中,返回非0值;否则返回0。
void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);
从视频作者给出的例子看,select关心的描述符集中有描述符准备好了就会返回,然后下方代码就要用
for(i=0; i<maxfdp; ++i ) { ... FD_ISSET(i, 字符集); ... }
的方式对范围内的所有描述符进行测试。如果被打开的描述符是想要的,就执行相关操作。
**注意!**所谓的
“被打开的描述符”
是在调用select
函数前,由程序员自己通过调用FD_SET
函数来设置的。
maxfdp1
:最大描述符编号值+1。即指定描述符集的右区间。
[0, maxfdp1)
3个描述符集
readfds:读,常用。
writefds:写,只有在输出缓冲区满时才会被阻塞,很少会遇到。
execptfds:在网络编程中用不到。
pselect
添加了信号屏蔽参数,但很少用到,因为有其他的方式屏蔽信号。
select水平触发
如果一个标识符的事件没有被处理完,select会再次报告该事件。(例如没有将客户端传送到的数据一次读完)
select 缺点
1、默认支持的描述符数量只有1024,可以修改,但数量越多,效率越低。
2、每次确认描述符都要遍历select。
代码(已运行,能响应多个客户端)
#include "CTcpServer.h"
#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>
#include <sys/select.h>
CTcpServer g_TcpServer;
// 处理SIGINT和SIGTERM信号
void EXIT(int sig)
{
printf("程序退出,信号值=%d \n", sig);
close(g_TcpServer.m_listenfd);
exit(0);
}
int main()
{
if (g_TcpServer.InitServer(5000) == false)
{
printf("服务端初始化失败,程序退出。\n");
return -1;
}
fd_set readfdset;
int maxfd;
int listensock = g_TcpServer.m_listenfd;
FD_ZERO(&readfdset);
FD_SET(listensock, &readfdset);
maxfd = listensock;
while (1)
{
fd_set tmpfdset = readfdset;
int infds = select(maxfd + 1, &tmpfdset, NULL, NULL, NULL);
if (infds < 0)
{
printf("select() failed. \n");
perror("select()");
break;
}
if (infds = 0)
{
printf("select() timeout. \n");
continue;
}
for (int eventfd = 0; eventfd <= maxfd; ++eventfd)
{
if (FD_ISSET(eventfd, &tmpfdset) <= 0)
continue;
if (eventfd == listensock)
{
sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock, (sockaddr*)&client, &len);
if (clientsock < 0)
{
printf("accept() failed\n");
continue;
}
printf("client(socket = %d) connect success.\n", clientsock);
FD_SET(clientsock, &readfdset);
if (maxfd < clientsock)
maxfd = clientsock;
continue;
}
else
{
char strbuffer[1024];
memset(strbuffer, 0, sizeof(strbuffer));
ssize_t isize = read(eventfd, strbuffer, sizeof(strbuffer));
if (isize <= 0)
{
printf("client(evenfd = %d) disconnected. \n", eventfd);
close(eventfd);
FD_CLR(eventfd, &readfdset);
if (eventfd == maxfd)
{
for (int i = maxfd; i > 0; --i)
if (FD_ISSET(i, &readfdset))
{
maxfd = i;
break;
}
printf("maxfd = %d, update. \n", maxfd);
}
continue;
}
printf("recv(eventfd = %d, size = %d):%s \n)", eventfd, isize, strbuffer);
write(eventfd, strbuffer, strlen(strbuffer));
}
}
}
return 0;
}
问题(似乎已解决)
作者代码中调用select
时,要先创建一个fd_set类型
的临时变量tmpfdset
,让该临时变量参与select
函数,在遍历查找有事件响应的标识符时,使用的也是临时变量tmpfdset
。
fd_set tmpfdset = readfdset;
int infds = select(maxfd+1, &tmpfdset, NULL, NULL, NULL);
...
for(int eventfd = 0; eventfd <= maxfd; ++eventfd)
{
if(FD_ISSET(eventfd, &tmpfdset) <= 0)
continue;
if(eventfd == listensock)
...
}
经过自己的粗略检查(在select前后检查tmpfdset
标识符4的值)得出的结论:
似乎select
在返回时,会将有事件的描述符以外的所有描述符都清零,所以使用for循环
从0开始遍历一定且只会遇到发生了事件的那个描述符。
尝试过直接将
readfdset
传入select
,结果服务端只能响应第一个连入的客户端。
验证代码:
#define CHECK(x) printf("readfdset[%d] is %d \n", x, FD_ISSET(x, &tmpfdset))
...
fd_set tmpfdset = readfdset;
CHECK(4); // 经过几次实践,已知描述符4一定对应第一个连接的客户端socket
int infds = select(maxfd+1, &tmpfdset, NULL, NULL, NULL);
...
for(int eventfd = 0; eventfd <= maxfd; ++eventfd)
{
if(FD_ISSET(eventfd, &tmpfdset) <= 0)
continue;
CHECK(4);
if(eventfd == listensock)
...
}
结果:第1个客户端的对应描述符是4
...
-----------------------
readfdset[4] is 1 // 调用select之前
readfdset[4] is 0 // 调用select之后
recv(eventfd = 5, size = 3): 345
-----------------------
readfdset[4] is 1
readfdset[4] is 1
recv(eventfd = 4, size = 3): qwe
如果不是第1个客户端的事件导致select返回,则select返回后,对应的FD_ISSET(客户端1, tmpfdset)
会返回0。
poll
#include <poll.h>
int poll(struct pollfd fdarray[]. nfds_t nfds, int timeout);
返回值:成功——准备就绪的描述符数目;超时——返回0;出错——返回-1
nfds
:就是select的maxfdp1
。
struct pollfd
{
inf fd; // 文件标识符。
short events; // 期待标识符上发生的事件
short revents; // 标识符发生事件后的返回值
}
fd
:若设置为-1,则表示忽略events
,且revents
返回0。
events
:若设置为0,则忽略所有fd
发生的事件,且revents
返回0。输入的参数似乎可以用“|”
连接。
revents
:是个输出参数,值由内核填充,表示发生的事件。
例子中的代码结构与select基本一致。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/fcntl.h>
// ulimit -n
#define MAXNFDS 1024
// 初始化服务端的监听端口。
int initserver(int port);
int main(int argc,char *argv[])
{
...
// 初始化服务端用于监听的socket。
int listensock = initserver(atoi(argv[1]));
printf("listensock=%d\n",listensock);
...
int maxfd; // fds数组中需要监视的socket的大小。
struct pollfd fds[MAXNFDS]; // fds存放需要监视的socket。
// ------------- 等价于FD_ZERO() -------------
for (int ii=0;ii<MAXNFDS;ii++) fds[ii].fd=-1; // 初始化数组,把全部的fd设置为-1。
// 把listensock添加到数组中。
fds[listensock].fd=listensock;
fds[listensock].events=POLLIN; // 有数据可读事件,包括新客户端的连接、客户端socket有数据可读和客户端socket断开三种情况。
maxfd=listensock;
while (1)
{
// ------------- 阻塞 -------------
int infds = poll(fds, maxfd+1, 5000);
// 返回失败。
if (infds < 0)
{
printf("poll() failed.\n"); perror("poll():"); break;
}
// 超时。
if (infds == 0)
{
printf("poll() timeout.\n"); continue;
}
// 检查有事情发生的socket,包括监听和客户端连接的socket。
// 这里是客户端的socket事件,每次都要遍历整个集合,因为可能有多个socket有事件。
for (int eventfd=0; eventfd <= maxfd; eventfd++)
{
if (fds[eventfd].fd<0) continue;
// ------------- 与select略有不同,这里先检查“.revents”事件类型 -------------
if ((fds[eventfd].revents&POLLIN)==0) continue;
// ------------- 未知 -------------
fds[eventfd].revents=0; // 先把revents清空。
// ------------- “.f”非零,且“.revents&POLLIN”非零,再检查匹配标识符 -------------
if (eventfd==listensock)
{
// ------------- 下文内容与select基本相同 -------------
// 如果发生事件的是listensock,表示有新的客户端连上来。
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if (clientsock < 0)
{
printf("accept() failed.\n"); continue;
}
printf ("client(socket=%d) connected ok.\n",clientsock);
if (clientsock>MAXNFDS)
{
printf("clientsock(%d)>MAXNFDS(%d)\n",clientsock,MAXNFDS); close(clientsock); continue;
}
// ------------- poll登记新标识符 -------------
fds[clientsock].fd=clientsock;
fds[clientsock].events=POLLIN;
fds[clientsock].revents=0;
if (maxfd < clientsock) maxfd = clientsock;
printf("maxfd=%d\n",maxfd);
continue;
}
else
{
// 客户端有数据过来或客户端的socket连接被断开。
char buffer[1024];
memset(buffer,0,sizeof(buffer));
// 读取客户端的数据。
ssize_t isize=read(eventfd,buffer,sizeof(buffer));
// 发生了错误或socket被对方关闭。
if (isize <=0)
{
printf("client(eventfd=%d) disconnected.\n",eventfd);
close(eventfd); // 关闭客户端的socket。
fds[eventfd].fd=-1;
// 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
if (eventfd == maxfd)
{
for (int ii=maxfd;ii>0;ii--)
{
if ( fds[ii].fd != -1)
{
maxfd = ii; break;
}
}
printf("maxfd=%d\n",maxfd);
}
continue;
}
printf("recv(eventfd=%d,size=%d):%s\n",eventfd,isize,buffer);
// 把收到的报文发回给客户端。
write(eventfd,buffer,strlen(buffer));
}
}
}
return 0;
}
epoll
相关数据结构
typedef union epoll_data {
void *ptr;
int fd; // 目前唯一关注的成员
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events的取值
EPOLLIN: 关联的文件已经对read操作可用。(常用)
EPOLLPUT: 关联的文件已经对write操作可用。(常用)
EPOLLRDHUP: 对应的socket连接的已经关闭。(常用)
EPOLLPRI: 紧急消息对read操作可用。(~)
EPOLLERR: 对应的文件发生错误,默认等待,不需要再手动设置。(-)
EPOLLHUP: 对应的文件挂起,默认等待,不需要再手动设置。(-)
EPOLLET: 设置为模式边缘触发模式。(默认是水平触发模式)
EPOLLONESHOT: “Sets the one-shot behavior for the associated file descriptor.”
代码结构
// 1、需要1个int类型接收epoll_create函数的返回值(标识符,表示eopll实例),epoll_create的参数大于0即可,没什么意义
int epollfd = epoll_create(1);
// 2、创建一个epoll_event结构并初始化,设定标识符和监听的事件
struct epoll_event ev;
ev.data.fd = listensock;
ev.events = EPOLLIN;
// 3、使用epoll_ctl函数进行设置
epoll_ctl(epollfd, EPOLL_CTL_ADD, listensock, &ev);
// 4、while。添加删除socket标识符的方式与2、3流程一致
while (1)
{
// MAXEVENTS的值由实际需求决定
struct epoll_event events[MAXEVENTS];
// -------------------------------
int infds = epoll_wait(epollfd, events, MAXEVENTS, -1);
if (infds < 0)...
if (infds == 0)...
// 查找的范围由epoll_wait的返回值决定
for (int i=0; i<infds; i++)
{
if ((events[i].data.fd == listensock) &&(events[i].events & EPOLLIN))
{
...响应客户端连接...
int clientsock = accept(li...;
...
// -------------------------------
// 复用全局变量ev,设置好参数后用epoll_ctl添加
// 把新的客户端添加到epoll中。
memset(&ev,0,sizeof(struct epoll_event));
ev.data.fd = clientsock;
ev.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, clientsock, &ev);
}
else if (events[i].events & EPOLLIN)
{
...与客户端通信...
if (isize <=0) // 发生错误或连接被断开
{
// 把已断开的客户端从epoll中删除。
// -------------------------------
// 执行流程与上文的添加基本一致,就只是修改了epoll_ctl的1个参数而已
memset(&ev, 0, sizeof(struct epoll_event));
ev.data.fd = events[i].data.fd;
ev.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, &ev);
close(events[i].data.fd);
continue;
}
}
}
}
// 5、关闭epoll
close(epollfd);
水平触发和边缘触发
epoll默认使用水平触发。
水平触发:报告了fd
后事件没被处理或数据没有被全部读取,epoll
会立即再报告该fd
。
边缘触发:报告了fd
后事件没被处理或数据没有被全部读取,epoll
会下次再报告该fd
。
----------------------------
pthread_equal
int pthread_equal(pthread_t tid1, pthread_t tid2);
返回值:相等——非0值,不相等——0。
pthread_t
类型是采用数据类型来实现的,所以不能作为整数处理(==
),得用pthread_equal
来进行比较。
一个打印pthread_t类型变量的方法
// “%lu” 和 “%lx”
pthread_t tid = pthread_self();
printf("tid: %lu(0x%lx) \n", (unsigned long)tid, (unsigned long)tid);
// 输出
tid: 140048119990016(0x7f5f7e718700)
pthread_self
pthread_t pthread_self(void);
返回值:调用函数的线程ID
通常与pthread_equal
一起使用。
pthread_create
int pthread_create(pthread_t* restrict tidp, constpthread_attr_t* restrict attr, void* (*start_rtn)(void*), void* restrict arg);
返回值:成功——0;失败——错误编号。
restrict:C语言中的一种类型限定符(Type Qualifiers),用于告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。
创建后的线程ID存入tidp
所指的地址;
attr
:属性;
start_rtn
:函数指针
arg
:函数参数
线程能按顺序创建,但不一定会按顺序执行。
函数调用失败时会返回错误码,不会设置errno。
pthread_exit
void pthread_exit(void* rval_ptr);
在不终止整个进程的情况下,停止线程。
rval_ptr
指针指向线程要返回的数据。其它线程可以通过pthread_join
访问到这个指针。(如果要返回的数据大小<=sizeof(void*)
,则可以通过强制转换直接返回数据。)
**注意:**如返回的是个指针,必须确保在线程结束后,指针所指内存数据仍有效。如果返回的是值,则不需要担心。
void* func_one(void* ptr)
{
int val = 10;
pthread_exit((void*)(long)val);
// pthread_exit((void*)(long)20);
// 两种方法都能有效将返回值传递出去
}
SIGSEGV
如果上方代码是:
void* func_one(void* ptr)
{
int* val = 0;
*val = 30;
pthread_exit((void*)(long)val);
}
在gdb调试中就会有如下警告:
Program terminated with signal SIGSEGV, Segmentation fault.
SIGSEGV是当一个进程执行了一个无效的内存引用,或发生段错误时发送给它的信号。
与return的区别
return
只是退出了函数,线程仍有可能存在;pthread_exit
让线程停止。
二者都会调用清理函数,其它的区别还看不懂。
pthread_join
int pthread_join(pthread_t thread, void** rval_ptr);
返回值:成功——0;失败——错误编号。
如果线程被取消(pthread_cancel
),rval_ptr
所指向的内存单元被设置为PTHREAD_CANCELED
(好像值是-1)。
如果不关心线程返回值,rval_ptr
可以直接填NULL
。
获取返回值:
帮助理解:pthread_join
返回后,rval_ptr
所指向的内存单元保存的就是pthread_exit
的返回值(可能是数据本身,也可能是指针)。
// 1、返回数据本身
void* func_one(void* ptr)
{
g_val = 10;
pthread_exit((void*)(long)g_val);
}
int main()
{
pthread_t thread_one;
pthread_create(&thread_one, NULL, func_one, NULL);
int i;
// &i所指向的内存单元就是i的值,也就是g_val的值
pthread_join(thread_one, (void**)&i);
printf("%d\n", i);
}
// 2、返回的是指针
void* func_one(void* ptr)
{
g_val = 10;
pthread_exit((void*)&g_val);
}
int main()
{
pthread_t thread_one;
pthread_create(&thread_one, NULL, func_one, NULL);
int *i;
// &i所指向的内存单元是指针i,仍是一个指针,也就是&g_val,解一次引用后得到数据
pthread_join(thread_one, (void**)&i);
printf("%d\n", *i);
}
pthread_cancel
int pthread_cancel(pthread_t tid);
返回值:成功——0;失败——错误编号。
终止同一进程中的其它线程。
调用这一函数只是提出请求,并不会阻塞等待。
线程安排它退出时需要调用的函数。这样的函数被称为线程清理处理程序,可以设置多个,存入栈中。
例子:让一个子线程取消另一个子线程,参数的传递好像有点麻烦
int main()
{
...
// 将tid1的地址转为void*后传给线程函数th_cancel
pthread_create(&tid2, NULL, th_cancel, (void*)&tid1);
...
}
void* th_cancel(void* arg)
{
// 先将arg转为pthread_t*类型的指针,然后对其解引用*(...)
pthread_cancel(*((pthread_t*)arg));
}
pthread_cleanup_push/pop
设置线程清理处理程序。
线程结束时要执行一些善后工作,这些代码不方便写在主函数中,所以有了这对函数。
void pthread_cleanup_push(void (*rtn)(void*), void *arg);
void pthread_cleanup_pop(int execute);
- 二者是以宏的形式定义的,
pthread_cleanup_push
带有一个"{"
,而pthread_cleanup_pop
带有一个"}"
,所以必须成对使用! - 如果使用
pthread_cleanup_pop(0)
,则只会将栈中的一个清理程序弹出,不执行。传入任意非0的值都会弹出并执行函数。
清理函数的执行时机
**!!**清理函数执行时,执行多少个清理函数,按什么顺序执行,都取决于此时栈中的情况!!
**1、**函数运行到pthread_cleanup_pop(非0)
;
**2、**线程退出。自行退出或被cancel都能触发清理函数,且会执行栈中的所有清理函数。
注意!:如果主线程-进程终止而导致的子线程终止,可能会使清理函数来不及执行。
所以主线程总是要给子线程的运行留下足够的时间。
再补充:清理函数要被执行,一个大前提是“栈中存有清理函数”,即线程退出时,pthread_cleanup_pop
被执行的次数必须少于pthread_cleanup_push
。
误区纠正
误区1:pthread_cleanup_pop
仅仅是用来设置栈中函数在弹出时是否执行。
纠正:pthread_cleanup_pop
既是函数调用,也会在线程被结束时弹出并调用清理函数。
证:
void* func_one(void* ptr)
{
pthread_cleanup_push(exit_foo, (void*)(long)1);
pthread_cleanup_push(exit_foo, (void*)(long)2);
// pthread_exit((void*)(long)5);
// 主线程用此函数创建了子线程后,立即调用pthread_cancel
sleep(5);
printf("before pop \n");
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
printf("after pop, sleep \n");
sleep(5);
printf("thread exit \n");
pthread_exit((void*)(long)10);
}
输出结果:和预料的一样,子线程在退出时按栈顺序调用了清理函数
[root@localhost coding]# make run
g++ -g -o test test.cpp -lpthread
./test
thread cleanup 2
thread cleanup 1
-1
------------------------------------------------------
void* func_one(void* ptr)
{
pthread_cleanup_push(exit_foo, (void*)(long)1);
pthread_cleanup_push(exit_foo, (void*)(long)2);
// pthread_exit((void*)(long)5);
// 主线程创建子线程后,等待
// sleep(5);
printf("before pop \n");
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
printf("after pop, sleep \n");
sleep(5);
printf("thread exit \n");
pthread_exit((void*)(long)10);
}
输出结果:就像调用了函数一样,在线程结束前就执行了清理函数,等到线程退出时不再执行清理函数
[root@localhost coding]# make run
./test
before pop
thread cleanup 2
thread cleanup 1
after pop, sleep
// (约5s后)
thread exit
10
!收获
被pthread_cleanup_push
和pthread_cleanup_pop
“包裹”起来的代码段,就是这对push/pop
要负责善后的代码段。
实际应用中应该是像这样吧?:
pthread_cleanup_push(文件清理函数);
...文件操作...
pthread_cleanup_pop(1);
pthread_cleanup_push(IO清理函数);
...IO操作...
pthread_cleanup_pop(1);
pop的位置
例子:在本例中,return
和pthread_exit
效果一样。
void* func_one(void* ptr)
{
pthread_cleanup_push(exit_foo, (void*)(long)1);
pthread_cleanup_push(exit_foo, (void*)(long)2);
// 一、
// pthread_exit((void*)(long)5);
pthread_cleanup_pop(0);
pthread_cleanup_pop(1);
// 二、
pthread_exit((void*)(long)10);
}
// -------------------------------
// 一、
[root@localhost coding]# make run
./test
thread cleanup 2
thread cleanup 1
5
// 二、
[root@localhost coding]# make run
g++ -g -o test test.cpp -lpthread
./test
thread cleanup 1
10
将pthread_exit
放在pthread_cleanup_pop
之前,所有的清理函数都会执行,无论是否传入参数0。(是否可以理解为“还没有读到pthread_cleanup_pop的具体设置,线程就退出了,所以默认都弹出并执行”
)
**pthread_cancel
同理!**如果一个子线程在执行到pthread_cleanup_pop
之前就被其它线程cancel,也会执行所有的清理函数,而不管具体的pop设置。
pthread_detach
可以让线程函数自己调用pthread_detach(pthread_self())
,或是由别的线程调用pthread_detach(tid)
。
int pthread_detach(pthread_t tid);
返回值:成功——0;失败——错误编号。
让ID为tid
的线程处于分离状态,不能再用pthread_join
对其进行等待。
对分离状态的线程调用pthread_join
不会阻塞调用函数的线程,且pthread_join
接收的数据没有意义。
目前只知道会对
pthread_join
有影响。cancel和push/pop无影响。
----------------------------
除了对应的init函数,似乎所有锁都能用一个PTHREAD_XXX_INITIALIZER
变量进行赋值来完成初始化。
pthread_mutex_init/destroy
PTHREAD_MUTEX_INITIALIZER
可对静态分配的互斥锁(或作为全局变量的互斥锁)进行初始化。
// 2个参数
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);
int pthread_mutex_destroy(pthread_mutex_t* restrict mutex);
2个函数的返回值:成功——0;失败——错误编号。
将attr
设为NULL,可使用默认的初始化互斥量。
pthread_mutex_lock/trylock/unlock
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
3个函数的返回值:成功——0;失败——错误编号。
使用pthread_mutex_lock
对互斥量上锁,如果互斥量已经上锁,调用pthread_mutex_lock
的函数会被阻塞,直到互斥量被解锁。
pthread_mutex_unlock
解锁互斥量。
如果不希望线程被阻塞,可以使用pthread_mutex_trylock
尝试对互斥量加锁。如果互斥量可用,加锁;否则返回EBUSY。
pthread_mutex_timedlock
tsptr
使用的是绝对时间:1970年1月1日以来经过的秒数。
int pthread_mutex_timedlock(pthread_mutex_t* restrict mutex, const struct timespec* restrict tsptr);
返回值:成功——0;失败——错误编号。
功能与pthread_mutex_lock
基本等价,但到达超时时间时,会返回错误码ETIMEDOUT
。即这个函数愿意阻塞等待X秒
。
tsptr
:timespec结构,2个成员:time_t tv_sec
、long tv_nsec
分别表示s
和ns
。
1*109ns= 1s 。
获取时间参数的例子
struct timespec tout;
clock_gettime(CLOCK_REALTIME, &tout);
tout.tv_sec += 10;
pthread_mutex_timedlock(&lock, &tout);
pthread_rwlock_init/destroy
PTHREAD_RWLOCK_INITIALIZER
可对静态分配的读写锁(或作为全局变量的读写锁)进行初始化。
// 2个参数
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock, const pthread_rwlockattr_t* restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
2个函数的返回值:成功——0;失败——错误编号。
只有当读状态的锁的使用频率远高于写状态的锁的使用频率,使用读写锁才可以改善性能。
pthread_rwlock_rdlock/wrlock/unlock
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
3个函数的返回值:成功——0;失败——错误编号。
pthread_rwlock_rdlock
:在读模式下锁定读写锁。
pthread_rwlock_wrlock
:在写模式下锁定读写锁。
pthread_rwlock_unlock
:2种模式的读写锁都能解锁。
pthread_rwlock_tryrdlock/trywrlock
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
返回值:成功——0;失败——错误编号。
可以获得锁,加锁,返回0;否则返回EBUSY。
pthread_rwlock_timedrdlock/timedwrlock
int pthread_rwlock_timedrdlock(pthread_rwlock_t* restrict rwlock, const struct timespec* restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t* restrict rwlock, const struct timespec* restrict tsptr);
返回值:成功——0;失败——错误编号。
超时到期都返回ETIMEDOUT
。都使用绝对时间。