目录
[一. 线程](#一. 线程)
[1.1 什么是线程](#1.1 什么是线程)
[1.2 线程与进程的区别](#1.2 线程与进程的区别)
[1.3 线程的共享与非共享资源](#1.3 线程的共享与非共享资源)
[1.3.1 线程共享资源](#1.3.1 线程共享资源)
[1.3.2 线程非共享资源](#1.3.2 线程非共享资源)
[1.4 线程优缺点](#1.4 线程优缺点)
[1.4.1 线程优点](#1.4.1 线程优点)
[1.4.2 线程缺点](#1.4.2 线程缺点)
[二. 线程控制函数](#二. 线程控制函数)
[2.1 pthread_self()](#2.1 pthread_self())
[2.2 pthread_create()](#2.2 pthread_create())
代码2,循环创建多个线程,并且要求每个线程打印自己是第几个被创建的:
[2.3 pthread_exit()](#2.3 pthread_exit())
[2.4 pthread_join()](#2.4 pthread_join())
[2.5 pthread_detach()](#2.5 pthread_detach())
[2.6 pthread_cancel()](#2.6 pthread_cancel())
[2.7 线程终止的三种方式](#2.7 线程终止的三种方式)
[2.8 进程与线程控制原语对比](#2.8 进程与线程控制原语对比)
一. 线程
1.1 什么是线程
线程:light weight process(LWP)轻量级进程,本质还是进程。
1.2 线程与进程的区别
|--------------------------|-------------------|
| 进程 | 线程 |
| 独立的地址空间,拥有PCB | 拥有独立的PCB,但是地址空间共享 |
| 是最小分配资源单位(可认为是只有一个线程的进程) | 最小执行单位 |

另外,若在进程A中创建了多个线程,但在CPU眼中其为进程,则进程A争夺CPU资源具有优势(线程为最小执行单位),但线程并不是越多越好。

这里通过一个小例子来说明:
在Linux系统下打开火狐浏览器:使用ps ajx命令查看其进程pid为6940

再使用命令 ps -Lf 6940 查看其LWP线程情况:LWP指的是线程号(CPU执行的最小单位),可以看到总共有64个线程,故浏览器的执行速度一般很快。

1.3 线程的共享与非共享资源
线程可以看作寄存器和栈的集合
1.3.1 线程共享资源
|------------------------------------------|
| 文件描述符表 |
| 每种信号的处理方式、未决信号集(说明某信号一但处理完成,其他线程无需在进行处理) |
| 当前工作目录 |
| 用户ID和组ID |
| 内存地址空间(.text/.data/.bss/heap/共享库) |
1.3.2 线程非共享资源
|------------------------------|
| 线程ID |
| 处理器现场(寄存器)和栈指针(内核栈) |
| 独立的栈空间(用户栈) |
| errno变量(线程控制函数失败时大部分直接返回错误号) |
| 信号屏蔽字(mask) |
| 调度优先级(可以通过指定调度优先级来指导哪个线程先执行) |
总结:
1、虽然线程共享每种信号的处理方式,但是线程不共享信号屏蔽字,可以通过修改信号屏蔽字来
指定一个线程来处理特定的信号。
2、线程共享.data,说明共享全局变量,线程间通信简单。
1.4 线程优缺点
1.4.1 线程优点
1、提高程序的并发性
2、开销小
3、共享数据,数据通信简单
1.4.2 线程缺点
1、库函数不稳定
2、调试,编写困难,不支持gdb调试
3、对信号支持不好
二. 线程控制函数
|--------------------------|----------------------|
| pthread_t pthread_self() | 获取线程ID |
| int pthread_creat() | 创建线程 |
| void pthread_exit() | 将单个线程退出 |
| int pthread_join() | 阻塞等待线程退出,获取线程退出状态 |
| int pthread_detach() | 设置线程分离,线程结束后自己主动释放资源 |
| int pthread_cancel() | 杀死线程,需要取消点 |
2.1 pthread_self()


作用:获取当前调用此函数线程的线程ID。
返回值:此函数总是成功。
注意:线程ID与LWP的区别:
线程ID:是进程内部识别线程的标志,主要是给本进程使用的(两个进程间,线程ID允许相同)
LWP:标识线程身份,给CPU用,CPU使用其来分配资源
2.2 pthread_create()

作用:`pthread_create()` 函数会在调用进程内启动一个新的线程。新线程通过调`start_routine()` 函数开始执行,而参数 `arg` 则作为 `start_routine()` 函数的唯一参数被传递进去。
返回值:成功返回0 失败返回错误号。
参数一:传出参数,传出线程ID
参数二:线程属性,一般为NULL
参数三:回调函数,线程的执行逻辑,
注意:这个函数传参时应该使用传值调用,借助强转来接收参数(精度不会丢失)。
参数四:回调函数的参数
备注:在一个线程中调用pthread_create() 创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。 start_routine函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为void *,这个指针按什么类型解释由调用者自己定义。start_routine的返回值类型也是void *,这个指针的含义同样由调用者自己定义。start_routine返回时,这个线程就退出了,其它线程可以调用pthread_join得到start_routine的返回值,类似于父进程调用wait()得到子进程的退出状态,稍后详细介绍pthread_join。
pthread_create成功返回后,新创建的线程的id被填写到thread参数 所指向的内存单元。我们知道进程id的类型是pid_t,每个进程的id在整个系统中是唯一的,调用getpid()可以获得当前进程的id,是一个正整数值。线程id的类型是pthread_t ,它只在当前进程中保证是唯一的,在不同的系统中pthread_t这个类型有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址,所以不能简单地当成整数用printf打印,调用pthread_self()可以获得当前线程的id。
代码1,创建一个线程:
cpp
void *tfun(void *arg)//子线程的回调函数
{
printf("thread: pid is %d,tid is %lu\n",getpid(),pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
int ret;
printf("main : pid is %d,tid is %lu\n",getpid(),pthread_self());
ret = pthread_create(&tid, NULL, tfun, NULL);//创建子线程
if(ret != 0)
{
fprintf(stderr,"pthread_creat error: %s\n",strerror(ret));
exit(1);
}
printf("tid is %lu\n",tid);
sleep(1);//防止主线程先结束,进程地址空间销毁,子线程无法执行回调函数
return 0;
}
执行结果:

代码2,循环创建多个线程,并且要求每个线程打印自己是第几个被创建的:
正确的写法:
cpp
void *tfun(void *arg)
{
int i = (int)arg;
sleep(i);
printf("%d pthread:pid is %d,tid is %lu\n",i + 1,getpid(), pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
int ret, i;
printf("main:pid is %d,tid is %lu\n",getpid(),pthread_self());
for(i = 0; i < 5; i++)
{
ret = pthread_create(&tid, NULL, tfun, (void*)i);//传参采用值传递
//借助强转
if(ret != 0)
{
fprintf(stderr,"pthread_create error: %s\n",strerror(ret));
exit(1);
}
}
sleep(i);
return 0;
}
结果:

错误写法:
cpp
void *tfun(void *arg)
{
int i = *((int*)arg);
sleep(i);
printf("%d pthread:pid is %d,tid is %lu\n",i + 1,getpid(), pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
int ret, i;
printf("main:pid is %d,tid is %lu\n",getpid(),pthread_self());
for(i = 0; i < 5; i++)
{
ret = pthread_create(&tid, NULL, tfun, (void*)&i);
if(ret != 0)
{
fprintf(stderr,"pthread_creat error: %s\n",strerror(ret));
exit(1);
}
}
sleep(i);
return 0;
}
结果:

两段代码的区别在于传参时的区别:正确的代码采用传值调用,直接将i的值传给回调函数,错误的代码采用传址调用,将i的地址传给回调函数。
**错误分析:**i变量保存在main函数的栈帧里,当将i的地址传递给回调函数时,回调函数的栈帧,也就是tfun这个函数的栈帧拿到的是i的地址,通过这个地址访问的是main函数栈帧的i值,此时main函数这个线程正在运行,i会发生变化,故会出错。

代码3,线程间共享全局变量测试:
cpp
int var = 100;
void *tfun(void *arg)//子线程的回调函数
{
var = 200;
printf("pthread:var = %d\n",var);
return NULL;
}
int main()
{
pthread_t tid;
int ret;
printf("before pthread var = %d\n",var);//线程之间共享全局变量
ret = pthread_create(&tid, NULL, tfun, NULL);//创建子线程
if(ret != 0)
{
fprintf(stderr,"pthread_create error: %s\n",strerror(ret));
exit(1);
}
sleep(1);//防止主线程先结束,进程地址空间销毁,子线程无法执行回调函数
printf("after pthread var = %d\n",var);
return 0;
}
结果:

2.3 pthread_exit()

作用:将单个线程退出,不销毁进程地址空间。
参数:retval表线程退出状态
另注意:pthread_exit返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在
线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
返回值:成功返回0 失败返回错误号。
exit、return、pthread_exit各自退出效果:
exit:表示退出当前进程
return:表示返回到调用者
pthread_exit:表示退出单个线程
代码:
cpp
void *tfun(void *arg)
{
int i = (int)arg;
sleep(i);
if(i == 2)
//exit(0);//表示退出当前进程
// return NULL;//表示返回到调用者
pthread_exit(NULL);//表示退出单个线程
printf("%d pthread:pid is %d,tid is %lu\n",i + 1,getpid(), pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
int ret, i;
printf("main:pid is %d,tid is %lu\n",getpid(),pthread_self());
for(i = 0; i < 5; i++)
{
ret = pthread_create(&tid, NULL, tfun, (void*)i);//传参采用值传递
//借助强转
if(ret != 0)
{
fprintf("pthread_create error: %s\n",strerror(ret));
exit(1);
}
}
sleep(i);
return 0;
}
exit结果:

return结果:

pthread_exit结果:

代码:
cpp
void *tfun(void *arg)//子线程的回调函数
{
printf("thread: pid is %d,tid is %lu\n",getpid(),pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
int ret;
printf("main : pid is %d,tid is %lu\n",getpid(),pthread_self());
ret = pthread_create(&tid, NULL, tfun, NULL);//创建子线程
if(ret != 0)
{
fprintf(stderr,"pthread_creat error: %s\n",strerror(ret));
exit(1);
}
//sleep(1);//防止主线程先结束,进程地址空间销毁
//子线程无法执行回调函数
//return 0;
// pthread_exit(NULL);
pthread_exit((void*)0);
}
2.4 pthread_join()

作用:阻塞等待线程退出,获取线程退出状态。
参数:thread表示线程ID,retval传出参数,存储线程结束状态。
注:若线程非正常死亡,例如被pthread_cancel函数取消,则retval所指向的位置
为-1(PTHREAD_CANCELED )(另外注意是retval所指向的位置为-1,并不是pthread_join
函数返回-1)
返回值:成功返回0 失败返回错误号。
对比记忆:
进程中:main返回值、exit 参数为 int 类型,则等待子进程结束 wait 函数 的参数类型为 int * 类型。
线程中:线程主函数返回值(pthread_create函数的第三个参数,是一个函数指针,返回值类型为void*)、pthread_exit 的参数为void * 类型,则等待线程结束 pthread_join 函数 的参数为 void ** 类型。
调用该函数的线程将挂起等待,直到id 为 thread 的线程终止。thread线程以不同的方法终止,通过 pthread_join 得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_cancel异常终止掉,++retval所指向的单元里存放的是常数PTHREAD_CANCELED(-1)++。
- 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
代码:
cpp
typedef struct stu{
int age;
char name[20];
}stu;
void *tfun(void *arg)
{
stu *s1;
s1 = malloc(sizeof(stu));
s1->age = 18;
strcpy(s1->name, "zhangsan");
return (void*)s1;
}
/*
void *tfun(void *arg)
{
stu s1;
s1.age = 18;
strcpy(s1.name, "zhangsan");
return (void*)&s1;//这样不行,函数调用结束,栈帧销毁
//返回不了局部变量
}
*/
int main()
{
pthread_t tid;
stu arg, *s;
int ret;
pthread_create(&tid, NULL, tfun, NULL);
ret = pthread_join(tid, (void**)&s);//获取子线程的退出状态/退出值
if(ret != 0)
{
fprintf(stderr,"pthread_join error: %s\n",strerror(ret));
exit(1);
}
printf("pthread_join finish\n");
printf("age = %d, name = %s\n",s->age,s->name);
free(s);
s=NULL;
pthread_exit(NULL);
}
结果:

使用valgrind工具查看其有无内存泄漏:

2.5 pthread_detach()

作用:设置线程分离,线程终止不能残留资源在内核中,线程自己主动释放资源(网络,多线程服
务器常用)
参数:thread表示指定线程的ID。
返回值:成功返回0 失败返回错误号。
**1、线程分离状态:**指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。
2、**僵尸进程:**进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。
3、也可使用 pthread_create函数参2(线程属性)来设置线程分离。
4、不能对一个已经处于detach状态 的线程调用pthread_join,++这调用的pthread_join将返回**22号errnoEINVAL(参数无效)**错误(++ 这里是pthread_join函数返回22,而不改变retval所指向的位置的值,与pthread_cancel函数对pthread_join函数的效果不一样++)。++也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
代码:
cpp
void *tfun(void *arg)//子线程的回调函数
{
printf("thread: pid is %d,tid is %lu\n",getpid(),pthread_self());
return NULL;
}
int main()
{
pthread_t tid;
int ret;
printf("main : pid is %d,tid is %lu\n",getpid(),pthread_self());
ret = pthread_create(&tid, NULL, tfun, NULL);//创建子线程
if(ret != 0)
{
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
ret = pthread_detach(tid);//线程分离,线程自动回收资源
if(ret != 0)
{
fprintf(stderr,"pthread_detach error:%s\n",strerror(ret));
exit(1);
}
sleep(1);
ret = pthread_join(tid,NULL);
printf("pthread_join ret is %d\n",ret);
if(ret != 0)
{
fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
exit(1);
}
pthread_exit(NULL);
}
结果:

2.6 pthread_cancel()

作用:杀死(取消)线程
参数:参数:thread表示指定线程的ID。
返回值:成功返回0 失败返回错误号。
注意:1、线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。
2、取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用
creat,open,pause,close,read,write..... 执行命令man 7 pthreads可以查看具备这
些取消点的系统调用列表。
3、可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过
调用 pthread_testcancel函数自行设置一个取消点。

4、被取消的线程,退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值
是 -1。可在头文件pthread.h中找到它的定义:#define PTHREAD_CANCELED ((void
*) -1)。 因此当我们对一个已经被取消的线程使用 pthread_join 回收时,得到的返回值
为-1。

代码1:
cpp
void *tfun(void *arg)//子线程的回调函数
{
while(1)
{
printf("thread: pid is %d,tid is %lu\n",getpid(),pthread_self());
sleep(1);
}
return (void*)666;
}
int main()
{
pthread_t tid;
int ret;
void *arg;
ret = pthread_create(&tid, NULL, tfun, NULL);//创建子线程
if(ret != 0)
{
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
pthread_cancel(tid);
ret = pthread_join(tid, &arg);
if(ret != 0)
{
fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
exit(1);
}
printf("pthread_join arg is %d\n",(int)arg);
printf("main : pid is %d,tid is %lu\n",getpid(),pthread_self());
pthread_exit(NULL);
}
结果:

代码2,三种终止线程方式的比较:
cpp
//三种终止线程方式的比较
void *tfun1(void *arg)//子线程的回调函数
{
printf("thread 1 return\n");
return (void*)111;
}
void *tfun2(void *arg)//子线程的回调函数
{
printf("thread 2 exit\n");
pthread_exit((void*)222);
}
void *tfun3(void *arg)//子线程的回调函数
{
while(1)
{
// printf("thread 3 cancel\n");
// sleep(1);
pthread_testcancel();//自己添加取消点
}
return (void*)666;
}
int main()
{
pthread_t tid;
int ret;
void *arg;
ret = pthread_create(&tid, NULL, tfun1, NULL);//创建子线程
ret = pthread_join(tid, &arg);
printf("1 pthread_join arg is %d\n",(int)arg);
ret = pthread_create(&tid, NULL, tfun2, NULL);//创建子线程
ret = pthread_join(tid, &arg);
printf("2 pthread_join arg is %d\n",(int)arg);
ret = pthread_create(&tid, NULL, tfun3, NULL);//创建子线程
sleep(3);
pthread_cancel(tid);
ret = pthread_join(tid, &arg);
printf("3 pthread_join arg is %d\n",(int)arg);//pthread_cancel杀死的
//线程返回-1,可用pthread_join
//接收
pthread_exit(NULL);
}
结果:

2.7 线程终止的三种方式
总结:终止某个线程而不终止整个进程,有三种方法:
- 从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用exit(结束当前进程)。
- 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
- 线程可以调用pthread_exit终止自己。
2.8 进程与线程控制原语对比
|--------|----------------|
| 进程 | 线程 |
| getpid | pthread_self |
| fork | pthread_create |
| wait | pthread_join |
| kill | pthread_cancle |
| exit | pthread_exit |