目录
[1. 线程创建](#1. 线程创建)
[2. 获取线程ID](#2. 获取线程ID)
[pthread_create 函数的输出参数](#pthread_create 函数的输出参数)
[3. 等待线程退出](#3. 等待线程退出)
[detached 模式](#detached 模式)
[4. 线程取消](#4. 线程取消)
前边以在Linux下为例,主要介绍了线程,及其相关知识的扩展,那么本文就来向大家介绍一些线程控制的接口;

1. 线程创建
在Linux中创建线程的方式有两种:pthread_create、clone;
pthread_create
pthread_create是 POSIX 线程库(pthread 库)提供的函数,用于创建一个新的线程。它是在用户空间层面操作线程,相对clone函数更易于使用和移植,在大多数 Linux 应用程序中,如果需要创建线程,通常会使用pthread_create;
注意:使用 POSIX 线程库时需要指定连接pthread库(-lpthread);
函数原型:
cpp
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
参数看起来很复杂,参数介绍:
- thread:是一个输出参数,用于返回新创建线程的 ID。
- attr:用于指定线程的属性,如线程的栈大小、调度策略等。如果为NULL,则使用默认属性(系统自动分配)。
- start_routine:是一个函数指针,指向新线程开始执行的函数。
- arg:是传递给start_routine函数的参数。
使用示例:
cpp
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
// 新线程执行的函数
void *thread_func(void *arg) {
printf("This is a new thread. Argument: %s\n", (char *)arg);
return NULL;
}
int main() {
pthread_t thread;
char *arg = "Hello World";
// 创建新线程
int ret = pthread_create(&thread, NULL, thread_func, arg);
if (ret!= 0) {
perror("pthread_create");
return 1;
}
// 等待新线程结束, 稍后进行解释
pthread_join(thread, NULL);
return 0;
}
clone
clone函数是 Linux 系统提供的一个底层系统调用,用于创建新的进程或线程,它的灵活性较高,可以通过参数来指定创建的是进程还是线程,以及共享哪些资源等;
函数原型:
cpp
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg)
- fn:是一个函数指针,指向新线程(或进程)开始执行的函数。
- child_stack:指定新线程(或进程)的栈空间地址。
- flags:是一组标志位,用于指定创建的行为和共享属性等。比如,CLONE_VM 表示父子进程(或线程)共享虚拟内存空间,CLONE_THREAD 表示创建的是线程而不是进程,与调用者共享线程组等。
- arg:是传递给fn函数的参数。
使用示例:
cpp
#include <stdio.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#define STACK_SIZE 1024*1024
// 新线程执行的函数
int thread_func(void *arg) {
printf("This is a new thread. Argument: %s\n", (char *)arg);
return 0;
}
int main() {
// 为新线程分配栈空间
void *stack = malloc(STACK_SIZE);
if (!stack) {
perror("malloc");
return 1;
}
// 创建新线程
int pid = clone(thread_func, (char *)stack + STACK_SIZE, CLONE_VM | CLONE_THREAD, "Hello World");
if (pid < 0) {
perror("clone");
free(stack);
return 1;
}
// 等待新线程结束
waitpid(pid, NULL, 0);
// 释放栈空间
free(stack);
return 0;
}
为什么是 (char *)stack + STACK_SIZE ?
栈空间是在堆上申请的空间,栈的生长方向和堆的是相反的(栈:从高地址向低地址);child_stack 参数需要指定新线程(或进程)栈空间的栈顶地址。使用 malloc 函数分配栈空间,malloc 返回的是栈空间的起始地址(低地址),而栈是从高地址向低地址生长的,所以新线程的栈顶应该是栈空间的最高地址;
在 Linux 系统中,线程被实现为轻量级进程(LWP,Lightweight Process)。从内核的角度来看,线程和进程并没有本质的区别,它们都由 task_struct 结构体来管理,都有自己独立的执行上下文(如寄存器状态、栈等)。不过,线程之间可以共享一些资源,比如地址空间、文件描述符等,这是通过 clone 函数的 flags 参数来控制的。如果不指定任何共享标志,clone 就相当于 fork,创建一个新的进程;
线程组
为了支持多线程编程,Linux 引入了线程组(Thread Group)的概念。同一个进程中的所有线程都属于同一个线程组,每个线程组有一个唯一的线程组 ID(TGID),通常等于主线程的 PID。getpid 函数返回的实际上是线程组 ID,而 gettid 函数返回的是线程自身的唯一 ID。
2. 获取线程ID
获取线程ID的方式有三种:2种POSIX线程库方式,一种系统调用的方式;这里只介绍线程库中的方式,感兴趣的可以私下了解一下系统调用方式获取线程ID;
pthread_create 函数的输出参数
使用 pthread_create 函数创建线程时,可以通过其第一个参数获取新创建线程的 pthread_t 类型的线程 ID,这里不再进行演示,可参考上文示例;
pthread_self
该函数用于获取当前线程的 pthread_t 类型的线程 ID。pthread_t 是一个不透明的数据类型,用于唯一标识一个线程。返回调用该函数的线程的 pthread_t 类型的线程 ID。
函数原型:
cpp
pthread_t pthread_self(void);
示例:
cpp
#include <stdio.h>
#include <pthread.h>
void* thread_function(void* arg) {
pthread_t tid = pthread_self();
printf("Thread ID (pthread_self): %lu\n", (unsigned long)tid);
return NULL;
}
int main() {
pthread_t thread;
int ret = pthread_create(&thread, NULL, thread_function, NULL);
if (ret != 0) {
perror("pthread_create");
return 1;
}
pthread_join(thread, NULL);
return 0;
}
3. 等待线程退出
线程默认是需要被等待的
线程退出没有等待,会导致类似进程的僵尸问题
线程退出时,主线程如何获取新线程的返回值?
pthread_join
功能:等待指定线程(阻塞等待),直到目标线程执行完毕并退出,同时还可以获取目标线程的返回值
函数原型:
cpp
int pthread_join(pthread_t thread, void **retval);
参数说明:
- thread:要等待的目标线程的 pthread_t 类型的线程 ID。
- retval:二级指针,用于存储目标线程的返回值。如果不需要获取返回值,可以将其设置为 NULL。
返回值:成功时返回 0,失败时返回错误码。
示例:
cpp
#include <stdio.h>
#include <pthread.h>
// 线程执行的函数
void* thread_function(void* arg) {
int* result = (int*)malloc(sizeof(int));
*result = 42;
return (void*)result;
}
int main() {
pthread_t thread;
int ret = pthread_create(&thread, NULL, thread_function, NULL);
if (ret != 0) {
perror("pthread_create");
return 1;
}
void* retval;
ret = pthread_join(thread, &retval);
if (ret != 0) {
perror("pthread_join");
return 1;
}
int* result = (int*)retval;
printf("Thread returned: %d\n", *result);
free(result);
return 0;
}
pthread_join 没有等待方式选项,默认是阻塞等待;线程通常有两种模式:joinable(可结合的)和 detached(分离的);默认线程模式为:joinable 该模式线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏;
对比进程的waitpid,线程并没有什么线程状态;主要原因:线程出异常就会使整个进程收到信号挂掉
detached 模式
pthread_join等待线程,调用线程需要阻塞的去等待指定线程退出,如果不想让调用阻塞等待怎么办?
detached 模式,使用该模式后新线程不需要被等待,执行结束自动释放资源;线程分离可以在主线程中选择分离的其他的线程,也可以在新线程中分离自己;
函数原型:
cpp
int pthread_detach(pthread_t thread);
参数:thread 是要标记为分离状态的线程的 pthread_t 类型的线程 ID
返回值:成功时返回 0,失败时返回错误码
使用示例:
cpp
#include <stdio.h>
#include <pthread.h>
// 线程执行的函数
void* thread_function(void* arg) {
printf("Thread is running.\n");
return NULL;
}
int main() {
pthread_t thread;
int ret = pthread_create(&thread, NULL, thread_function, NULL);
if (ret != 0) {
perror("pthread_create");
return 1;
}
ret = pthread_detach(thread);
if (ret != 0) {
perror("pthread_detach");
return 1;
}
// 主线程可以继续执行其他任务
printf("Main thread continues.\n");
// 让主线程休眠一段时间,确保子线程有机会执行
sleep(1);
return 0;
}
4. 线程取消
线程取消是指一个线程向另一个线程发送取消请求,被请求的线程并非会马上停止执行,而是在合适的时机才响应此请求并终止;
线程里能够响应取消请求的位置被叫做取消点,常见的取消点包含一些阻塞的系统调用,像 read、write、sleep 等。
相关接口:
cpp
#include <pthread.h>
int pthread_cancel(pthread_t thread);
参数:thread 是要取消的线程的标识符。
返回值:成功时返回 0,失败时返回错误码。
这里主要探究线程取消对线程的影响:
cpp
void* threadRoutine(void* arg)
{
int cnt = 5;
while (cnt--)
{
std::cout << "new thread is running" << endl;
sleep(1);
}
pthread_exit((void*)"thread-1 done");
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
// pthread_detach(tid);//分离线程
int n = pthread_cancel(tid);//取消线程
std::cout << "main thread cancel done"
<< " n: " << n << std::endl;
void* ret = nullptr;
n = pthread_join(tid, &ret);
// 线程被取消时,其返回值通常是 PTHREAD_CANCELED
//#define PTHREAD_CANCELED ((void *) -1)
std::cout << "main thread join done,"
<< " n: " << n << " thread return: " << (int64_t)ret << std::endl;
return 0;
}

新线程被取消后,主进程可以等待新线程,获取新线程的退出信息;(返回为-1表明线程是被取消的)
启动线程分离执行结果:

新线程被分离,它仍然可以被取消,但无法被主线程join获取退出信息;
5. pthread线程库
上述的 pthread_ 接口全部都不是系统直接提供的接口,而是线程库pthread提供的接口;

Linux中没有线程的概念,它是由进程模拟的线程;在用户创建5个进程时,OS就要有与之对应的5个的轻量级进程;Linux中的线程,一般称之为用户级线程; 所以用户看到的线程都是经过封装的;用户对线程的管理,就需要借助pthread库;因此线程库也需要的对这些轻量级进程进行管理;
线程库中的线程控制块与OS中轻量级进程是一一对应的;每个线程的大部分数据都是共享的,但线程也有自己的独立属性:
- 栈
- 上下文
每个新线程的栈都在库里边维护,pthread属于原生库,在Linux中进程使用线程时需要包含库,指定链接库的名字pthread库会被加载到进程的共享区;在共享区如何维护每个线程的栈?
可以这么理解:在pthread中会new一块新的的空间(堆区),在pthread库中使用指针维护;线程终止后再将维护的栈结构释放掉;默认地址空间中的栈在主线程中使用;
因为线程库就在用户空间,因此新线程所使用的栈空间都是由用户提供的;线程库是共享的,所以pthread库内部要管理整个系统中多个用户启动的所有线程;

在线程库中有类似与数组的结构,来存储不同的线程数据,如上图,如果有第二个线程,那就紧挨着第一个线程描述对象,再创建线程的这三种结构;可以理解为数组,对线程的管理就变成了对数组的管理;
前边我们提到线程的tid就是一个地址,pthread库为了便于快速找到某个线程,所以线程的id其实就是线程属性集合在库中的地址;
6. 扩展
6.1 __thread
在多线程中,全局变量是被所有线程共享的 ;
cpp
int g_val = 100; // 全局变量,所有线程共享的
// __thread int g_val = 100;
void *threadRoutine(void *args) {
std::string name = static_cast<const char *>(args);
while (true) {
sleep(1);
std::cout << name << ", g_val: " << g_val << ", &g_val: " << &g_val << "\n";
g_val++;
}
return nullptr;
}
int main() {
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
while (true) {
sleep(1);
std::cout << "main thread, g_val: " << g_val << ", &g_val: " << &g_val << "\n";
}
pthread_join(tid, nullptr);
}

全局变量被所有线程共享,访问到的是同一个变量;
添加__thread修饰:

每个线程各自一份g_val;两个线程访问到的变量地址不同了;
__thread 是 GCC 编译器提供的一个线程局部存储机制的关键字,用于声明线程局部变量,在编译期间就已经确定。
总结
以上便是本文的全部内容,希望对你有所帮助,感谢阅读!