【Linux线程】线程控制

目录

[1. 线程创建](#1. 线程创建)

pthread_create

clone

[2. 获取线程ID](#2. 获取线程ID)

[pthread_create 函数的输出参数](#pthread_create 函数的输出参数)

pthread_self

[3. 等待线程退出](#3. 等待线程退出)

pthread_join

[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中轻量级进程是一一对应的;每个线程的大部分数据都是共享的,但线程也有自己的独立属性:

  1. 上下文

每个新线程的栈都在库里边维护,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 编译器提供的一个线程局部存储机制的关键字,用于声明线程局部变量,在编译期间就已经确定。


总结

以上便是本文的全部内容,希望对你有所帮助,感谢阅读!

相关推荐
Christal_pyy18 分钟前
树莓派4基于Debian GNU/Linux 12 (Bookworm)添加多个静态ipv4网络
linux·网络·debian
csbDD1 小时前
2025年网络安全(黑客技术)三个月自学手册
linux·网络·python·安全·web安全
专注VB编程开发20年3 小时前
除了 EasyXLS,加载和显示.xlsx 格式的excel表格,并支持单元格背景色、边框线颜色和粗细等格式化特性
c++·windows·excel·mfc·xlsx
Natsuagin3 小时前
轻松美化双系统启动界面与同步时间设置(Windows + Ubuntu)
linux·windows·ubuntu·grub
我们的五年4 小时前
【Linux网络编程】应用层协议HTTP(请求方法,状态码,重定向,cookie,session)
linux·网络·http
夏天的阳光吖4 小时前
C++蓝桥杯基础篇(四)
开发语言·c++·蓝桥杯
oioihoii4 小时前
C++17 中的 std::to_chars 和 std::from_chars:高效且安全的字符串转换工具
开发语言·c++
张胤尘4 小时前
C/C++ | 每日一练 (2)
c语言·c++·面试
強云5 小时前
23种设计模式 - 装饰器模式
c++·设计模式·装饰器模式
yatingliu20195 小时前
代码随想录算法训练营第六天| 242.有效的字母异位词 、349. 两个数组的交集、202. 快乐数 、1. 两数之和
c++·算法