详细学习的地方,可以当字典备查:
Multi-Threaded Programming With POSIX Threads (kent.edu)。
书《POSIX多线程程序设计》。
书《Unix_Linux_Windows_OpenMP多线程编程》中的《第三章 Unix/Linux 多线程编程》。
多线程指的是在单个 程序/进程 中可以同时运行多个不同的线程(可以视作共享同一块内存资源的多个任务),执行不同的任务:
更高的运行效率,并行执行;
多线程是模块化的编程模型;
与进程相比,线程的创建和切换开销更小;
通信方便;
能简化程序的结构,便于理解和维护;更高的资源利用率。
多线程的应用场景:
程序中出现需要等待的操作,比如网络操作、文件IO等,可以利用多线程充分使用处理器资源,而不会阻塞程序中其他任务的执行。
程序中出现可分解的大任务,比如耗时较长的计算任务,可以利用多线程来共同完成任务,缩短运算时间。
程序中出现需要后台运行的任务,比如一些监测任务、定时任务,可以利用多线程来完成。
线程安全与线程同步:
线程安全:多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全:不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。如果多个线程同时读写共享变量,会出现数据不一致的问题。
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,
线程异步:访问资源时在空闲等待时同时访问其他资源,实现多线程机制。
同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去
异步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程仍然请求的到,A线程无需等待
线程同步的优势:
好处:解决了线程的安全问题。
弊端:每次都有判断锁,降低了效率。
但是在安全与效率之间,首先考虑的是安全。
Linux 系统下的用 C 开发多线程使用叫 pthread 的线程库;内核级线程 和 用户级线程 是在创建线程时通过传入 API 的不同参数进行 区分/设置 的。
因为 pthread 并非 Linux 系统的默认库,而是 POSIX 标准的线程库。在 Linux 中将其作为一个库来使用,因此编译选项需要加上 -lpthread(或-pthread)以显式链接该库。例子:gcc xxx.c -lpthread -o xxx.bin
。
pthread_self()——获取线程 ID。
/* pthread_self()——函数获取线程 ID #includepthread_t pthread_self(void); 成功:返回线程号 */ #include #include int main() { pthread_t tid = pthread_self(); printf("tid = %lu\n",(unsigned long)tid); return 0; }
pthread_create()——线程创建。
/* pthread_create()——线程创建 #includeint pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg); 该函数第一个参数为pthread_t指针,用来保存新建线程的线程号。 第二个参数表示了线程的属性,可传入NULL表示默认属性。 第三个参数是一个函数指针,就是线程执行的函数。这个函数返回值为void*,形参为void*。 第四个参数则表示为向线程处理函数传入的参数,若不传入,可用NULL填充。 返回 0 表示成功,负值表示失败。 */ #include #include #include #include void *fun(void *arg) { printf("pthread_New = %lu\n",(unsigned long)pthread_self()); } int main() { pthread_t tid1; int ret = pthread_create(&tid1,NULL,fun,NULL); ... 简化,错误处理略 /* tid_main 为通过pthread_self获取的线程ID,tid_new通过执行pthread_create成功后tid指向的空间 */ /* 即 tid1 与 pthread_New 打印结果应为一致 */ printf("tid_main = %lu tid_new = %lu \n",(unsigned long)pthread_self(),(unsigned long)tid1); /* 因线程执行顺序随机,不加sleep可能导致主线程先执行,导致进程结束,无法执行到子线程 */ /* 也就是说,主线程 执行到这里 如果不加 sleep 则 后面直接 return 结束了,那么 线程 fun 还没执行 本进程就结束了 */ sleep(1); return 0; } /* 通过pthread_create确实可以创建出来线程,主线程中执行pthread_create后的tid指向了线程号空间,与子线程通过函数pthread_self打印出来的线程号一致。 特别说明的是,当主线程伴随进程结束时,所创建出来的线程也会立即结束,不会继续执行。并且创建出来的线程的执行顺序是随机竞争的,并不能保证哪一个线程会先运行。可以将上述代码中sleep函数进行注释,观察实验现象。 */
创建 进程时候 传入参数:
#include#include #include #include void *fun1(void *arg){ printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg); } void *fun2(void *arg){ printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg); } int main() { pthread_t tid1,tid2; int a = 50; int ret = pthread_create(&tid1,NULL,fun1,(void *)&a); /* 传入地址 */ ... 简化,错误处理略 ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a); /* 传入值 */ ... 简化 sleep(1); printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a); return 0; }
pthread_exit / pthread_cancel 和 pthread_join / pthread_tryjoin_np——线程的退出。
线程的退出情况有三种:
第一种是进程结束,进程中所有的线程也会随之结束。
第二种是通过函数 pthread_exit() 来主动的退出所在的线程。
第三种被其他线程调用 pthread_cancel() 来被动退出。
关于线程退出后的资源回收:
一个进程中的多个线程是共享数据段的。如果一个线程是 joinable 或者叫 非分离状态的,在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放,要用 pthread_join/pthread_tryjoin_np 函数来同步并释放资源,即 当线程结束后,主线程要通过函数 pthread_join/pthread_tryjoin_np 来回收线程的资源,并且获得线程结束后需要返回的数据。如果一个线程是 unjoinable 或者叫 分离状态的,则 在线程退出之后 其自己会主动回收资源,主线程里便不用再调用 pthread_join/pthread_tryjoin_np 来回收线程的资源,当然此时 线程退出的时候也就不能传出参数。joinable 和 unjoinable 可以设置,后面 线程属性 部分会说到。
关于主线程 / 进程的退出:
在主线程中,在 main 函数中 return 了或是调用了 exit() 函数,则主线程退出,且整个进程也会终止,此时进程中的所有线程也将终止,因此要避免 main 函数过早结束。
在任何一个线程中调用 exit() 函数都会导致进程结束,进程一旦结束,那么进程中的所有线程都将结束。
以下 是对 pthread_exit / pthread_cancel 和 pthread_join / pthread_tryjoin_np 线程的退出 相关 API 的说明。
/* 线程主动退出 pthread_exit #includevoid pthread_exit(void *retval); pthread_exit函数为线程退出函数,在退出时候可以传递一个void*类型的数据带给主线程,若选择不传出数据,可将参数填充为NULL。 pthread_exit函数唯一的参数value_ptr是函数的返回值,只要pthread_join中的第二个参数value_ptr不是NULL,这个值将被传递给value_ptr 线程被动退出 pthread_cancel,其他线程使用该函数让另一个线程退出 #include int pthread_cancel(pthread_t thread); 成功:返回0 该函数传入一个tid号,会强制退出该tid所指向的线程,若成功执行会返回0。 线程资源回收(阻塞在执行到 pthread_join 的地方,然后等待 thread 线程的退出) #include int pthread_join(pthread_t thread, void **retval); 该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后才返回。第一个参数为要回收线程的tid号,第二个参数为线程回收后接受线程传出的数据, 或者该线程被取消而返回PTHREAD_CANCELED。 线程资源回收(非阻塞,需要循环查询) #define _GNU_SOURCE #include int pthread_tryjoin_np(pthread_t thread, void **retval); 该函数为非阻塞模式回收函数,通过返回值判断是否回收掉线程,成功回收则返回0,其余参数与pthread_join一致。 阻塞方式 pthread_join 和 非阻塞方式 pthread_tryjoin_np 使用上的区别: 通过函数 pthread_join 阻塞方式回收线程,几乎规定了线程回收的顺序,若最先回收的线程未退出,则一直会被阻塞,导致后续先退出的线程无法及时的回收。 通过函数 pthread_tryjoin_np 使用非阻塞回收线程,可以根据退出先后顺序自由的进行资源的回收。 */
线程属性相关:
参考 pthread_attr_init线程属性高司机的博客-CSDN博客pthread_attr_destroy,线程属性详解 线程属性pthread_attr_t简介_Robin Hu的专栏-CSDN博客。
/* 定义 pthread_attr_t 线程属性变量,用于设置线程属性,主要包括 scope 属性(用于区分用户态或者内核态)、detach(分离/joinable)属性、堆栈地址、堆栈大小、优先级 */ pthread_attr_t attr_1,attr_2_3_4[3]; /* 首先调用 pthread_attr_init 来对 线程属性变量 进行默认的初始化,然后才可以调用 pthread_attr_xxx 类函数来改变其值 */ pthread_attr_init(&attr_1); /* 比如 (这里举一个例子,有很多设置属性的 API) pthread_attr_setdetachstate(&attr_1,PTHREAD_CREATE_DETACHED); 来设置 该线程的 可分离属性(pthread_detach 函数也是设置某一个 线程的 这个属性) 在默认情况下线程是 joinable 或者叫非分离状态的,这种情况下,主线程等待子线程退出后,只有当 pthread_join() 函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。如果设置线程 为 unjoinable 或者叫 分离状态,即 子线程退出后 其主动的回收资源,主线程这里不必再调用 pthread_join 等待 子线程退出了。 可以使用 pthread_attr_setdetachstate 函数把线程属性 detachstate 设置为下面的两个合法值之一:设置为 PTHREAD_CREATE_DETACHED 以分离状态启动线程(unjoinable);或者设置为 PTHREAD_CREATE_JOINABLE 正常启动线程(joinable,即默认的)。 可以使用 pthread_attr_getdetachstate 函数获取当前的 datachstate 线程属性。 另外,一般 不 建 议 去主动更改线程的优先级。 上面的 线程属性相关 的参考 链接中 对更多属性设置API进行了介绍,包括 继承性、调度策略(两种可选+其它方式)、调度参数 */ /* 这里是通过设置线程属性 设置为 系统级线程,还是用户级线程 */ pthread_attr_setscope(&attr_1, PTHREAD_SCOPE_SYSTEM); /* 系统级线程,适合计算密集 */ pthread_attr_setscope(&attr_2_3_4[0], PTHREAD_SCOPE_PROCESS); /* 用户级线程,适合IO密集 */ /* 然后用这个属性去创建线程 */ pthread_create(&tid, &attr_1, fn, arg); /* 可以将属性都设为 NULL 值,来重新 init 然后设置 */ pthread_attr_destroy(&attr_1);
线程的竞争:参考 Linux线程的实现 & LinuxThread vs. NPTL & 用户级内核级线程 & 线程与信号处理 - blcblc - 博客园 (cnblogs.com),(227条消息) Linux进程解析_deep_explore的博客-CSDN博客。
系统级线程会与其它 进程 共同竞争时间片,用户及线程仅与所在进程内的其它用户及线程竞争调度。
Linux 2.6 以后的 pthread 使用 NPTL(更好支持 POSIX) 实现,都是系统级别的1:1线程(一个线程相当于一个进程,1:n就相当于一个进程里面n各线程相互竞争)模型,都是系统级线程。而 pthread_create() 里调用 clone() 时设置了CLONE_VM,所以在内核看来就产生了两个拥有相同内存空间的进程。所以用户态创建一个新线程,内核态就对应生成一个新进程。
因此,Linux 是一个多任务,多线程的操作系统。但其实现的线程机制非常独特,从内核的角度来说,它并没有线程这个概念。 Linux 把所有的线程当作进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。进程和线程都有自己的 task_struct, 在内核看来两者没有司马区别。
etc.
/* 文件:线程基本API的例子\线程API的例程-Linux下,被动回收.c */ #define _GNU_SOURCE #include#include #include #include #include /* 例子说明: 创建 系统级 线程 1,无传入和传出参数,死循环,特定条件时退出,使用 pthread_join 回收 创建 用户级 线程 2、3、4 三个线程,线程号使用数组,传入和传出参数,使用 pthread_tryjoin_np 回收 在 linux 环境 的 gnu 编译器 下 执行编译:gcc temp.c -lpthread -o temp.bin */ /* 用于设置线程属性,主要包括 scope 属性(用于区分用户态或者内核态)、detach(分离/joinable)属性、堆栈地址、堆栈大小、优先级 */ pthread_attr_t attr_1, attr_2_3_4[3]; /* 指向线程标识符的指针,区分线程,即可称为 线程号,仅在本进程中有效。本质是 unsigned long int */ pthread_t id_1, id_2_3_4[3]; /* 线程 1,无传入和传出参数,执行完后退出,使用 pthread_join 回收 */ void *thread_1(void *in_arg) { int i = 0; printf("thread_1 ID = %lu\n", (unsigned long)pthread_self()); for(;;) { printf("thread_1 print times = %d\n", ++i); if(i >= 3) pthread_exit(NULL); /* 用 pthread_exit() 来调用线程的返回值,用来退出线程,但是退出线程所占用的资源不会随着线程的终止而得到释放 */ sleep(1); /* sleep() 单位秒,程序挂起 1 秒 */ } } /* 线程 2 3 4 */ void *thread_2_3_4(void *in_arg) { /* 必须要 static 修饰,否则 pthread_join/pthread_tryjoin_np 无法获取到正确值 */ static char* exit_arg; /* exit_arg 是 本函数的一个局部变量,多个线程 2、3、4 都会修改它,因此最后返回的时候不知道是谁最后一次修改的 */ /* 因此要格外注意 */ exit_arg = (char*)in_arg; pthread_t self_id = pthread_self(); if(self_id == id_2_3_4[0]) { printf("thread_2 ID = %lu\n", (unsigned long)self_id); sprintf((char*)in_arg,"id_2 gagaga"); }else if(self_id == id_2_3_4[1]) { printf("thread_3 ID = %lu\n", (unsigned long)self_id); sprintf((char*)in_arg,"id_3 lalala"); }else if(self_id == id_2_3_4[2]) { printf("thread_4 ID = %lu\n", (unsigned long)self_id); sprintf((char*)in_arg,"id_4 hahaha"); }else { pthread_exit(NULL); } pthread_exit((void*)in_arg); } int main(void) { int ret = -1, i = 0, return_thread_num = 0; char *str_gru[3]; void *exit_arg = NULL; pthread_attr_init(&attr_1); pthread_attr_setscope(&attr_1, PTHREAD_SCOPE_SYSTEM); /* 系统级线程 */ for(i = 0;i < 3;i++) { pthread_attr_init(&attr_2_3_4[i]); pthread_attr_setscope(&attr_2_3_4[i], PTHREAD_SCOPE_PROCESS); /* 用户级线程 */ } /* 创建线程 1 */ ret = pthread_create(&id_1, &attr_1, thread_1, NULL); if(ret != 0) { /* perror 把一个描述性错误消息输出到标准错误 stderr, 调用"某些"函数出错时,该函数已经重新设置了errno 的值。perror 函数只是将你输入的一些信息和 errno 所对应的错误一起输出 */ perror("pthread1, pthread_create: "); return -1; } /* 创建线程 2、3、4 */ for(i = 0;i < 3;i++) { str_gru[i] = (char*)malloc(sizeof(char) * 42 + i); ret = pthread_create(&id_2_3_4[i], &attr_2_3_4[i], thread_2_3_4, (void *)str_gru[i]); if(ret != 0) { perror("pthread 2 3 4, pthread_create: "); return -1; } } /* 等待所有线程结束,先等 线程 2、3、4 相继的、无顺序要求的 退出,再等 线程 1 退出 */ for(;;) { for(i = 0;i < 3;i++) { /* pthread_tryjoin_np 的 np 为不可移植,是gnu定的非POSIX标准的API,仅linux里面的编译器能用 */ if(pthread_tryjoin_np(id_2_3_4[i], &exit_arg) == 0) { printf("pthread : %lu exit with str: %s\n", (unsigned long)id_2_3_4[i], (char*)exit_arg); free(str_gru[i]); return_thread_num++; } } if(return_thread_num >= 3) break; } pthread_join(id_1, NULL); return 0; }