线程

编译多线程程序时,要注意参数

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_routinearg参数分别指定新线程将运行的函数参数。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_exitreturn替代也能正常运行。

二者区别(网络):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个线程互相针对对方调用pthread_join,或针对自身调用pthread_join

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。两种用途使用相同的锁(增加了锁的粒度),简化了代码结构。

线程与信号

外界信号不会中断子线程的运行(除非是终止进程的信号)。

在多线程程序中,捕获信号的函数放在哪都一样,通常放在主函数中。

多线程程序中,在任一线程中调用signalsigaction都会改变所有信号的信号处理函数。

主线程向子线程发送信号用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_seclong tv_nsec分别表示sns

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:等待指定的时间。当指定的任一描述符准备好,或超时时返回。

readfdswritefdsexecptfds:指向描述符集的指针。描述符集为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_pushpthread_cleanup_pop“包裹”起来的代码段,就是这对push/pop要负责善后的代码段。

实际应用中应该是像这样吧?:

pthread_cleanup_push(文件清理函数);
...文件操作...
pthread_cleanup_pop(1);

pthread_cleanup_push(IO清理函数);
...IO操作...
pthread_cleanup_pop(1);

pop的位置

例子:在本例中,returnpthread_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_seclong tv_nsec分别表示sns

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。都使用绝对时间。