Linux线程控制
POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是“pthread_”打头的
- 要使用这些函数库,要通过引入头文
<pthread.h>
- 链接这些线程函数库时要使用编译器命令的
“-lpthread”
选项
创建线程
功能:创建一个新的线程
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性。
start_routine:是个函数地址,线程启动后要执行的函数。
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。
- pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小。
如下代码演示:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg) {
int i;
for( ; ; ) {
printf("I'am thread 1\n");
sleep(1);
}
}
int main( void )
{
pthread_t tid;
int ret;
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
int i;
for(; ; ) {
printf("I'am main thread\n");
sleep(1);
}
}
运行代码后可以看到,新线程每隔一秒执行一次打印操作,而主线程每隔两秒执行一次打印操作。
当我们用
ps axj
命令查看当前进程的信息时,虽然此时该进程中有两个线程,但是我们看到的进程只有一个,因为这两个线程都是属于同一个进程的。
其中,LWP(Light Weight Process)
就是轻量级进程的ID,可以看到显示的两个轻量级进程的PID是相同的,因为它们属于同一个进程。
注意: 在Linux中,应用层的线程与内核的LWP是映射关系的,实际上操作系统调度的时候采用的是LWP,而并非PID,只不过我们之前接触到的都是单线程进程,其PID和LWP是相等的,所以对于单线程进程来说,调度时采用PID和LWP是一样的。
线程ID及进程地址空间布局
pthread_ create
函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。- 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_ create
函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL
线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。- 线程库NPTL提供了
pthread_ self
函数,可以获得线程自身的ID:
获取线程ID
常见获取线程ID的方式有两种:
- 创建线程时通过输出型参数获得。
- 通过调用pthread_self函数获得。
pthread_t pthread_self(void);
如下代码演示:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg) {
int i;
for( ; ; ) {
printf("I'am thread 1,pthread ID:%lu\n",pthread_self());
sleep(1);
}
}
int main( void )
{
pthread_t tid;
int ret;
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
int i;
for(; ; ) {
printf("I'am main thread ,thread ID:%lu\n",pthread_self());
sleep(1);
}
}
运行结果:
注意: 用pthread_self函数获得的线程ID与内核的LWP的值是不相等的,pthread_self函数获得的是用户级原生线程库的线程ID,而LWP是内核的轻量级进程ID,它们之间是映射的关系。
简单理解用户级线程ID与内核LWP ID
pthread_t
到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t
类型的线程ID,本质就是一个进程地址空间上的一个地址。
首先,Linux不提供真正的线程,只提供轻量级进程,也就意味着操作系统只需要对内核执行流LWP进行管理,而供用户使用的线程接口等其他数据,应该由线程库自己来管理,因此管理线程时的“先描述,再组织”就应该在线程库里进行。
通过ldd
命令可以看到,我们采用的线程库实际上是一个动态库。
如下图:
进程运行时,线程库被加载到内存里,通过页表映射到进程地址空间的共享区里,这时候进程内的每个线程都共享该线程库了。
如下图:
每个线程都有自己私有的栈,其中主线程采用的栈是进程地址空间中原生的栈(该栈用于main函数内,main函数的回调函数所使用),而其余线程采用的栈就是在共享区中开辟的。除此之外,在用户层我们需要对线程进行描述,因此每个线程都有自己的struct pthread结构体,当中包含了对应线程的各种属性;每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据。
每一个新线程在共享区都有这样一块区域对其进行描述,因此我们要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息。
如下图:
因为Linux没有给我们提供线程的接口,而只是提供轻量级进程的接口,在线程库里我们对内核的LWP进行封装来模拟线程,在线程库里我们要有对应的LWP的ID,我们通过ps -aL
指令可以找到LWP的 ID,LWP的ID用来表示 LWP唯一性。线程库进行操作时,只需要通过LWP的ID就能通过ID来调用LWP的接口。
线程等待
为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内,造成内存泄漏(类似于进程等待)。
- 创建新的线程不会复用刚才退出线程的地址空间。
pthread_join()函数等待线程
功能:等待线程结束
原型:
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1、从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit()
。
2、线程可以调用pthread_ exit()
终止自己。
3、一个线程可以调用pthread_ cancel()
终止同一进程中的另一个线程。
调用pthread_join
函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join()
得到的终止状态是不同的,总结如下:
说明一下:value_ptr:pthread_join函数的第二参数,用来获取线程终止结果。
1、如果thread线程通过return返回,value_ ptr
所指向的单元里存放的是thread线程函数的返回值。
2、如果thread线程被别的线程调用pthread_ cancel
异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED
(-1)。
3、 如果thread线程是自己调用pthread_exit
终止的,value_ptr
所指向的单元存放的是传给pthread_exit
的参数。
4、如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr
参数。
下面我们用代码演示线程的三种退出情况:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
// return 返回
void *thread1(void *arg) {
return (void*)123;
}
// pthread_exit() 退出
void *thread2(void *arg) {
pthread_exit((void*)123);
}
// pthread_cancel() 取消
void *thread3(void *arg) {
while(true){sleep(1);}
}
int main( void )
{
pthread_t tid1,tid2,tid3;
void * ret=nullptr;
pthread_create(&tid1, NULL, thread1, NULL);
pthread_create(&tid2, NULL, thread2, NULL);
pthread_create(&tid3, NULL, thread3, NULL);
//int pthread_join(pthread_t thread, void **value_ptr);
sleep(3);
pthread_cancel(tid3);
pthread_join(tid1,&ret);
printf("thread1 ret=%d\n",(int*)ret);//123
pthread_join(tid2,&ret);
printf("thread2 ret=%d\n",(int*)ret);//123
pthread_join(tid3,&ret);
printf("thread3 ret=%d\n",(int*)ret);//-1
printf("%d\n",PTHREAD_CANCELED);//-1
return 0;
}
线程分离
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
函数原型:
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
注意:
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。线程分离后,等待分离线程会失败pthread_wait()返回错误码。
如下代码演示:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *thread(void *arg)
{
pthread_detach(pthread_self());
return (void *)123;
}
#define NUM 3
int main(void)
{
pthread_t tid[NUM];
void *ret = (void*)66;
for (int i = 0; i < NUM; i++)
{
pthread_create(&tid[i], NULL, thread, NULL);
}
sleep(1);
for (int i = 0; i < NUM; i++)
{
int retval_pthread_join = pthread_join(tid[i], &ret);
printf("thread %d的ret=%d retval_pthread_join=%d\n", i, ret,retval_pthread_join);
}
return 0;
}
运行结果: