文章目录
0. 前言
对于Linux多线程,本文介绍POSIX库以及相关接口函数的使用,在不同的部分有相应的接口使用,以及分离线程的意义和接口函数。
1. POSIX线程库
POSIX线程库( pthreads) 提供了一套用于多线程编程的接口。它包括线程创建、管理、同步等功能。
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”开头的
- 使用这些函数库,要引入头文件<pthread.h>
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项
下面将介绍PISIX线程库中的相关接口函数:
1.1 创建线程
pthread_create()
- 功能:创建一个新的线程
- 原型
cpp int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
- 参数:
thread
: 返回线程IDattr
: 设置线程的属性,attr为NULL表示使用默认属性start_routine
: 是个函数地址,线程启动后要执行的函数arg
: 传给线程启动函数的参数- 返回值:成功返回0;失败返回错误
错误检查:
- 传统的一些函数是,执行成功返回0,失败返回-1,并对全局变量errno赋值以指示错误。
pthreads
函数出错时不设置全局变量errno(大部分其他POSIX函数会设置);而是将错误代码通过返回值返回;pthreads
同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,一般通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小。
下面是一段示例代码:
#include <stdio.h>
#include <pthread.h>
void *thread_function(void *arg) {
printf("Hello from the thread! Argument: %d\n", *(int *)arg);
return NULL;
}
int main() {
pthread_t thread;
int arg = 42;
// 创建新线程
if (pthread_create(&thread, NULL, thread_function, &arg) != 0) {
perror("pthread_create failed");
return 1;
}
// 等待线程结束
pthread_join(thread, NULL);
return 0;
}
1.2 进程ID和线程ID
- Linux目前的线程实现是 Native POSIX Thread Libaray(NPTL)
- 在这种实现下,线程又被称为轻量级进程(Light Weighted Process), 每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
- 线程存在前,一个进程对应内核里的一个进程描述符,对应一个进程ID。
- 引入线程后,一个用户进程下管理N个用户态线程,每个线程作为独立的调度实体在内核态都有自身的进程描述符,进程和内核的描述符变成了1:N关系,
- POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题呢? —— 线程组
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};
① 线程组
- 多线程的进程,又被称为线程组;
- 线程组内的每个线程在内核中都存在一个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实对应的是线程ID;进程描述符中的tgid(Thread Group ID),该值对应的是用户层面的进程ID
和进程ID一样,线程ID是pid_t类型的变量,而且是用来唯一标识线程的一个整型变量。
如何查看一个线程的ID?
- 使用
ps
命令
ps 命令可以列出系统中的进程和线程。使用 ps 命令加上适当的选项可以显示线程ID。
ps -eLf
-e
:显示所有进程-L
:显示线程-f
:显示完整格式
输出中的 LWP 列显示线程ID,PID 列显示进程ID。
如下图:
PID LWP TTY STAT TIME COMMAND
1000 1000 ? Ss 0:00 /sbin/init
1000 1001 ? S 0:00 \_ /sbin/init
1000 1002 ? S 0:00 \_ /sbin/init
1010 1010 ? Ss 0:00 /usr/sbin/sshd
1010 1011 ? S 0:00 \_ /usr/sbin/sshd
1020 1020 ? Ss 0:00 /usr/bin/python3
1020 1021 ? S 0:00 \_ /usr/bin/python3
1020 1022 ? S 0:00 \_ /usr/bin/python3
- 函数调用
Linux提供了gettid系统调用来返回其线程ID,可是glibc并没有将该系统调用封装起来,在开放接口来共程序员使用。如果确实需要获得线程ID,可以使用syscall
#include <sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid)
线程组内的第一个线程,在用户态被称为主线程(main thread); 在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID的值设置成第一个线程的线程ID,group_leader指针则指向自身,既主线程的进程描述符。
线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。
/* 线程组ID等于线程ID,group_leader指向自身 */
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);
线程组内的第一个线程,在用户态被称为主线程(main thread),其线程组ID总是和主线程的线程组ID一致,无论是主线程直接创建线程,还是创建出来的线程再次创建线程,都如此。
if ( clone_flags & CLONE_THREAD )
p->tgid = current->tgid;
if ( clone_flags & CLONE_THREAD ) {
P->group_lead = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}
需要注意的是: 线程和进程不一样,进程有父进程的概念,但在线程组里面,所有的线程都是对等关系
1.3 线程ID 与 进程地址空间布局
pthread_ create
函数会产生一个线程ID,存放在第一个参数指向的地址中;该线程ID和上文的id不同。- 上文的线程ID属于进程调度的范畴。由于线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
- pthread_ create函数第一个参数指向一个虚拟内存单元
- 该内存单元的地址为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作就是根据该线程ID来操作的。
- 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID
pthread_t 的类型取决于其实现。根据NPTL实现, pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址
1.4 线程终止
如果需要终止某个线程而不终止整个进程,可以有三种方法:
- 线程函数中return:这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程调用pthread_ exit终止自己。
- 线程调用pthread_ cancel终止同一进程中的另一个线程
下面简单介绍这两个函数:
pthread_exit()
- 功能:线程终止
- 原型
cpp void pthread_exit(void *value_ptr);
- 参数
value_ptr
: value_ptr不要指向一个局部变量。- 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了
pthread_cancel()
功能:取消一个执行中的线程
- 原型
cpp int pthread_cancel(pthread_t thread);
- 参数
thread
:线程ID- 返回值:成功返回0;失败返回错误码
1.5 线程等待 为什么需要线程等待
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间
pthread_join()
- 功能:等待线程结束
- 原型
cpp int pthread_join(pthread_t thread, void **value_ptr);
- 参数
thread
:线程IDvalue_ptr
:它指向一个指针,后者指向线程的返回值- 返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。
thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数
#include <iostream>
#include <pthread.h>
#include <unistd.h> // For sleep function
// 线程函数
void* threadFunction(void* arg) {
int* threadId = static_cast<int*>(arg);
std::cout << "Thread " << *threadId << " is running." << std::endl;
sleep(2); // 模拟长时间运行的任务
std::cout << "Thread " << *threadId << " is done." << std::endl;
return nullptr;
}
int main() {
const int numThreads = 3;
pthread_t threads[numThreads];
int threadIds[numThreads];
// 创建线程
for (int i = 0; i < numThreads; ++i) {
threadIds[i] = i + 1;
int ret = pthread_create(&threads[i], nullptr, threadFunction, &threadIds[i]);
if (ret != 0) {
std::cerr << "Error creating thread " << i << ": " << ret << std::endl;
return 1;
}
}
// 等待所有线程完成
for (int i = 0; i < numThreads; ++i) {
int ret = pthread_join(threads[i], nullptr);
if (ret != 0) {
std::cerr << "Error joining thread " << i << ": " << ret << std::endl;
return 1;
}
}
std::cout << "All threads have finished." << std::endl;
return 0;
}
执行上面的代码,会有如下结果:
Thread 1 is running.
Thread 2 is running.
Thread 3 is running.
Thread 1 is done.
Thread 2 is done.
Thread 3 is done.
All threads have finished.
2. 分离线程 pthread_detach
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,可以告知系统,当线程退出时,自动释放线程资源。
pthread_detach
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
int pthread_detach(pthread_self());
int pthread_detach(pthread_t thread);
joinable和分离是冲突的,一个线程不能既是joinable又是分离的
示例代码:
#include <iostream>
#include <pthread.h>
#include <unistd.h> // For sleep function
// 线程函数
void* threadFunction(void* arg) {
int* threadId = static_cast<int*>(arg);
std::cout << "Thread " << *threadId << " is running." << std::endl;
sleep(2); // 模拟长时间运行的任务
std::cout << "Thread " << *threadId << " is done." << std::endl;
return nullptr;
}
int main() {
const int numThreads = 2;
pthread_t threads[numThreads];
int threadIds[numThreads];
// 创建线程
for (int i = 0; i < numThreads; ++i) {
threadIds[i] = i + 1;
int ret = pthread_create(&threads[i], nullptr, threadFunction, &threadIds[i]);
if (ret != 0) {
std::cerr << "Error creating thread " << i << ": " << ret << std::endl;
return 1;
}
// 分别使线程在完成后自动清理资源
if (i % 2 == 0) { // 对偶数线程调用 pthread_detach
pthread_detach(threads[i]);
std::cout << "Thread " << threadIds[i] << " is detached." << std::endl;
}
}
// 等待其中一个线程完成
int ret = pthread_join(threads[1], nullptr); // 只等待线程 2 完成
if (ret != 0) {
std::cerr << "Error joining thread 1: " << ret << std::endl;
return 1;
}
std::cout << "Main thread has joined thread 2." << std::endl;
// 等待一段时间以确保所有线程输出完成
sleep(3);
std::cout << "Main thread is done." << std::endl;
return 0;
}
说明:
线程函数 (
threadFunction
):- 打印线程的开始和结束信息,并使用
sleep
模拟长时间运行的任务。
- 打印线程的开始和结束信息,并使用
主函数 (
main
):- 创建两个线程。
- 对偶数线程调用
pthread_detach
,使这些线程在完成后自动清理资源。 - 只对第二个线程调用
pthread_join
,以等待其完成。 - 程序在主线程结束前等待一段时间,确保所有线程的输出都能被看到。
运行结果:
Thread 1 is running.
Thread 2 is running.
Thread 1 is detached.
Thread 2 is done.
Main thread has joined thread 2.
Thread 1 is done.
Main thread is done.
对于这段代码,线程 1 被设置为分离状态(detached),因此主线程不需要调用 pthread_join
来等待它完成。主线程只等待线程 2 完成。线程 1 和线程 2 的输出顺序可能会有所不同,具体取决于调度。