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

一、线程的优点

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 =
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值