线程控制 下

线程

线程的优点

与进程之间的切换相⽐,线程之间的切换需要操作系统做的⼯作要少很多

最主要的区别**是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。**这两种上下⽂切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

另外⼀个隐藏的损耗是上下⽂的切换会扰乱处理器的缓存机制。简单的说,⼀旦去切换上下⽂,处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚拟内存空间的时候,处理的⻚表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在⼀段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。

进程切换会导致TLB和Cache失效,下次运行需要重新缓存。

线程的缺点

性能损失

⼀个很少被外部事件阻塞的计算密集型线程往往⽆法与其它线程共享同⼀个处理器。如果计算密集型线程的数量⽐可⽤的处理器多,那么可能会有较⼤的性能损失,这⾥的性能损失指的是增加了额外的同步和调度开销,⽽可⽤的资源不变。

健壮性降低

编写多线程需要更全⾯更深⼊的考虑,在⼀个多线程程序⾥,因时间分配上的细微偏差或者因共享了不该共享的变量⽽造成不良影响的可能性是很⼤的,换句话说线程之间是缺乏保护的。

缺乏访问控制

进程是访问控制的基本粒度,在⼀个线程中调⽤某些OS函数会对整个进程造成影响。

编程难度提⾼

编写与调试⼀个多线程程序⽐单线程程序困难得多。

Linux进程VS线程 -- 哪些资源共享,哪些独占

进程和线程

进程是资源分配的基本单位

线程是调度的基本单位•

线程共享进程数据,但也拥有⾃⼰的⼀部分"私有"数据:

线程ID

⼀组寄存器,线程的上下⽂数据,背后就是线程的独立调度。

栈。独立结构,线程是一个动态的概念。

errno

信号屏蔽字

调度优先级

linux线程调度

LWP light weight process 轻量级进程

CPU调度的时候看的不是pid,而是lwp。

线程异常,会导致整个进程崩溃。也就是健壮性降低。

Linux系统不存在真正意义上的线程,所谓的概念,使用轻量级进程模拟的,但OS中只有轻量级进程,所谓模拟线程只是我们的说法。

user只认线程,pthread库把创建的轻量级进程封装起来,给用户提供一批创建线程的接口。linux线程的实现是在用户层,称之为用户级线程。

语言的一致性

linux线程控制接口

线程创建

功能:创建⼀个新的线程

原型:

复制代码
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;失败返回错误码

错误检查:

传统的⼀些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指⽰错误。

pthreads函数出错时不会设置全局变量errno(⽽⼤部分其他POSIX函数会这样做)。⽽是将错误代码通过返回值返回

pthreads同样也提供了线程内的errno变量,以⽀持其它使⽤errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要⽐读取线程内的errno变量的开销更⼩

线程终⽌

如果需要只终⽌某个线程⽽不终⽌整个进程,可以有三种⽅法:

  1. 从线程函数return。这种⽅法对主线程不适⽤,从main函数return相当于调⽤exit。

  2. 线程可以调⽤pthread_ exit终⽌⾃⼰。

  3. ⼀个线程可以调⽤pthread_ cancel终⽌同⼀进程中的另⼀个线程。

功能:线程终⽌

原型:

复制代码
void pthread_exit(void *value_ptr);

参数:

value_ptr:value_ptr不要指向⼀个局部变量。

返回值:

⽆返回值,跟进程⼀样,线程结束的时候⽆法返回到它的调⽤者(⾃⾝)

注意: 线程不能用exit()终止,因为exit是终止进程的!!!!!

线程等待

为什么需要线程等待?

已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。

创建新的线程不会复⽤刚才退出线程的地址空间。

线程创建好之后, 新线程要被主线程等待,类似僵尸进程的问题,内存泄漏。

功能:等待线程结束

原型

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

参数:

thread:线程ID

value_ptr:它指向⼀个指针,后者指向线程的返回值

返回值:成功返回0;失败返回错误码

调⽤该函数的线程将挂起等待,直到id为thread的线程终⽌。thread线程以不同的⽅法终⽌,通过pthread_join得到的终⽌状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元⾥存放的是thread线程函数的返回值。

  2. 如果thread线程被别的线程调⽤pthread_ cancel异常终掉,value_ ptr所指向的单元⾥存放的是常数PTHREAD_ CANCELED。

  3. 如果thread线程是⾃⼰调⽤pthread_exit终⽌的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

  4. 如果对thread线程的终⽌状态不感兴趣,可以传NULL给value_ ptr参数。

代码示例:

复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h> // sleep/usleep 头文件
using namespace std;

// 你的 Task 和 Result 类
class Task
{
public:
    Task(int a, int b) : _a(a), _b(b) {}
    int Execute() { return _a + _b; }
    ~Task() {}

private:
    int _a;
    int _b;
};

class Result
{
public:
    Result(int result) : _result(result) {}
    int GetResult() { return _result; }
    ~Result() {}

private:
    int _result;
};

// 线程入口函数
void *routine(void *args)
{
    Task *t = static_cast<Task*>(args);
    usleep(100000); // 改为100毫秒,替代sleep(100),方便测试
    Result *res = new Result(t->Execute());
    usleep(100000); // 100毫秒
    return res;
}

int main()
{
    // 1. 创建任务对象
    Task task(10, 20);

    // 2. 创建线程:传入task指针作为参数
    pthread_t tid;
    int ret = pthread_create(&tid, nullptr, routine, &task);
    if (ret != 0)
    {
        cerr << "线程创建失败!" << endl;
        return -1;
    }

    // 3. 等待线程结束,并获取返回值
    void *thread_ret;
    ret = pthread_join(tid, &thread_ret);
    if (ret != 0)
    {
        cerr << "等待线程失败!" << endl;
        return -1;
    }

    // 4. 解析返回值:转为Result*,获取结果
    Result *res = static_cast<Result*>(thread_ret);
    cout << "任务执行结果:" << res->GetResult() << endl; // 输出:30

    // 5. 释放堆内存(必须手动释放,避免内存泄漏)
    delete res;

    return 0;
}

函数签名:void *routine(void *args)

这是 POSIX 线程(pthread)的强制要求,不能改:

返回值 void*:线程执行完后,可以返回任意类型的指针给主线程;

参数 void* args:主线程可以给子线程传递任意类型的参数(通过 pthread_create 传);

为什么是 void*?------ 通用性:可以接收 / 返回任意类型的指针(比如 Task*Result*int* 等)。

Task *t = static_cast<Task*>(args);

作用 :把主线程传入的 void* 类型参数,转回原本的 Task* 类型;

为什么需要转换? 主线程调用 pthread_create 创建线程时,只能传 void* 类型的参数(比如 &tasknew Task(10,20)),所以子线程里必须转回来才能调用 TaskExecute() 方法;

Result *res = new Result(t->Execute());

第一步:t->Execute()

调用 TaskExecute() 方法,执行任务逻辑(比如 10+20=30),返回 int 类型的结果;因为 tTask* 类型,所以用 -> 访问成员函数(如果是对象则用 .)。

第二步:new Result(...)

为什么用 new(堆内存)? 错误写法:Result res(t->Execute()); return &res;如果在栈上创建 res,线程退出时栈空间会被操作系统回收,返回的 &res 会变成野指针 (指向已释放的内存),主线程使用这个指针会崩溃;正确写法:new Result(...)堆内存的生命周期不受线程退出影响,只要不手动 delete,内存就一直有效,主线程能安全接收。

返回值new 会返回堆上 Result 对象的指针,赋值给 resResult* 类型)。

分离线程

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进⾏pthread_join操作,否则⽆法释放资源,从⽽造成系统泄漏。

如果不关⼼线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,⾃动释放线程资源。

int pthread_detach(pthread_t thread);

分离的线程,依旧在进程的地址空间中,进程的所有资源,被分离的线程,依旧可以访问,可以操作。

线程ID及进程地址空间布局

Linux没有真正的线程,OS提供的接口,不会直接提供线程接口,在用户层封装轻量级进程,形成原生线程库 动态库->ELF的库。

线程的概念是在库中维护的,在库内部就一定会存在多个创建好的线程。我的可执行程序加载形成进程,动态链接和动态地址重定向要将动态库加载到内存并且映射到当前进程的地址空间中。

相关推荐
Howrun7772 小时前
C++ 类间交互
开发语言·c++
2401_857683542 小时前
C++代码静态检测
开发语言·c++·算法
岳轩子2 小时前
JVM 运行时数据区域详解 第三节
jvm
时艰.2 小时前
JVM 垃圾收集器(G1&ZGC)
java·jvm·算法
2401_838472512 小时前
内存泄漏自动检测系统
开发语言·c++·算法
开发者小天2 小时前
python中的class类
开发语言·python
2501_933329552 小时前
Infoseek数字公关AI中台技术解析:如何构建企业级舆情监测与智能处置系统
开发语言·人工智能
m0_706653232 小时前
基于C++的爬虫框架
开发语言·c++·算法
梵刹古音2 小时前
【C语言】 数据类型的分类
c语言·开发语言