linux系统-线程及其管理

线程

线程概念

  • LWP: light weight process, 轻量级进程,本质仍是进程(在linux下)

  • 进程:独立地址空间,拥有PCB

  • 线程:也有PCB,但没有独立的地址空间(共享)

  • 区别:在于是否共享地址空间。进程独居,线程合租。

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);
//成功返回0, 失败返回errno;

注意:

  • 不应使用全局变量pthread_t tid, 而应使用pthread_self

  • 在子线程中通过pthread_create传出参数来获取线程ID

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);
}
/*父进程等待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; //注意传地址最后取出来的值
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;
// printf("in main, thread id = %lu\n",pthread_self());
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;
}

stack地址

错误分析: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);//参数:retval表示线程退出状态,通常传NULL
  • exit()函数用来退出当前进程, 不可以用在线程中, 否则全部退出(exit退出会使进程退出

  • pthread_exit()函数才是用来将单个的线程退出

  • pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者malloc分配的, 不能在线程函数的栈上分配, 因为其他线程得到这个返回指针时线程函数已经退出了

returnexit的区别:

  • 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);
}
// sleep(1);
pthread_exit(NULL); //主线程退出
return 0;
}

pthread_join函数

阻塞等待线程退出,获取进程退出状态。其作用对应进程中的waitpid()函数。

1
int pthread_join(pthread_t thread,void** retval);//成功返回0,失败返回错误号.线程的退出状态是void*, 回收时传的就是void**
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回收子线程*/
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); //成功返回0,失败返回错误号

线程分离状态:指定该状态,线程主动与主控线程断开关系线程结束后,其退出状态不由其他线程获取,而是直接自己主动释放。网络、多线程服务器常用。

进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要是由于进程死后,大部分资源被释放,一点残留资源仍然在系统中,导致内核以为该进程仍然存在。

也可以使用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);
// pthread_detach(tid); //让线程分离,自动退出,无系统残留资源
#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);//成功返回0,失败返回错误号

注意:线程的取消并不是实时的,而是有一定的延时,需要等待线程到达某一个取消点(检查点,进入内核的契机),所以如果一个线程一直使用系统调用(一直不进内核), 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)
{
// printf("thread 3: I'm going to die in 3 seconds...\n"); //取消点
// sleep(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;
}
/*
thread 1 returing
thread 1 exit code = 111
thread 2 exiting
thread 2 exit code = 222
thread 3: I'm going to die in 3 seconds...
thread 3 exit code = 666*/

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); //成功返回0,失败返回错误号

销毁线程属性所占用的资源

1
int pthread_attr_destroy(pthread_attr_t *attr);//成功返回0,失败返回错误号

线程的分离状态

调用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);
/*
detachstate取值:
PTHREAD_CREATE_DETACHED
PTHREAD_CREATE_JOINABLE */
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);
/*join试一下,由于线程已经分离了,会出错*/
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_setstackpthread_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)

线程同步

同步概念

同步,即同时起步,协调一致。不同的对象,对同步的理解方式不同。例如:设备同步指在两个设备之间规定一个共同的时间参考。 数据库同步指让两个或多个数据库内容保持一致,或者按需要部分保持一致。文件一致指让两个或多个文件夹中的文件保持一致。

线程同步

一个线程发出某一功能调用时,再没有得到结果之前,该调用不返回。同时其他线程为保证数据的一致性,不能调用该功能。

避免产生与时间有关的错误