线程 线程概念
Linux下:
线程为最小执行单位(cpu获得效率)
进程为最小资源分配单位 ,可看作只有一个线程的进程。
查看LWP号: ps -Lf pid
查看指定线程的lwp号
linux内核线程实现 linux中进程和线程关系密切
线程是轻量级进程(light weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
从内核里看进程和线程是一样的,都有各自不同的PCB,但PCB中指向的内存资源的三级页表是相同的
进程可以蜕变为线程
线程可以看作寄存器和栈(主要体现在函数调用,每个线程的stack空间不一样)的集合
在linux下,线程是最小执行单位,进程是最小资源分配单位
三级页表 PCB中持有当前进程的页目录表的指针, 页目录表中每一项指向一个个页表, 用页表检索物理内存页面
线程之间共享的资源
文件描述符表
每种信号的处理方式(线程和信号最好不要一起使用)
当前工作目录
用户ID和组ID
内存地址空间(.text/ .data/ .bss/ heap/共享库
)
线程非共享资源
线程id
处理器现场和栈指针(内核栈)
独立的栈空间(用户空间栈)
errno变量
信号屏蔽字
调度优先级
线程优缺点 优点: 提高程序并发性,开销小,数据通信、共享数据方便
缺点:库函数不稳定,调试编写困难、gdb不支持,对信号支持不好
linux下的实现方法使得进程和线程的差别不是很大。但可以通过在一个进程中开多个线程来达到抢占cpu的目的。
线程控制原语 pthread_self函数 获取线程ID。其作用对应进程中getpid()函数
1 pthread_t pthread_self(void );
线程ID:pthread_t
类型,本质:在Linux下为无符号整数(lu%
),其他系统中可能是结构体实现
线程ID是进程内部的识别标志。(两个进程间的线程ID允许相同 )
创建线程:
1 2 int pthread_create (pthread_t * thread,const pthread_attr_t * attr,void * (*start_routine)(void * ),void * arg) ;
注意:
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 #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <string.h> void * thread_func (void * arg) { printf ("thread id is: %lu\n" ,pthread_self()); return NULL ; } int main (void ) { pthread_t tid; int ret; printf ("in main, thread id = %lu\n" ,pthread_self()); ret = pthread_create(&tid, NULL ,thread_func,NULL ); if (ret != 0 ) { fprintf (stderr ,"pthred_create error: %s\n" ,strerror(ret)); exit (1 ); } sleep(1 ); return 0 ; }
循环创建线程 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 #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <string.h> void * thread_func (void * arg) { int i = (int )arg; sleep(i); printf ("%dth thread id is: %lu\n" ,i,pthread_self()); return NULL ; } int main (void ) { pthread_t tid; int ret, i; for (i=0 ; i<5 ; i++) { ret = pthread_create(&tid, NULL ,thread_func,(void *)i); if (ret != 0 ) { fprintf (stderr ,"pthred_create error: %s\n" ,strerror(ret)); exit (1 ); } } sleep(i); return 0 ; }
注意参数传递方式, 先将int型的i强转成void*传入, 用到时再强转回int型
线程参数传递 如果使void*
过程中不用强转, 看似规规矩矩的传地址再解引用, 会出现问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void * tfn (void * arg) { int i=*((int *)arg); printf ("I'm %dth thread,pid=%d,tid=%lu\n" ,i+1 ,getpid(),pthread_self()); sleep(i); return NULL ; } int main (int argc,char * argv[]) { int i=0 ; int ret=0 ; pthread_t tid=0 ; for (i=0 ;i<5 ;++i){ ret=pthread_create(&tid,NULL ,tfn,(void *)&i); if (ret!=0 ) perr_exit("pthread_create error" ); } sleep(i); return 0 ; }
错误分析:main
中给tfn
传入的是它的函数栈帧中局部变量i
的地址, 这样tfn
能随时访问到i的值, 考虑到线程之间是并发执行的, 每次中main
中固定的地址中拿数据, 相当于各个线程共享了这块地址, 由于访问时刻随机, 所以访问到的各个值也是很随机的
使用强转可以保证变量i
的实时性(C语言值传递的特性)
线程共享 线程默认共享数据段, 代码段等地址空间, 常用的是全局变量, 而进程不共享全局变量, 只能借助mmap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> int var = 20 ;void *fun (void *arg) { var = 10 ; printf ("thread\n" ); return NULL ; } int main () { printf ("befor pthread_create, var = %d\n" ,var); pthread_t tid; tid = pthread_create(&tid, NULL ,fun, NULL ); sleep(1 ); printf ("after pthread_create, var = %d\n" ,var); return 0 ; }
pthread_exit函数 将单个线程退出。
1 void pthread_exit (void * rerval) ;
exit()
函数用来退出当前进程, 不可以用在线程中 , 否则全部退出(exit退出会使进程退出 )
pthread_exit()
函数才是用来将单个的线程退出
pthread_exit
或者return
返回的指针所指向的内存单元必须是全局的或者malloc
分配的 , 不能在线程函数的栈上分配, 因为其他线程得到这个返回指针时线程函数已经退出了
return
和exit
的区别:
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 #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <string.h> void * thread_func (void * arg) { printf ("thread id is: %lu\n" ,pthread_self()); return NULL ; } int main (void ) { pthread_t tid; int ret; printf ("in main, thread id = %lu\n" ,pthread_self()); ret = pthread_create(&tid, NULL ,thread_func,NULL ); if (ret != 0 ) { fprintf (stderr ,"pthred_create error: %s\n" ,strerror(ret)); exit (1 ); } pthread_exit(NULL ); return 0 ; }
pthread_join函数 阻塞等待线程退出,获取进程退出状态 。其作用对应进程中的waitpid()
函数。
1 int pthread_join (pthread_t thread,void ** retval) ;
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 #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <string.h> typedef struct { char ch; int var; char str[64 ]; }exit_t ; void *thred_func (void *args) { exit_t * retvar = (exit_t *)malloc (sizeof (exit_t )); retvar->ch = 'm' ; retvar->var = 10 ; strcpy (retvar->str,"my thread\n" ); pthread_exit((void *)retvar); } int main () { pthread_t tid; exit_t *val; pthread_create(&tid,NULL ,thred_func,NULL ); pthread_join(tid,(void **)&val); printf ("ch = %c, var = %d, str = %s\n" ,val->ch,val->var,val->str); free (val); pthread_exit((void *)1 ); }
错误写法:
1 2 3 4 5 6 7 8 9 void * tfn (void * arg) { struct thrd tval ; tval.var=100 ; strcpy (tval.str,"love you" ); return (void *)&tval; }
不能将子线程的回调函数的局部变量返回, 由于该函数执行完毕返回后, 其栈帧消失, 栈上的局部变量也就消失, 返回的是无意义的 。可以在main函数中创建局部变量
pthread_detach函数 实现线程分离, 线程终止会自动清理pcb, 无需回收 ,子线程分离后不能再调用pthread_join
回收了。(detach
相当于自动回收, join
相当于手动回收 )
1 int pthread_detach (pthread_t thread) ;
线程分离状态:指定该状态,线程主动与主控线程断开关系 。线程结束后,其退出状态不由其他线程获取,而是直接自己主动释放。网络、多线程服务器常用。
进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要是由于进程死后,大部分资源被释放,一点残留资源仍然在系统中,导致内核以为该进程仍然存在。
也可以使用pthread_create
函数的第2个参数来实现:
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 #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <string.h> void *tfn (void *arg) { int n =3 ; while (n--){ printf ("thread count %d\n" ,n); sleep(1 ); } return (void *)1 ; } int main () { 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 ); #else pthread_create(&tid, NULL , tfn,NULL ); #endif while (1 ){ err = pthread_join(tid, &tret); printf ("--------------err = %d\n" ,err); if (err != 0 ){ fprintf (stderr ,"thread %s\n" , strerror(err)); } else { fprintf (stderr ,"thread exit code %d\n" ,(int )tret); } sleep(1 ); } return 0 ; }
一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join
获取其状态为止。但是线程也可以被设置为detach
状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。
pthread_cancel函数 杀死(取消)线程, 作用对应于进程中的kill()
函数
1 int pthread_cancel (pthread_t thread) ;
注意:线程的取消并不是实时的,而是有一定的延时,需要等待线程到达某一个取消点(检查点,进入内核的契机),所以如果一个线程一直使用系统调用(一直不进内核), cancel就无法杀死该线程
取消点:线程检查是否被取消,并按请求进行动作的一个位置:通常是一些系统调用 create, open , pause, close, read, write...
执行命令man 7 pthreads
可以查看具备这些取消点的系统调用列表。
可以粗略认为一个系统调用(进入内核)为一个取消点。如果线程中没有取消点,可以通过调用pthread_testcancel
函数自行设置一个取消点。
被取消的线程,退出值定义在linux的pthread
库中。常数PTHREAD_CANCELED
的值是-1,可在头文件pthread.h中找到定义:#define PTHREAD_CANCELED((void*)-1)
。因此当对一个已经被取消的线程使用pthread_join
回收时,得到的返回值为-1。
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <string.h> void * tfn1 (void * arg) { printf ("thread 1 returing\n" ); return (void *)111 ; } void * tfn2 (void * arg) { printf ("thread 2 exiting\n" ); pthread_exit((void *)222 ); } void * tfn3 (void * arg) { while (1 ) { } return (void *)666 ; } int main () { pthread_t tid; void * tret = NULL ; pthread_create(&tid,NULL ,tfn1,NULL ); pthread_join(tid, &tret); printf ("thread 1 exit code = %d\n" ,(int )tret); pthread_create(&tid, NULL , tfn2, NULL ); pthread_join(tid, &tret); printf ("thread 2 exit code = %d\n" ,(int )tret); pthread_create(&tid, NULL , tfn3, NULL ); sleep(3 ); pthread_cancel(tid); pthread_join(tid,&tret); printf ("thread 3 exit code = %d\n" ,(int )tret); return 0 ; }
pthread_equal 比较两个线程ID是否相等
1 int pthread_eaqul (pthread_t t1, pthread_t t2) ;
线程属性 linux下线程的属性可以根据实际项目需求来设置 。
1 2 3 4 5 6 7 8 9 10 11 12 typedef stuct { int etachstate; int schedpolicy; struct sched_param schedparam ; int inheritsched; int scope; size_t guardsize; int stackaddr_set; void * stackaddr; size_t stacksize; }pthread_attr_t ;
默认情况为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
线程栈大小查看命令 :ulimit -a
线程属性初始化 应先初始化线程属性,再pthread_create创建线程
1 int pthread_attr_init (pthread_attr_t *attr) ;
销毁线程属性所占用的资源
1 int pthread_attr_destroy (pthread_attr_t *attr) ;
线程的分离状态 调用pthread_detach()
函数或者通过属性设置可以使线程分离。如果一个线程为分离线程,而这个线程又运行非常之快,它很可能在pthread_create
函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create
的线程就得到了错误的线程号。要避免这种情况的发生可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait
函数,让这个线程等待一会,留出足够的时间让函数pthread_create返回 。设置一段等待时间,是在多线程中常用的方法。但注意不要使用wait()
之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
1 2 3 4 5 6 7 8 int pthread_attr_setdetachstate (pthread_attr_t * attr, int detachstate) ;int pthread_attr_getdetachstate (const pthread_attr_t * attr, int * detachstate) ;
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 int main (int argc,char * argv[]) { int ret=0 ; pthread_t tid=0 ; pthread_attr_t attr; ret=pthread_attr_init(&attr); if (ret!=0 ) perr_exit("pthread_attr_init error" ,ret); pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); if (ret!=0 ) perr_exit("pthread_attr_setdetachstate error" ,ret); printf ("main:pid=%d,tid=%lu\n" ,getpid(),pthread_self()); ret=pthread_create(&tid,&attr,tfn,NULL ); if (ret!=0 ) perr_exit("pthread_create error" ,ret); ret=pthread_join(tid,NULL ); if (ret!=0 ) perr_exit("pthread_join error" ,ret); ret=pthread_attr_destroy(&attr); if (ret!=0 ) perr_exit("pthread_attr_destory error" ,ret); pthread_exit(NULL ); return 0 ; }
线程的栈地址 当进程栈空间地址不够用时,指定新建线程使用由malloc
分配的空间作为自己的栈空间(各个子线程会均分进程的栈空间, 但是线程的栈空间大小是可以调整的) 。通过pthread_attr_setstack
和pthread_attr_getstack
两个函数分别设置和获取进程的栈地址。
线程的栈大小 1 2 int pthread_attr_setstacksize (pthread_attr_t *attr, size_t stacksize) int pthread_atrt_getstacksize (pthread_attr_t *attr, size_t *stacksize)
线程同步 同步概念 同步,即同时起步,协调一致。不同的对象,对同步的理解方式不同。例如:设备同步指在两个设备之间规定一个共同的时间参考。 数据库同步指让两个或多个数据库内容保持一致,或者按需要部分保持一致。文件一致指让两个或多个文件夹中的文件保持一致。
线程同步 一个线程发出某一功能调用时,再没有得到结果之前,该调用不返回。同时其他线程为保证数据的一致性,不能调用该功能。
避免产生与时间有关的错误 。