【Linux】线程与线程控制

目录

线程的概念

线程的优点

线程的缺点

线程异常

线程的理解

哪些资源是线程共享的,哪些是线程独占的?

进程与线程对比

Linux 进程和线程有什么区别?

什么时候用进程?什么时候用线程?

线程的操作

线程周边问题

C++11 的线程库和 Linux 提供的原生线程库有什么关系?

线程 tid 究竟是什么? LWP 呢?

线程局部存储(Thread-Local Storage,TLS)

线程的概念

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
  • 一切进程至少都有一个执行线程。
  • 线程是进程内部的一个执行分支,线程是 CPU 调度的基本单位。
  • 线程在进程内部运行,本质是在进程地址空间内运行。

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多。
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。操作系统需要从一个进程切换到另一个进程时,它需要保存当前进程的上下文信息(包括程序计数器、CPU寄存器的值、内存管理信息、打开的文件状态等),由于进程资源的不互通,每次进程切换后,访问数据很有可能缓存不命中,这时就需要重新填充 cache;而线程切换则相对简单,因为线程共享同一个进程的地址空间和资源,cache 的命中率会高很多,cache 的填充与否就会大大拉开进程与线程切换的效率。
  • 线程占用的资源要比进程少很多。多个线程共享同一份进程控制块、进程地址空间、页表、代码段与数据等资源。
  • 能充分利用多处理器的可并行数量。
  • 在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务。
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点

  • 性能损失。一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低。编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制。进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高。编写与调试一个多线程程序比单线程程序困难得多。

线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

线程的理解

如下图所示,如果操作系统为对进程和线程分别设置对应的控制块,并用相应的数据结构来维护一个进程中的所有线程,那么就意味着操作系统还要为进程和线程设计不同的调度算法等。

Linux 系统的设计者认为,进程和线程都是执行流,具有极度的相似性,因此,没必要单独设计数据结构和调度算法,直接复用同样的代码即可,使用进程来模拟线程,也就是说 Linux 内核没有线程的概念,只有 LWP(Light Weight Process) —— 轻量级进程的概念,进程和线程均采用 task_struct 来管理和维护,同一个进程里的 LWP 共享同一份地址空间 mm_struct、页表、文件描述符表、代码、全局数据等。而 Windows 将进程和线程隔离,分别设计数据结构和调度算法等。

查看系统轻量级进程
ps -aL

哪些资源是线程共享的,哪些是线程独占的?

进程可以拥有资源,并且是系统拥有资源的基本单位 。线程本身并不拥有系统资源,仅有一些能保证独立运行的资源,这块资源的各个线程私有的。

线程各自独立拥有的部分:

  • 线程ID
  • 一组寄存器(保存程序运行上下文)
  • 独立栈
  • errno(错误编码)
  • 信号屏蔽字(一个进程中 pending(未决)信号只有一个,但是任意一个线程都可以处理这个信号)
  • 调度优先级

共享区域

  • 同一个地址空间(代码、全局数据、堆都是共享的。如果定义一个函数,在每个线程中都可以共享到,如果定义一个全局变量,在任何一个线程中都可以访问到
  • 文件描述符表
  • 每种信号的处理方式
  • 当前工作目录
  • 用户ID和组ID

进程与线程对比

重新以资源分配和调度的角度看待进程和线程。

进程是承担系统资源分配的基本实体,是独立运行的基本单位。

线程是进程内部的一个执行分支,是 CPU 调度的基本单位。

Linux 进程和线程有什么区别?

线程是进程中活跃状态的实体,一方面进程中所有的线程共享一些资源,另一方面,线程又有自己专属的资源,例如有自己的程序计数器,用户栈、内核栈,有自己的硬件上下文、调度策略等等。我们一般会说进程调度什么的,但是实际上线程才是是调度器的基本单位。对于Linux内核,线程的实现是一种特别的存在,和经典的unix都不一样,在linux中并不区分进程和线程,都是用task_struct 来抽象,只不过支持多线程的进程是由一组 task_struct 来抽象,而这些task_struct会共享一些数据结构(例如文件描述符)。我们用 thread ID 来唯一标识进程中的线程,POSIX规定线程ID在所属进程中是唯一的,不过在 Linux kernel 的实现中,thread ID 是全系统唯一的。对于单线程的进程,process ID 和 thread ID 是一样的,对于支持多线程的进程,每个线程有自己的thread ID,但是所有的线程共享一个process ID。

什么时候用进程?什么时候用线程?

多进程与多线程的对比

对比维度多进程多线程总结
数据共享、同步数据共享复杂,需要用 IPC;数据是分开的,同步简单因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂各有优势
内存、CPU占用内存多,切换复杂,CPU 利用率低占用内存少,切换简单,CPU 利用率高线程占优
创建销毁、切换创建销毁、切换复杂,速度慢创建销毁、切换简单,速度很快线程占优
编程、调试编程简单,调试简单编程复杂,调试复杂进程占优
可靠性进程间不会互相影响一个线程挂掉将导致整个进程挂掉进程占优
分布式适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单适应于多核分布式进程占优

多进程与多线程的选择策略:

  • 需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。
  • 线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应
  • 因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;
  • 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;
  • 需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。
  • 强相关的处理用线程,弱相关的处理用进程
  • 都满足需求的情况下,用你最熟悉、最拿手的方式

但是实际中更常见的是进程加线程的结合方式,并不是非此即彼的。

线程的操作

Linux 采用原生线程库将 LWP 进行了封装,对用户提供了线程接口,所以编译时需要链接 phread 库。

创建一个线程

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
        void *(*start_routine) (void *), void *arg);

线程等待并回收线程资源

int pthread_join(pthread_t thread, void **retval);

获取当前线程的 tid
pthread_t pthread_self(void);

终止当前线程

void pthread_exit(void *retval);

不推荐!

终止指定线程,线程退出码为 PTHREAD_CANCELED,即 -1。

int pthread_cancel(pthread_t thread);

将当前线程与主线程分离

int pthread_detach(pthread_t thread);

注:线程分离后,主线程就不能 pthread_join 了,当线程退出后,自动释放线程资源。

即使线程已经分离,但主线程退出,其他线程依旧会全部退出;其他线程出现异常,所有进程都会退出。

一般都希望主线程是最后退出的,无论是 join 还是 detach。

线程周边问题

C++11 的线程库和 Linux 提供的原生线程库有什么关系?

C++11的多线程,是对原生线程库的封装。

  1. 为什么要封装?代码可移植性,语言跨平台性。
  2. 其他语言有没有封装原生线程库呢?原生线程库是 Linux 提供多线程的底层唯一方式!其他语言希望向用户提供自己的线程库,就必然要使用 Linux 提供的原生线程库。

 以下代码是 Linux 环境下线程库的简单实现

#ifndef __THREAD_HPP__
#define __THREAD_HPP__

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

namespace zyh
{
    template<typename T>
    using func_t = std::function<void(T)>;

    template<typename T>
    class Thread
    {
    public:
        Thread(func_t<T> func, T data, const std::string& threadName = "none")
            : m_func(func)
            , m_data(data)
            , m_threadName(threadName)
            , m_stop(true)
        {}

        ~Thread()
        {}

        static void* threadRoutine(void* args)
        {
            Thread<T>* self = static_cast<Thread<T>*>(args);
            self->execute();
            return nullptr;
        }

        bool start()
        {
            int n = pthread_create(&m_tid, nullptr, threadRoutine, this);
            if (!n)
            {
                m_stop = false;
                return true;
            }
            else
            {
                return false;
            }
        }

        void execute()
        {
            m_func(m_data);
        }

        void join()
        {
            if (!m_stop)
            {
                pthread_join(m_tid, nullptr);
            }
        }

        void detach()
        {
            if (!m_stop)
            {
                pthread_detach(m_tid);
            }
        }

        void stop()
        {
            m_stop = true;
        }

        std::string getThreadName()
        {
            return m_threadName;
        }

        pthread_t getTid()
        {
            return m_tid;
        }

    private:
        pthread_t m_tid;
        std::string m_threadName;
        T m_data;
        func_t<T> m_func;
        bool m_stop;
    };
}

#endif // __THREAD_HPP__

测试代码

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

using namespace zyh;

void print(int cnt)
{
    while (cnt)
    {
        std::cout << "I am a thread, cnt: " << cnt-- << std::endl;
        sleep(1);
    }
}

const int num = 10;

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

    // 1. 创建线程
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        threads.emplace_back(print, 10, name);
    }

    // 2. 启动线程
    for (auto& thread : threads)
    {
        thread.start();
    }

    // 3. 等待线程
    for (auto& thread : threads)
    {
        thread.join();
        std::cout << "wait thread done, thread is: " << thread.getThreadName() << std::endl;
    }

    return 0;
}

线程 tid 究竟是什么? LWP 呢?

Linux 内核中没有线程的概念,只有轻量级进程。

用户可以使用线程相关接口,管理线程,比如创建、终止、等待等。由原生线程库对线程接口进行封装,那么线程的管理工作就只能由库来进行,怎么管理?先描述,再组织。

原生线程库中采用相应的数据结构对每个线程的资源进行管理,包括线程局部存储数据、上下文数据、线程栈等,线程 tid 就是线程库中线程 tcb 的起始地址,这也就是为什么我们以整型打印 tid 会如此大的原因。

而 LWP 是与 PID 相应的一个概念,如上图所示,通过主线程创建了10个新线程,主线程的 LWP 与进程 PID 相同,而10个新线程具有相同的 PID,即这些新线程都从属于同一个进程。

线程局部存储(Thread-Local Storage,TLS)

运行以下代码

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


int g_val = 10;
const int num = 4;

void* handler(void* args)
{
    g_val += 10;
    std::cout << "g_val: " << g_val << ", &g_val: " << &g_val << std::endl;
    sleep(1);
    return nullptr;
}

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

    for (int i = 0; i < num; ++i)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, handler, nullptr);
        threads.push_back(tid);
    }

    for (auto& thread :threads)
    {
        pthread_join(thread, nullptr);
    }

    return 0;
}

运行结果

g_val 是全局变量,所有线程共享这个数据,所以4个线程打印出来的 g_val 的地址相同。

__thread

对全局变量使用 __thread 可使该变量放到每个线程的局部存储空间里。

注意:

  • _thread 是 gcc 的一个扩展,不是 C 或 C++ 标准的一部分。因此,其可用性可能取决于编译器和平台。
  • TLS 变量的空间大小有限制,具体限制取决于操作系统和硬件平台。
  • TLS 变量不能用于动态分配的内存(如 malloc 分配的内存),因为 TLS 变量的生命周期与线程相同,而动态分配的内存需要手动管理。
  • 在使用 TLS 变量时,需要注意避免使用静态或全局的指针指向TLS变量,因为这可能会导致指针共享和线程安全问题。
  • TLS 只能用于内置类型。

在上面代码的基础上,对 g_val 使用局部线程存储。

__thread int g_val = 10;

运行结果

可以看到,使用局部线程存储后,每个线程都有一份 g_val,这段代码是线程安全的。

  • 22
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

毕瞿三谲丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值