linux编程--线程
一、线程概念
1.什么是线程
LWP:light weight process 轻量级的进程,本质仍是进程(在Linux环境下)
进程:独立地址空间,拥有PCB
线程:也有PCB,但没有独立的地址空间(共享)
区别:在于是否共享地址空间。 独居(进程);合租(线程)。
Linux下: 线程:最小的执行单位
进程:最小分配资源单位,可看成是只有一个线程的进程。
2.线程共享资源与非共享资源
共享资源:
1.文件描述符表
2.每种信号的处理方式
3.当前工作目录
4.用户ID和组ID
5.内存地址空间 (.text/.data/.bss/heap/共享库)
非共享资源:
1.线程id
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户空间栈)(自己的栈空间)
4.errno变量
5.信号屏蔽字
6.调度优先级
3.线程优缺点
优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便
缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。
二、线程控制函数
1.pthread_self()函数
获取线程ID。其作用对应进程中 getpid() 函数。
函数原型:pthread_t pthread_self(void); 返回值:成功:0; 失败:无
线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现
线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)
注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。
在主控线程中可以使用tid获得线程ID。
2.pthread_create()函数
创建一个新线程。其作用,对应进程中fork() 函数。
函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值:成功:0;失败:错误号 -----Linux环境下,所有线程特点,失败均直接返回错误号,但不设置errno,所以无法使用perror
函数打印错误信息,需要使用strerror函数将返回的错误号转换成错误信息后再打印。
参数:
1. thread:传出参数,保存系统为我们分配好的线程ID
2. attr:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数,后面线程属性会详细讲解。
3. start_routine:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
4. arg:传递给线程主函数执行期间所使用的参数,回调函数。
示例代码:循环创建N个子线程,每个线程打印自己是第几个被创建的线程。(类似于进程循环创建子进程)
#include <pthread.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> void *tfn(void *arg) { int i; i = (int)arg; sleep(i); //通过i来区别每个线程 printf("I'm %dth thread, Thread_ID = %lu ", i+1, pthread_self()); return NULL; } int main(int argc, char *argv[]) { int n = 5, i; pthread_t tid; if (argc == 2) n = atoi(argv[1]); for (i = 0; i < n; i++) { pthread_create(&tid, NULL, tfn, (void *)i); //将i转换为指针,在tfn中再强转回整形。 } sleep(n); printf("I am main, and I am not a process, I'm a thread! " "main_thread_ID = %lu ", pthread_self()); pthread_exit(NULL); }
思考:将pthread_create函数参4修改为(void *)&i, 将线程主函数内改为 i=*((int *)arg) 是否可以。
答案:不可以,值传递与地址传递的区别,如果使用i=*((int *)arg),就是地址传递,直接操作读取主控线程中的i值,
而主控线程中的i值在for循环中一直自增,即第一个子线程创建出来后,i已经自增,在子线程中读取的是自增后的值,
所以是从2开始打印的。
3.pthread_exit()函数
将单个线程退出,类似于进程中的exit()
函数原型:void pthread_exit(void *retval);
参数:retval表示线程退出状态,通常传NULL,当retval不为NULL时,其值由pthread_join函数接收,后续会讲到
思考:使用exit将指定线程退出,可以吗?
结论:多线程环境中,应尽量少用,或者不使用exit函数,取而代之使用pthread_exit函数,将单个线程退出。任何线程里exit
导致进程退出,其他线程未工作结束,主控线程退出时不能return或exit。pthread_exit或者return返回的指针所指向的内存单元
必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
总结:
return:返回到调用者那里去,在主线程使用return会将整个进程退出。
pthread_exit():将调用该函数的线程退出
(在主控线程使用只是将线程退出,而不是整个进程退出,在主线程中使用return会将整个进程退出,
导致子线程来不及执行就结束了, 在主线程尽量不要要exit和return)
exit: 将进程退出,在线程中使用exit会导致整个进程退出,导致其他线程未执行,
所以在多线程编程统一使用pthread_exit()函数。
4.pthread_join()函数
阻塞等待线程退出,获取线程退出状态 , 其作用对应进程中 waitpid() 函数。
函数原型:int pthread_join(pthread_t thread, void **retval);
返回值:成功:0;失败:错误号
参数:thread:线程ID (【注意】:不是指针);retval:存储线程结束状态(pthread_exit(void*)retval)的值)。
对比记忆:
进程中:main返回值、exit参数-->int;等待子进程结束 waitpid 函数参数-->int * status
线程中:线程主函数返回值、pthread_exit-->void *;等待线程结束 pthread_join 函数参数-->void **retval
示例代码:参数 retval 非空用法,使用pthread_join回收pthread_exit()的退出值
#include <stdio.h> #include <unistd.h> #include <pthread.h> #include <stdlib.h> typedef struct{ int a; int b; } exit_t; void *tfn(void *arg) { exit_t *ret; ret = malloc(sizeof(exit_t)); ret->a = 100; ret->b = 300; pthread_exit((void *)ret); return NULL; //should not be here. } int main(void) { pthread_t tid; exit_t *retval; pthread_create(&tid, NULL, tfn, NULL); /*调用pthread_join可以获取线程的退出状态*/ pthread_join(tid, (void **)&retval); printf("a = %d, b = %d ", retval->a, retval->b); return 0; }
总结:调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join
得到的终止状态是不同的,总结如下:
1.如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
2.如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
3.如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
4.如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
阻塞等待回收,防止僵尸线程;
示例代码:使用pthread_join函数将循环创建的多个子线程回收。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> int var = 100; void *tfn(void *arg) { int i; i = (int)arg; sleep(i); if (i == 1) { //i = 0 100 333 333 777 777 var = 333; printf("var = %d ", var); pthread_exit((void *)var); } else if (i == 3) { var = 777; printf("I'm %dth pthread, pthread_id = %lu var = %d ", i+1, pthread_self(), var); pthread_exit((void *)var); } else { printf("I'm %dth pthread, pthread_id = %lu var = %d ", i+1, pthread_self(), var); pthread_exit((void *)var); } return NULL; } int main(void) { pthread_t tid[5]; int i, *ret[5]; for (i = 0; i < 5; i++) pthread_create(&tid[i], NULL, tfn, (void *)i); for (i = 0; i < 5; i++) { pthread_join(tid[i], (void **)&ret[i]); printf("-------%d 's ret = %d ", i, (int)ret[i]); } printf("I'm main pthread tid = %lu var = %d ", pthread_self(), var); pthread_exit(NULL); }
5.pthread_detach()函数
实现线程分离,
线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。
网络、多线程服务器常用。
进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,
导致内核认为该进程仍存在。也可使用 pthread_create函数参2(线程属性)来设置线程分离,后续在线程属性中详细讲解。
函数原型:int pthread_detach(pthread_t thread);
返回值: 成功:0;失败:错误号
示例代码:使用pthread_detach函数实现线程分离
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <string.h> void *tfn(void *arg) { int n = 3; while (n--) { printf("thread count %d ", n); sleep(1); } return (void *)1; } int main(void) { pthread_t tid; void *tret; int err; #if 1 pthread_attr_t attr; /*通过线程属性来设置游离态,必须在在线程创建前设置属性*/ pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_create(&tid, &attr, tfn, NULL); pthread_attr_destroy(&attr); #else pthread_create(&tid, NULL, tfn, NULL); pthread_detach(tid); #endif while (1) { err = pthread_join(tid, &tret); if (err != 0) fprintf(stderr, "thread %s ", strerror(err)); else fprintf(stderr, "thread exit code %d ", (int)tret); sleep(1); } return 0; }
总结:1.一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以
被置为detach状态,这样的线程一旦终止就立刻回收它占用的 所有资源而不保留终止状态。
2.不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。
如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
6.pthread_cancel()函数
杀死(取消)线程 ,其作用对应进程中 kill() 函数。
函数原型:int pthread_cancel(pthread_t thread);
返回值: 成功:0;失败:错误号
注意事项:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点(检查点)。
类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。杀死线程也不是立刻就能完成,
必须要到达取消点。
取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,
write..... ——参 APUE.12.7 取消选项。
可粗略认为一个系统调用(进入内核)即为一个取消点。
若线程主体函数长时间不会调用系统调用函数,可以自己手动添加取消点:pthread_testcancel(void); //自己添加取消点
扩展:
设置线程可取消和不可取消状态函数:int pthread_setcancelstate(int state,int *oldstate);
函数参数:state:分离属性,可分离或者不可分离,可以设置的参数为PTHREAD_CANCEL_ENABLE或者
PTHREAD_CANCEL_DISABLE
oldstate:函数将当前可取消状态设置为state,而将原来的可取消状态保存在oldstate所指向的内存空间,
这两步是原子操作, 不可分割
我们所默认的取消为推迟取消,即调用pthread_cancel函数后,在线程没有到达取消点之前,并不会取消。
可以通过调用pthread_setcreatetype()来修改取消类型
函数原型:pthread_setcanceltype(int type,int *oldtype);
参数:type:参数可以设置为PTHREADCANCEL_DEFERRED或者PTHREADCANCEL_ASYNCHRONOUS,
推迟取消或者异步取消
oldtype:保存原来的状态
使用异步取消时,可以在任意时间撤销而不需要非要遇到取消点
示例代码:终止线程的三种方法。注意“取消点”的概念。return,pthread_exit,pthread_cancel
#include <stdio.h> #include <unistd.h> #include <pthread.h> #include <stdlib.h> void *tfn1(void *arg) { printf("thread 1 returning "); return (void *)111; } void *tfn2(void *arg) { printf("thread 2 exiting "); pthread_exit((void *)222); } void *tfn3(void *arg) { while (1) { // printf("thread 3: I'm going to die 1 seconds after... "); // sleep(1); pthread_testcancel(); //自己添加取消点 } } int main(void) { pthread_t tid; void *tret = NULL; pthread_create(&tid, NULL, tfn1, NULL); pthread_join(tid, &tret); printf("thread 1 exit code = %d ", (int)tret); pthread_create(&tid, NULL, tfn2, NULL); pthread_join(tid, &tret); printf("thread 2 exit code = %d ", (int)tret); pthread_create(&tid, NULL, tfn3, NULL); sleep(3); pthread_cancel(tid); pthread_join(tid, &tret); printf("thread 3 exit code = %d ", (int)tret); return 0; }
总结:终止某个线程而不终止整个进程,有三种方法:
- 从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用exit。
- 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
- 线程可以调用pthread_exit终止自己。
7.线程与共享
【牢记】:线程默认共享数据段、代码段等地址空间,常用的是全局变量。而进程不共享全局变量,只能借助共享内存。
示例代码:设计程序,验证线程之间共享全局数据。
#include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> int var = 100; void *tfn(void *arg) { var = 200; printf("thread "); return NULL; } int main(void) { printf("At first var = %d ", var); pthread_t tid; pthread_create(&tid, NULL, tfn, NULL); sleep(1); printf("after pthread_create, var = %d ", var); return 0; }
8.最终总结,控制函数对比
进程 线程
fork pthread_create
exit pthread_exit
waitpid pthread_join
kill pthread_cancel
getpid pthread_self 命名空间
综合练习:多线程拷贝
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <fcntl.h> #include <unistd.h> #include <sys/stat.h> #include <sys/mman.h> #define T_NUM 5 //默认线程数5 #define ITEMS 66 //'='的个数 void err_sys(void *str) { perror(str); exit(1); } void err_usr(char *str) { fputs(str, stderr); exit(1); } /*每个线程都对应如下3个属性*/ typedef struct { int off; //拷贝的起始位置 int size; //拷贝的长度 int t_no; //自己是第几个被创建的线程 } arg_t; char *s, *d; int *done; //为一个数组,每个元素记录每个线程完成任务字节数 int n = T_NUM; void *tfn(void *arg) { arg_t *arg_p; int i; char *p, *q; arg_p = (arg_t *)arg; //每个线程自己的结构体 arg{off, size, t_no} p = s + arg_p->off; //当前线程执行拷贝任务,在原文件中的起始位置 q = d + arg_p->off; //目标文件的起始位置 //每个线程按字节拷贝自己的任务,并将拷贝字节数写入字节对应的done数组中 for (i = 0; i < arg_p->size; i++) { *q++ = *p++; done[arg_p->t_no]++; usleep(100); } return NULL; } void *display(void *arg) { int size, interval, draw, sum, i, j; size = (int)arg; //文件总大小 interval = size / ITEMS; //每个'='所代表的字节数 draw = 0; //画出的'='的个数 while (draw < ITEMS) { for (i = 0, sum = 0; i < n; i++) //借助done数组获取当前已经拷贝的总字节数 sum += done[i]; j = sum / interval; //计算到当前应该打印多少个'=' for (; j > draw; draw++) { //输出与线程拷贝字节数相对应个数的'=' putchar('='); fflush(stdout); } } putchar(' '); return NULL; } int main(int argc, char *argv[]) { int src, dst, i, len, off; struct stat statbuf; pthread_t *tid; arg_t *arr; if (argc != 3 && argc != 4) err_usr("usage : cp src dst [thread_no] "); if (argc == 4) n = atoi(argv[3]); //用户指定线程数,默认值5 src = open(argv[1], O_RDONLY); if (src == -1) err_sys("fail to open"); dst = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0644); if (dst == -1) err_sys("fail to open"); if (fstat(src, &statbuf) == -1) err_sys("fail to stat"); ftruncate(dst, statbuf.st_size); //源文件映射区首地址,保存在 全局变量s中,转换为指针方便定位操作,否则对文件只能使用lseek定位 s = (char *)mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, src, 0); if (s == MAP_FAILED) err_sys("fail to mmap"); //目标文件映射区首地址,保存在 全局变量d中 d = (char *)mmap(NULL, statbuf.st_size, PROT_WRITE , MAP_SHARED, dst, 0); if (d == MAP_FAILED) err_sys("fail to mmap"); close(src); close(dst); //pthread_t tid[n+1]; 线程ID数组分配空间 tid = (pthread_t *)malloc(sizeof(pthread_t) * (n+1)); if (tid == NULL) err_sys("fail to malloc"); //int done[n] 为int *done分配空间 done = (int *)calloc(sizeof(int), n); if (done == NULL) err_sys("fail to calloc"); //arr[n] 为结构体数据arr分配空间 arr = (arg_t *)malloc(sizeof(arg_t) * n); if (arr == NULL) err_sys("fail to malloc"); //计算每个线程应拷贝字节数, 起始偏移位置归零 len = statbuf.st_size / n, off = 0; //计算出每个线程拷贝的起始地址, 对应i写入结构体数组arr for (i = 0; i < n; i++, off += len) arr[i].off = off, arr[i].size = len, arr[i].t_no = i; //调整下最后一个线程拷贝的字节个数 arr[n-1].size += (statbuf.st_size % n); //创建拷贝线程, 对应i将每个线程arr[i]中记录的应拷贝的任务,传入线程主函数 for(i = 0; i < n; i++) pthread_create(&tid[i], NULL, tfn, (void *)&arr[i]); //创建进度线程, 传入文件总大小 pthread_create(&tid[n], NULL, display, (void *)statbuf.st_size); //回收子线程 for(i = 0; i < n+1; i++) pthread_join(tid[i], NULL); munmap(s, statbuf.st_size); munmap(d, statbuf.st_size); free(tid); free(done); free(arr); return 0; }