线程 - 线程优缺点、线程自有和共享的数据、多线程使用公共空间、线程分离、线程库对线程的管理

文章目录

  • 一、线程的优点
    • 1. 创建的代价
    • 2. 切换的代价
      • 缓存和进程/线程切换
    • 3. 占用的资源
    • 4. 效率
  • 二、线程的缺点
    • 1. 性能损失
    • 2. 健壮性降低
    • 3. 缺乏访问控制
    • 4. 编程难度高
  • 三、线程分离
    • 1. 线程分离
    • 2. pthread_detach ()
      • ① 函数细节
      • ② 函数使用
  • 四、线程自有和共享的数据
    • 1. 线程自有的数据
    • 2. 线程共享的资源
  • 五、多个线程使用公共空间
    • 1. 不要把公共空间传给多个线程
      • 1.1 问题
      • 1. 2 解决
    • 2. 线程的传参和返回值,可以是各种对象
  • 六、C++11 中的线程
    • 1. C++11 的多线程
    • 2. thread 类及使用
  • 七、线程库对线程的管理
    • 1. 线程的管理由谁来做?
    • 2. 线程管理的细节
      • ① 线程库首先要映射到当前进程的地址空间中
      • ② 进程地址空间中的动态线程库
      • ③ 整个系统的所有的线程都在这一个库中管理着
    • 3. 执行流怎么找到栈的?
  • 八、线程的封装
    • 1. Pthread.hpp
    • 2. test.cc

一、线程的优点

1. 创建的代价

创建一个新线程的代价要比创建一个新进程小的多。


创建线程只需要创建一个新的 PCB,然后将曾经进程的资源指派给线程即可;而进程创建的时空成本是较高的,有各种数据结构的创建,数据的加载。

2. 切换的代价

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


缓存和进程/线程切换

  • CPU内,每次读取当前进程的代码和数据时,都需要经过虚拟到物理的转换,然后得到内存中的数据放到 CPU 中做处理,如果这样,那 CPU 读取任何一条指令,都要访问内存,为了提高运行效率,CPU 中存在一种硬件 cache(高速缓冲存储器),在 CPU 进行虚拟到物理的寻址时,本来是找到一行代码,较大概率会执行下一行(当然也可能会跳转到别处),所以会将周边数据全读到 CPU 内部缓存起来,所以,之后 CPU 再访问代码数据时,不用再去读取内存了,而是从 cache 中再读取,大大提高 CPU 寻址的效率。
    在这里插入图片描述
  • 进程切换和线程切换:
    • 进程切换:
      在这里插入图片描述
      之前缓存的数据,就全都没有了,会重新加载新进程的数据

    • 线程切换:
      在这里插入图片描述
      cache 中的数据在概率上依旧能用,不用重新清空和重新加载。

所以这才是线程成本低的主要原因。

3. 占用的资源

线程占用的资源要比进程少很多。


进程要占用多执行流,地址空间,页表,代码数据;而线程占用的都是一份的。

4. 效率

  • 计算密集型应用:
    排序、查找、加密、解密、压缩等动作都以计算为主,是计算密集型应用,主要应用的 CPU 的资源。
  • I/O 密集型应用:
    下载、上传、拷贝等就是 I/O 密集型应用。

应用要不就是计算密集型,要不就是 I/O 密集型,要不就是两者都是。

  • 对于计算密集型应用:
    • 为了能在多处理器系统上运行,将计算分解到多个线程中实现。一个线程处理一部分,效率会高。
    • 当然也不是创建的线程越多越好,因为会有切换的成本,切换太多,说不定效率还不如一个进程从头算到尾。
    • 所以,对于计算密集型,一般是 CPU 有多少个核就创建几个。
      可以通过 lscpu 查看一下核数:在这里插入图片描述
  • 对于I/O 密集型应用:
    • 可以多创建进程,因为 I/O 过程大部分都在等,一个进程等 10G的数据,和 10 个线程每个等 1G 的数据,效率不一样。

二、线程的缺点

1. 性能损失

多线程切换有成本,像上面的计算密集型应用,线程过多可能会有较大的性能损失(增加了额外的同步和调度开销,而可用资源不变)。

2. 健壮性降低

一个线程出问题,整个进程就挂掉了(这一点,线程 - 线程退出 中有讲到)。
在一个多线程程序里,因共享了不该共享的变量而造成不良影响的可能性很大。

3. 缺乏访问控制

线程间共享资源,对于共享资源,大家都可以任意时间访问,可能会有同时访问同一量的情况,可能会出错。

4. 编程难度高

编写与调试一个多线程程序比单线程程序要困难很多,往往需要全面深入的考虑以保证程序的健壮性。

三、线程分离

1. 线程分离

线程默认是 joinable(可联接) 的,如果主线程不关心新线程的执行信息,可以将新线程设置为分离状态。
当线程处于分离状态后,退出时会自动释放线程资源。
在这里插入图片描述

  • 分离和等待:
    线程被分离后,就不能被 pthread_join() 了,调用这个函数,函数就会返回一个错误码。
  • 分离状态:
    ‘分离’仅仅是一种状态,一种不用主线程等的状态,而不是真正分离了。在这种状态下,线程出异常了,进程会受影响退出;主线程执行完了,进程退出,线程也会退出。

不管是分离还是等待,我们都希望主线程是最后退出的,所以分离的一个场景,就是主进程根本就不退出。
大部分程序都是一直在运行的,称为常驻进程。

2. pthread_detach ()

① 函数细节

在这里插入图片描述

  • thread 参数:要分离的线程的ID
  • 返回值:成功返回 0 ,失败返回错误码

② 函数使用

  • 主线程分离新线程:
#include <pthread.h>
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>

using std::cout;
using std::endl;

void *thfunc(void *arg)
{
    while (1)
    {
        sleep(1);
        cout << "new" << endl;
    }
    return nullptr;
}
int main()
{

    pthread_t rid;
    pthread_create(&rid, nullptr, thfunc, nullptr);

    pthread_detach(rid);

    sleep(3);
    cout << "man" << endl;

    return 0;
}

运行结果:
在这里插入图片描述

  • 新线程自己分离自己:
void *thfunc(void *arg)
{
    pthread_detach(pthread_self());
    while (1)
    {
        sleep(1);
        cout << "new" << endl;
    }
    return nullptr;
}
int main()
{
    pthread_t rid;
    pthread_create(&rid, nullptr, thfunc, nullptr);

    sleep(3);
    cout << "man" << endl;

    return 0;
}

运行结果:
在这里插入图片描述

  • 在线程分离后,再通过 pthread_join() 等待线程会等待失败,函数会返回一个错误码:
    在这里插入图片描述

四、线程自有和共享的数据

进程是资源分配的基本单位,线程是调度的基本单位。进程强调独立,线程强调共享。所以我们来看一下线程自有的和共享的部分。

1. 线程自有的数据

  • 线程 ID: 是用户级别的,内核级的不使用线程 ID,使用的是线程的 LWD
  • 硬件上下文: 每个线程都是被单独调度的执行流,所以要有自己的上下文数据,存放在一组寄存器中。
  • 独立栈结构: 线程本质是在执行自己的函数,每个函数内都可以定义各种临时变量,临时变量都是存放在栈上的。每个线程都有独立的用户栈,不敢让它们一起用一块栈空间。

除了上面几个较为重要的,还有 errno、信号屏蔽字、调度优先级等。

2. 线程共享的资源

  • 地址空间
  • 文件描述符表:因为文件描述符表表示的是进程和打开文件的关系,而不是线程。
  • 每种信号共享的处理方式:这就是为什么一个线程出异常,整个进程就崩掉了。
  • 当前工作目录:进程在哪里,线程就在哪里。
  • 用户 id 和组 id

五、多个线程使用公共空间

1. 不要把公共空间传给多个线程

我们来看下面这段代码:
我们想一次性创建 6 个线程,然后每一个线程都在自己的线程函数中打印一下自己的线程名字(从 16 号)。

#include <pthread.h>
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>

using std::cout;
using std::endl;

const int threadsnum = 6;	//要创建的线程个数

void *testfunc(void *arg)	//线程函数
{
    std::string threadsname = static_cast<char *>(arg);
    cout << threadsname << endl;
    return nullptr;
}
int main()
{
    std::vector<pthread_t> ts;	//将线程放到 vector 中管理一下

    char threadsname[32];

    for (int i = 0; i < threadsnum; i++)
    {
        snprintf(threadsname, 32, "线程 - %d", i + 1);	//在 threadsname 这个数组中放线程的名字
        
        pthread_t tid;
        pthread_create(&tid, nullptr, testfunc, threadsname);	//创建一个新线程,新线程去执行 testfunc 函数了

        ts.push_back(tid);	//将创建好的新线程放到 vecotr 中
    }

    for (auto &id : ts)
    {
        pthread_join(id, nullptr);	//等待 vector 中的每一个线程
    }
    return 0;
}

我们期望的执行结果是这样的:
在这里插入图片描述
(调度顺序不同,可能 1-6 号的顺序不同,但 1- 6 号都要有)。

1.1 问题

  • 我们来看实际的执行结果:
    在这里插入图片描述
  • 原因分析:
    线程函数拿到的参数是一个指针,指向的外部的threadname 空间,一次循环完成之后,再次for 循环还会创建新线程,threadname[64] 中的内容就被覆盖掉了,而 for 循环覆盖 threadsname 内容的速度快于新线程取 threadsname 的内容构成字符串的速度的,最后覆盖后, threadsname 里的内容是 6 号的,所以新线程它们运行到构建字符串那里,取去 threadsname 里拿到的就是 6 号的内容了。
  • 修改一下:
    所以,只要我们将覆盖 threadsname 中内容的操作变慢一些,让新线程在它被覆盖之前就拿到 threadsname 里的数据,就不会有上面的情况出现了。
    在这里插入图片描述
    这样确实是符合预期了,但并不是符合要求,难道每个我们都这样卡时间吗?那也太麻烦了,而且不符合实际。

1. 2 解决

给每个线程单独开一块空间,最后再释放掉。

void *testfunc(void *arg)
{
    std::string threadsname = static_cast<char *>(arg);
    cout << threadsname << endl;

    delete[] (char *)arg; 	//记得释放
    return nullptr;
}
int main()
{
    std::vector<pthread_t> ts;

    for (int i = 0; i < threadsnum; i++)
    {
        char *threadsname = new char[32];
        
        snprintf(threadsname, 32, "线程 - %d", i + 1);
        pthread_t tid;
        pthread_create(&tid, nullptr, testfunc, threadsname);

        ts.push_back(tid);
    }

    for (auto &id : ts)
    {
        pthread_join(id, nullptr);
    }
    return 0;
}

结果:
在这里插入图片描述
完全符合预期。

2. 线程的传参和返回值,可以是各种对象

借由上面的框架,我们可以试一试将对象传给新线程,新线程也返回对象。
示例代码如下:

#include <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <unistd.h>

namespace zzz
{
    class Task
    {
    public:
        Task(int x, int y) : _x(x), _y(y)
        {
        }
        int Excute()
        {
            return _x + _y;
        }

    private:
        int _x;
        int _y;
    };

    class ThreadData
    {
    public:
        ThreadData(int x, int y, std::string name)
            : _t(x, y), _thname(name)
        {
        }
        int Excute()
        {
            return _t.Excute();
        }
        std::string GetName()
        {
            return _thname;
        }

    private:
        std::string _thname;
        Task _t;
    };

    class ThreadResult
    {
    public:
        ThreadResult(int retval, std::string name)
            : _ret(retval), _thname(name)
        {
        }
        void Print()
        {
            std::cout << _thname << ":" << _ret << std::endl;
        }

    private:
        int _ret;
        std::string _thname;
    };
}

using std::cout;
using std::endl;

const int threadsnum = 6;

using namespace zzz;

void *ThreadHandle(void *tdata)
{
    ThreadData *td = static_cast<ThreadData *>(tdata);

    int retval = td->Excute();

    ThreadResult *ret = new ThreadResult(retval, td->GetName());

    delete td;
    return ret;
}

int main()
{
    std::vector<pthread_t> threads;
    std::vector<ThreadResult *> thread_rets;

    for (int i = 0; i < threadsnum; i++)
    {
        // 有需要线程处理的数据
        char threadsname[32];
        snprintf(threadsname, 32, "线程 - %d", i + 1);

        ThreadData *tdata = new ThreadData(i + 1, i + 1, (std::string)threadsname);

        // 创建一个新线程
        pthread_t tid;
        pthread_create(&tid, nullptr, ThreadHandle, tdata);

        threads.push_back(tid);
    }

    for (auto &tid : threads)
    {
        void *ret = nullptr;
        pthread_join(tid, &ret);

        thread_rets.push_back((ThreadResult *)ret);
    }
    for (auto ret : thread_rets)
    {
        ret->Print();
        delete ret;
    }
    return 0;
}

运行结果:
在这里插入图片描述

六、C++11 中的线程

1. C++11 的多线程

C++11 的多线程其实是对原生线程库的封装。
封装是为了跨平台性,不同系统底层创建进程时使用的接口不同,如果直接使用系统提供的接口函数,代码换一个系统就识别不了接口函数了。不同平台提供了不同的 C++ 标准库,所以在不同的平台,只需要链接不同的库就可以了。

2. thread 类及使用

C++11 的线程是 thread 类。

在这里插入图片描述
下面我们来简单使用一下:

#include <iostream>
#include <pthread.h>
#include <thread>

using std::cout;
using std::endl;

void thfunc(int num)
{
    cout << "new:" << num << endl;
}

int main()
{
    int num = 3;

    std::thread t(thfunc, num);
    t.join();

    cout << "main" << endl;
    return 0;
}

运行结果:
在这里插入图片描述

七、线程库对线程的管理

1. 线程的管理由谁来做?

系统只有轻量级进程的概念,它提供不了线程所需的数据:线程 id,优先级等,也就管理不了线程。
库将轻量级进程封装成了线程TCB,库给提供了创建、等待等线程的管理工作,所以库内管理了线程。
下面我们来看一些内部细节。

2. 线程管理的细节

① 线程库首先要映射到当前进程的地址空间中

在这里插入图片描述

② 进程地址空间中的动态线程库

在这里插入图片描述
这里有几个需要提及的点:线程ID,线程栈、线程局部存储。下面我们分别来看一下:

  • 线程ID: 从上图我们可以看到线程ID 到底是什么,在线程 - 四、创建线程中我们反复提到了线程的 ID,其实线程的 ID 就是一个地址,是当前线程对象在地址空间中对应位置的其实地址,所以通过线程 ID 就可以知道线程的虚拟地址,知道虚拟地址就可以通过页表找到对应的物理内存地址了。

  • 线程栈: 在上面的【四、2. 线程自有的数据】部分,我们提及了线程有独立栈结构,现在我们看到它了, 线程的栈结构是在库中维护的,每个线程都有自己的独立栈结构,主线程使用的是地址空间中的栈空间,叫主线程栈。通过下面的代码看一下:
    在这里插入图片描述
    让两个线程进同一个线程函数,打印同一个临时变量的地址,是不一样的,说明使用的是不同的栈结构。

  • 线程的局部存储: 一个全局变量,线程之间是可以共享的,因为它们共享地址空间,这一点,在 线程 - 六、线程退出 中有代码看过,确实对于同一个全局变量,它们的地址都是相同。那么我现在想让每个线程都私有一个全局变量 g_val,可以做到吗?可以的,我们来看一下:

    #include <iostream>
    #include <pthread.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    using std::cout;
    using std::endl;
    
    __thread int g_val = 100;
    
    void *pfunc(void *arg)
    {
        cout << (char *)arg << " " << &g_val << endl;
        return nullptr;
    }
    int main()
    {
        pthread_t rid;
        pthread_create(&rid, nullptr, pfunc, (void *)"thread-1");
        pthread_create(&rid, nullptr, pfunc, (void *)"thread-2");
    
        sleep(2);
        
        cout << "core end" << endl;
    
        return 0;
    }
    

    在这里插入图片描述

    • _threadgcc 的一种编译选项,会将它修饰的量,给各个线程都拷贝一份,放到了线程的局部存储空间中。这一点我们可以看一下:
    • 所以,如果想记录一些线程独有的数据,但又不想哪个线程中都定义一次局部变量,那就可以用 __thread 来修饰一个全局变量,让所有线程独用。
    • 需要强调一点的是:线程的局部存储只能存储内置类型!
      在这里插入图片描述

③ 整个系统的所有的线程都在这一个库中管理着

在这里插入图片描述

  • 这里我们可以思考一个问题:既然整个系统的所有的线程都在这一个库中管理着,那么一个线程会不会看到另一个进程的线程块?
  • 其实不会出现这种情况,因为每个进程的线程都是自己的,在自己的地址空间的动态库中管理着,只有这个进程有它的线程的虚拟地址,通过该进程自己的页表才能得到物理地址,其他进程看不到其他进程的线程的虚拟地址,更看不见其他进程的页表,也就无法映射看到真是的物理内存中的线程。

3. 执行流怎么找到栈的?

既然主线程和其他线程的栈空间不同,那么执行流怎么找到栈的呢?
其实,底层是调用了 clone() 系统调用。
在这里插入图片描述

  • fn:函数指针,可以指向线程函数。
  • stack:指向线程的栈空间的起始地址。

所以,是栈空间的地址是可以传递的,所以是可以找到的。

八、线程的封装

在这里插入图片描述
pthread 库对轻量级进程的封装我们并不清楚,所以这里我们所说的封装并不是这个,而是可以将pthread库提供给我们的接口封装一下,成为类,就像 C++11 提供的线程类一样。这里提供给大家一份,供大家参考。

1. Pthread.hpp

#ifndef __THREAD_HPP
#define __THREAD_HPP

#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>

namespace zzz
{

    template <typename T>
    class Thread
    {
        template <typename K>
        using func_t = std::function<void(K &)>;

    public:
        Thread(func_t<T> func, T data, const std::string &name = "default_thread")
            : _func(func), _data(data), _threadname(name), _stop(1)
        {
        }
        static void *threadfunc(void *arg)
        {
            Thread<T> *_this = static_cast<Thread<T> *>(arg);

            _this->_func(_this->_data);

            return nullptr;
        }

        bool start()
        {
            _stop = 0;

            int n = pthread_create(&_tid, nullptr, threadfunc, this);
            if (!n)
            {
                _stop = false;
                return true;
            }
            else
            {
                return false;
            }
        }
        void join()
        {
            if (!_stop)
                pthread_join(_tid, nullptr);
        }
        void detach()
        {
            if (!_stop)
                pthread_detach(_tid);
        }
        void stop()
        {
            _stop = 1;
        }
        const std::string &name() const
        {
            return _threadname;
        }
        pthread_t id() const
        {
            return _tid;
        }

    private:
        pthread_t _tid;          // 线程 id
        std::string _threadname; // 线程的名字
        func_t<T> _func;         // 线程函数
        T _data;                 // 线程函数的参数
        bool _stop;              // 线程开始运行
    };
}

#endif

2. test.cc

#include <iostream>
#include "Thread.hpp"
#include <vector>
#include <pthread.h>

using std::cout;
using std::endl;
using namespace zzz;

void testfun(int n)
{
    cout << n << endl;
}

int main()
{
    std::vector<Thread<int>> pths;

    for (int i = 0; i < 5; i++)
    {
        std::string tname = "thread-" + std::to_string(i + 1);

        pths.emplace_back(testfun, 100, tname);
    }
    for (auto &thread : pths)
    {
        thread.start();
    }
    for (auto &thread : pths)
    {
        thread.join();
    }
    cout << "main end" << endl;

    return 0;
}

运行结果:
在这里插入图片描述


本文到这里就结束了,如果对您有帮助,希望得到您的一个赞!🌷
如有错漏,欢迎指正!😄

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值