【Linux】线程概念与控制(2)线程控制与核心概念

目录

[一 Linux线程控制](#一 Linux线程控制)

[1 POSIX线程库](#1 POSIX线程库)

[2 创建线程](#2 创建线程)

[3 线程终止](#3 线程终止)

[4 线程等待](#4 线程等待)

[5 线程分离](#5 线程分离)


一 Linux线程控制

1 POSIX线程库

Linux中使用线程必须使用POSIX线程库

Linux只会提供创建轻量级进程的接口,必然要把接口做封装,体现线程的概念;所以我们使用的是基于POSIX库的用户级线程

pthread库叫做原生线程库,只要是Linux系统就必须有pthread库

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以 pthread_ 打头的。

  • 要使用这些函数库,需要引入头文件 <pthread.h>

  • 链接这些线程函数库时要使用编译器命令的**-lpthread** 选项

2 创建线程

cpp 复制代码
功能:创建⼀个新的线程
原型:
 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;失败返回错误码

Linux中用户级线程:LWP=1:1

线程ID本质是一个虚拟地址

线程在库里要被管理-->先描述,再组织;用struct thread表示一个打开的线程

可以给线程传递字符串,那么也可以传类,结构体变量等等;相当于给它派任务

pthread_self:获取调用线程自己的ID

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>

// 定义线程名字缓冲区大小
const int gsize = 64;

// 线程入口函数
void *threadrun(void *args)
{
    // 转换参数类型,获取线程名字
    std::string name = static_cast<const char *>(args);
    
    while (true)
    {
        // 打印:用户层线程ID(tid)、进程ID(pid)、线程名字
        // 注意:%lx 对应 pthread_t (通常是unsigned long)
        printf("我是一个新线程: tid: 0x%lx, pid: %d, name : %s\n", 
               pthread_self(), getpid(), name.c_str());
        sleep(1); // 每隔1秒打印一次
    }
    
    return nullptr; // 线程退出
}

// 主函数
int main(int argc, char *argv[])
{
    // 检查命令行参数
    if (argc != 2)
    {
        std::cout << "用法: " << argv[0] << " <线程数量>" << std::endl;
        return 1;
    }

    // 将输入的字符串参数转换为整数
    int num = std::stoi(argv[1]);
    
    // 用于存储所有创建的线程ID
    std::vector<pthread_t> tids;
    
    // 批量创建线程
    for (int i = 0; i < num; i++)
    {
        pthread_t tid;
        
        // 创建线程名字缓冲区
        // 注意:这里声明在循环内,意味着每个线程都有自己独立的栈空间
        // 解决了线程共享栈内存导致的乱码问题
        char threadname[gsize]; 
        
        // 格式化线程名字 (如: thread-1, thread-2)
        snprintf(threadname, sizeof(threadname), "thread-%d", i + 1);
        
        // 创建线程
        // threadrun: 线程执行的函数
        // (void *)threadname: 传递给线程的参数 (线程名字)
        pthread_create(&tid, nullptr, threadrun, (void *)threadname);
        
        // 将新创建的线程ID存入vector
        tids.push_back(tid);
        
        // 此处sleep(1)是为了让线程有足够的时间启动并打印,防止主线程过快循环覆盖数据
        // 实际生产中通常不需要sleep,而是通过管理线程ID数组来等待
        sleep(1); 
    }

    // 主线程死循环(防止进程提前退出)
    // 因为子线程是分离状态吗?不,这里是join等待模式
    // 为了演示,我们让主线程一直挂起
    while (true) 
    {
        sleep(1);
    }

    return 0;
}

pthread_create 创建线程成功后,主线程会继续向下执行,新线程则执行对应的线程函数 threadrun

当我们在主线程的 for 循环中,用栈上的局部数组 char threadname[gsize] 存储线程名(如 thread-1),并将其作为参数传递给新线程时:

数组名传参本质是传递缓冲区的地址(如 0x12345678),而非拷贝字符串内容。

新线程的 args 参数会指向主线程栈上的这块缓冲区,而非拥有独立副本。

由于线程调度由操作系统的大 O1 调度算法决定,主线程和新线程的执行顺序完全不确定:

如果主线程执行速度更快,会在新线程读取缓冲区前,就用下一轮循环的 snprintf 把缓冲区内容覆盖为 thread-2;

最终导致新线程拿到的是被覆盖后的新名字,出现 "老线程拿到新线程名字" 的问题

threadname 这段缓冲区之所以会被所有线程共享,核心原因是:
同一进程内的所有线程共享同一个虚拟地址空间,原则上进程内的所有数据(包括主线程栈上的临时变量)都是共享资源,只要拿到地址就能访问。
当多个线程并发访问同一份共享资源,且一个线程的操作会影响另一个线程的执行结果时,这份代码就存在线程安全问题。

因为共享资源的访问,一个线程可能会影响另一个线程的指向,此时我们叫做这份代码具有线程安全问题

解决方案
方案 1:临时加 sleep(1)

在 pthread_create 后加 sleep(1),可以让主线程等待新线程启动并读取完缓冲区后,再执行下一轮循环,从而避免缓冲区被覆盖。但这只是临时规避,并非根本解决,且会影响程序性能,不适合生产环境。
方案 2:堆上分配独立空间

如果不想使用 sleep,可以在每次创建线程时,用 new 在堆上为每个线程分配独立的缓冲区:

cpp 复制代码
char* threadname = new char[gsize];

这样每个线程都拥有自己独立的堆内存空间,互不干扰,从根本上解决了栈缓冲区被覆盖的线程安全问题

3 线程终止

线程的返回值是void*,所以返回的时候,函数也需要通过return返回某种返回值

函数跑完,线程自然就结束,退出

退出方法1:调用return,表示线程退出

main中return 0 表示主线程退出,进程退出

exit是用来终止进程的,不能用来终止线程,多线程中任意一个线程调用exit,都表示整个进程退出

位置 语句 效果
线程函数(threadrun) return 只退出这个线程,其他线程继续跑
main 函数 return 退出主线程 ⇒ 整个进程退出
任意线程 exit() 整个进程退出(不能退出线程!)

退出方法2:库函数pthread_exit

cpp 复制代码
void pthread_exit(void *retval);

void *retval:表示线程退出的返回值,可以是 nullptr / 整数 / 指针;这个值会被 pthread_join 接收

在unbuntu 24.04下,pthread_exit只能用来终止新线程,终止主线程没用

线程处理函数,除了要交给他任务去处理,任务处理的怎么样,也要告诉主线程;所以线程有一个返回值

怎么拿到线程退出的返回值?

线程退出时 把返回值交出来

主线程用 pthread_join 去拿

拿出来后 强转类型 就能用

4 线程等待

主线程创建了新线程,也要能等待新线程

等待线程函数:pthread_join

cpp 复制代码
int pthread_join(pthread_t thread, void **retval);

pthread_t thread

作用:要等待的线程 ID

就是你 pthread_create 出来的那个 tid

void retval
作用: 用来接收线程退出的返回值

线程返回的值会通过这个参数写回来

不需要返回值 → 填 nullptr

pthread_join 第二个参数是一个输出型参数,用来拿线程的返回值!

(1)为什么要等?

如果主线程不等待新线程,就会造成类似僵尸进程的问题-->导致内存泄漏

通过等待,得到线程退出的退出信息

(2)如何等待?

主线程调用pthread_join;由第二个参数,得到线程的退出信息

cpp 复制代码
void *threadrun(void *args)
{
    // ... 线程执行逻辑 ...
    return (void*)10; // 线程退出,返回值 10
}
int main()
{
    pthread_t tid;
    // ... 创建线程 ...
    void *ret = nullptr;          // 定义接收返回值的变量
    pthread_join(tid, &ret);      // 调用 join,传入 ret 的地址 &ret
    printf("ret code: %lld\n", (long long)ret); // 强转后打印,输出 10
    return 0;
}

线程的返回值,就是return后面的数字:return (void*)10; 相当于把数字10写到指针变量里面

在pthread库里面,会单独有一个空间,是单独存在的,它会把当前线程结束时所对应的退出数据或void*类型变量。直接写道这个空间里面,一个线程一个,互相不影响

之后主线程想通过函数获取这个保存在库变量里面的值 ,这个值是void*类型的,把void*类型变量拿出来,就得定义void**(void类型没有办法定义变量,它的大小是不确定的,void*可以定义变量。因为它的类型是明确的,是指针,32位平台下是4字节,64位平台下是8字节)

Eq: int* p = (int*)20; 指针变量也是变量,指针和指针变量是完全不一样的(指针变量更强调它的空间)

两者是完全不同的东西:指针是地址,指针变量是存储地址的容器,就像「门牌号」和「写着门牌号的纸条」的区别。

cpp 复制代码
int a = 10;//这里强调它是变量,对应空间
int b = a;//强调它的数值是10

同一个变量,有时被当成左值(空间),有时是右值(数值(

cpp 复制代码
int* p=(int*)20;这里的p是指针变量
int* q=p;这里的p指针

怎样通过函数获取void*里面的内容?

传递二级指针

如果线程异常了,pthread_join异常无法正确返回,没有意义;因为任何线程异常,进程都会退出,所以pthread_join不关心异常

如果新线程不退出,join就会一直等待下去

退出方法3(不推荐):一个线程可以调用pthread_cancel终止同一进程的另一个线程

cpp 复制代码
功能:取消⼀个执⾏中的线程
原型:
 int pthread_cancel(pthread_t thread);
 
参数:
 thread:线程ID
 
返回值:成功返回0;失败返回错误码

如果一个线程是被取消的,那么它的退出码是-1

线程的返回值不一定是整数,而可以是类,结构体....

如果·我们未来不想join阻塞等待新线程呢?

5 线程分离

如何进行线程分离?

pthread_detach:让一个线程从主线程中分离

cpp 复制代码
int pthread_detach(pthread_t thread);

我们把线程创建出来,默认改线程是必须被join的,如果不join,就会造成类似僵尸进程的问题;按这种情况,线程的状态叫做joinable

如果一个线程创建出来必须要等待,那么这个线程就是joinable的;如果不想等待,也不关心线程的退出情况,那么可以把线程设置成分离状态

一个线程如果创建了,它也可以自己把自己设置成分离:

cpp 复制代码
pthread_detach(pthread_self());

线程分离的理解:

在七八十年代,父亲和儿子分家,可以由父亲向儿子提,也可以儿子向父亲提;体现在代码的层面上,可以新线程自己分离,也可以主线程分离新线程,主线程就不再关心新线程

joinable和分离式冲突的,一个线程不能既是joinable的也是分离的

相关推荐
格林威1 小时前
面阵相机 vs 线阵相机:堡盟与Basler选型差异全解析 +C++ 实战演示
开发语言·c++·人工智能·数码相机·计算机视觉·视觉检测·工业相机
白夜11171 小时前
C++(不适合使用 CRTP情况)
开发语言·c++·笔记
宁静致远20212 小时前
ARM 架构 Ubuntu 20.04 / 22.04 触摸屏设备
linux·c++·ubuntu
栗少2 小时前
Python 入门教程(面向有 Java 经验的开发者)
java·开发语言·python
草莓熊Lotso2 小时前
Linux C++ 高并发编程:从原理到手撕,线程池全链路深度解析
linux·运维·服务器·开发语言·数据库·c++·mysql
Gh0st_Lx2 小时前
【8】分类任务原理
算法·分类·数据挖掘
WolfGang0073212 小时前
代码随想录算法训练营 Day45 | 图论 part03
算法·图论
小王师傅662 小时前
【Java结构化梳理】泛型-上
java·开发语言