文章目录
- 一、前言
- 二、认识线程控制函数
- 1.线程创建
- 2.线程退出
- 3.线程等待
- 4.查看线程id
- 5.线程分离
- 6.综合demo
- 三、线程id本质是地址?
一、前言
上篇博客谈到,Linux并没有真线程,而是通过复用进程的数据结构来模拟实现线程的。因此 Linux 自然不会提供线程操作函数,只提供了创建轻量级级进程的系统接口,例如clone、vfork函数。
但是对于用户来说,创建一个线程还需要自己维护和管理,使用成本太高,于是有人基于Linux在应用层编写了 pthread库 用于方便创建和管理多线程。通过调用OS提供的创建轻量级进程的系统接口,为上层用户提供应用级的线程接口。
由于pthread不是C原生的库,在使用g++编译链接的时候需要带上 -lpthread
选项。不理解为什么要带上这个选项的同学,可以参见之前的博客:Linux基础IO(四):动静态库的制作与使用
二、认识线程控制函数
1.线程创建
[作用]: 线程创建
[参数说明]:
- thread:返回型参数。用于返回线程ID
- attr:设置线程的属性,attr为NULL表示使用默认属性,一般使用默认即可
- start_rountine:指定线程启动后要执行的函数。注意该函数的参数和返回值类型都是 void*
- arg:传递给线程的参数,注意要类型要强转为
void*
[返回值]: 成功返回0,失败返回错误码
我们在前一篇博客里提到,线程只执行进程的部分代码。这如何实现的呢?此时你就理解了:我们通过传入函数指针的方式,让一个线程只执行该函数的代码
2.线程退出
[作用]: 退出线程,并通过 retval
返回线程的退出状态
【作用】: 用于终止指定线程
使用 return
也可以终止线程,return的结果就作为线程的退出状态:
void* rouRoutine(void* arg)
{
// ……
return (void*)10;
}
3.线程等待
[作用]: 线程等待,获取线程的退出状态;回收资源,避免内存
[返回值]: 成功返回0,错误返回错误码
[参数说明]::
- thread:线程ID
- retval:返回性参数。当
retval
不为NULL时,返回指定线程的退出状态(不能获取退出信号)。由于线程退出状态为void*,因此需要用 void** 的指针去接收
[使用说明]: 如果指定线程已经被退出了,函数会立即返回;否则将会阻塞等待
void* rouRoutine(void* arg)
{
// ……
return (void*)10;
}
int main()
{
// ……
void* retval;
pthread_join(tid, &retval);
cout << (long int)retval << endl; ;
}
(说明:Linux系统为64位,指针大小为8字节。64位平台下,long int 也是8字节,因此我们可以将指针强转为long int输出打印线程的退出状态)
[问题一]: 线程出异常了怎么办?不用获取退出信息吗?
线程就是进程的一部分,一个线程出异常,整个进程都会退出,也就是说线程异常 = 进程异常。当线程发生异常时,退出信息由父进程获取。
4.查看线程id
[作用]: 返回调用线程的id
// 使用案例:
void* runRoutine(void* arg)
{
pthread_t tid = pthread_self();
printf("%p\n", tid);
return nullptr;
}
// 某一次的输出结果:0x7ffb5ccb2700
[问题二]:使用 ps -aL指令也可以查看线程id,两者有什么区别?
LWP
(light weight process - 轻量级进程) 对应的就是每个线程的标识符,类似于PID
,是个操作系统使用的,用于标识每个线程的唯一性。
使用 pthread_self() 函数查看到的用户级线程id,本质上是一个地址,是给用户操作使用的。至于为什么是地址,这里埋个伏笔,我们将在后文中具体阐述。
[问题三]:能不能编写代码查看LWP
可以使用函数gettid()
获取,但是该函数是个半成品,不能直接使用,必须通过系统调用来使用:int main() { pthread_t thread; pthread_create(&thread, nullptr, func, (void*)"1"); cout << syscall(SYS_gettid) << endl; pthread_join(thread, nullptr); }
5.线程分离
默认情况下,新创建的线程是 joinable 即可结合的。在线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成内存泄漏。但是当我们不关心线程的返回值时,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源 。使用下面的函数就可以实现线程分离,传入线程id即可使用。
[问题四]:分离后的线程是不能被等待的,但下面的代码为什么没有报错?
void* func(void* arg) { pthread_detach(pthread_self()); for(int i = 0; i < 5; i++) { cout << "我是子线程" << endl; } return nullptr; } int main() { pthread_t thread; pthread_create(&thread, nullptr, func, (void*)"thread 1"); int n = pthread_join(thread, nullptr); cout << strerror(n) << endl; }
哪个线程先执行由调度器决定,是不确定的。在子线程运行pthread_detach()
之前,主线程可能已经可能已经往下执行,开始阻塞式地等待线程退出了。如果在 join 之前sleep(1)
就能看到正确的看到错误。
借这个问题,我也想提醒大家:在分离线程的时候,尽可能在主线程中分离。
6.综合demo
void* startRoutine(void* arg)
{
char* name = static_cast<char*>(arg);
cout << name << " tid = " << pthread_self() << endl;
return (void*)1;
}
int main()
{
pthread_t tid1; // 线程分离
pthread_t tid2; // 线程不分离
void* retval;
pthread_create(&tid1, nullptr, startRoutine, (void*)"thread 1");
pthread_create(&tid2, nullptr, startRoutine, (void*)"thread 2");
pthread_detach(tid1);
pthread_join(tid2, &retval);
cout << (long int) retval << endl;
return 0;
}
三、线程id本质是地址?
线程是对进程空间资源的划分,具体是如何划分的呢,我们不难形成这样的认识:
- 代码区以函数的形式划分
- 全局数据各个线程之间共享
- 申请的堆空间对于其他线程来说是可见的
但是,线程具有独立的栈空间,那么如何理解线程的独立栈呢?我们在前言中提到,pthread库通过调用OS提供的创建轻量级进程的系统接口,为上层用户提供应用级的线程接口。
因此,线程的全部实现,并没有完全体现在OS内,OS只是提供了执行流,具体的线程结构是由库来进行管理。既然要管理线程,依照先描述后组织的设计思想,库需要设计出线程相应的数据结构来实现对线程的管理:
union pthread_attr_t
{
char __size[__SIZEOF_PTHREAD_ATTR_T]; // 私有栈
long int __align; // tid
};
动态库是加载到进程地址空间的共享区中的,因此我们每创建一个线程,库就在共享区中为我们创建一个线程结构体,并将线程结构体的起始地址返回供用户操作使用。因此pthread_t本质就是该结构体的起始地址。拿到线程的起始地址后,我们就可以访问结构体的各个成员了。