前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【Linux】线程概念与控制

【Linux】线程概念与控制

作者头像
大耳朵土土垚
发布2025-01-24 08:01:49
发布2025-01-24 08:01:49
7700
代码可运行
举报
文章被收录于专栏:c/c++c/c++
运行总次数:0
代码可运行

1. 线程概念

  在⼀个程序里的⼀个执行路线(或者叫执行流)就叫做线程 (Thread)。更准确的定义是:线程是“⼀个进程内部的控制序列”。线程(Thread)是操作系统能够进行运算调度的最小单位,它比进程更小,是进程内的一个执行单元,也是进程内的实际运作单位。一个线程可以执行某个程序段,或是在给定的数据集上运行一段程序。线程有时被称为轻量级进程(lightweight process),因为它们与同一进程中的其他线程共享资源(如内存地址空间、文件描述符等),这使得线程之间的切换和通信相比于进程来说更加高效。

  • 一切进程至少都有⼀个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的⼤部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

如下图所示:

以下是线程的一些关键特点:

  1. 资源共享:同一进程内的所有线程共享该进程的资源,包括内存地址空间、打开的文件和信号处理等。这种共享减少了创建和上下文切换的开销,但同时也要求对共享资源的访问进行适当的同步以避免数据竞争(data race)。
  2. 独立性:每个线程都有自己的调用栈(call stack),因此它们可以在不同的函数中执行,并且拥有独立的局部变量副本。但是全局变量和静态变量则是共享的。
  3. 并发性:多线程允许一个应用程序同时执行多个任务。例如,在图形用户界面的应用中,一个线程可能负责绘制界面,而另一个线程则负责后台计算。
  4. 通信简单:由于线程共享相同的地址空间,所以它们之间可以通过直接读写共享内存来通信,而不必像进程那样使用IPC(Inter-Process Communication)机制。
  5. 生命周期依赖于所属进程:线程不能独立存在,必须存在于进程中;线程的生命周期不会超过其所属的进程。
  6. 线程的状态:线程在其生命周期内会经历不同的状态,如新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和终止(Terminated)。
  7. 线程优先级:一些操作系统支持为线程设置优先级,这会影响线程调度器决定哪个线程应该首先获得CPU时间。
  8. 线程同步:为了确保数据的一致性和完整性,通常需要使用同步工具(如互斥锁、条件变量、信号量等)来协调线程之间的操作顺序。

2. 线程优缺点

【优点】:

(1)创建⼀个新线程的代价要比创建⼀个新进程小得多; (2)与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多;

  • 最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
  • 另外⼀个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,⼀旦去切换上下文,处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚拟内存空间的时候,处理的⻚表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在这段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。

(3)线程占⽤的资源要⽐进程少很多; (4) 能充分利⽤多处理器的可并⾏数量; (5)在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务; (6) 计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现; (7) I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

【缺点】:

(1)性能损失

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

(2)健壮性降低

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

(3)缺乏访问控制

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

(4)编程难度提高

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

3.Linux进程VS线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的⼀部分数据: ◦ 线程ID ◦ ⼀组寄存器 ◦ 栈 ◦ errno ◦ 信号屏蔽字 ◦ 调度优先级
  • 进程的多个线程共享同⼀地址空间,因此Text Segment、Data Segment都是共享的,如果定义⼀个函数,在各线程中都可以调⽤,如果定义⼀个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
    • ⽂件描述符表
    • 每种信号的处理⽅式(SIG_ IGN、SIG_ DFL或者⾃定义的信号处理函数)
    • 当前⼯作⽬录
    • 用户id和组id

进程和线程的关系如下图:

也就是说之前学习的单进程是具有⼀个线程执行流的进程。

4. 线程控制

4.1 POSIX线程库

• 与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是以pthread_打头的; • 要使用这些函数,要通过引入头文件 <pthread.h>; • 链接这些线程函数库时要使⽤编译器命令的-lpthread选项。

4.2创建线程

代码语言:javascript
代码运行次数:0
复制
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;失败返回错误码

使用PS命令查看线程信息:

代码语言:javascript
代码运行次数:0
复制
ps -aL | head -1 && ps -aL | grep myprocess

创建线程后我们就可以通过该命令在命令行查看线程相关信息:

代码语言:javascript
代码运行次数:0
复制
PID     LWP     TTY     TIME     CMD
2711838 2711838 pts/235 00:00:00 mythread
2711838 2711839 pts/235 00:00:00 mythread

 -L 选项:打印线程信息

PID是进程ID,LWP指线程ID。有⼀个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的,而pthread库是在共享区的,所以除了主线程之外的其他线程的栈都在共享区。

4.3 线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit,直接终止进程。
  2. 线程可以调⽤pthread_ exit终止自己。
代码语言:javascript
代码运行次数:0
复制
void pthread_exit(void *value_ptr);
  • 参数:

value_ptr:value_ptr不要指向⼀个局部变量。也就是说指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

  • 返回值:

无返回值,跟进程⼀样,线程结束的时候无法返回到它的调用者(自身)

  1. ⼀个线程可以调⽤pthread_ cancel终止同⼀进程中的另⼀个线程。
代码语言:javascript
代码运行次数:0
复制
int pthread_cancel(pthread_t thread);
  • 参数:

thread:线程ID

  • 返回值:

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

4.4 线程等待

  因为已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。如果不进行回收,新的线程不会复用刚才退出线程的地址空间,就会造成资源的浪费。

代码语言:javascript
代码运行次数:0
复制
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线程的终止状态不感兴趣,可以传NULLvalue_ ptr参数。

4.5 线程分离

  默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关系线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源,函数如下:

代码语言:javascript
代码运行次数:0
复制
 int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进⾏分离,也可以是线程自己分离:

代码语言:javascript
代码运行次数:0
复制
pthread_detach(pthread_self());

joinable和分离是冲突的,⼀个线程不能既是joinable又是分离的

pthread_ create函数会产⽣⼀个线程ID,存放在第⼀个参数指向的地址中。线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:

代码语言:javascript
代码运行次数:0
复制
 pthread_t pthread_self(void);

5. 线程封装

  通过对线程创建、终止、等待、分离等函数的学习,我们就可以利用这些接口封装一个便于理解和使用的线程库。   线程库类成员变量包括线程ID进程ID线程名字线程当前所处状态线程是否可以分离以及线程绑定的函数这六个。

代码语言:javascript
代码运行次数:0
复制
#pragma once

#include <iostream>
#include<unistd.h>
#include<string>
#include<functional>
#include <pthread.h>
namespace ThreadModule
{
    enum class TSTATUS
    {
        NEW,
        RUNNING,
        STOP
    };
    static int number = 1;//静态全局变量
    class Thread
    {
    private:
            using func_t = std::function<void()>;
            static void* run(void* args)//static不用带this指针,所以可以传入一个void*类型
            {
                Thread *t = static_cast<Thread *>(args);//类型转换
                t->_status = TSTATUS::RUNNING;
                t->_func();//不直接pthread_create调用,而封装一层为了统一接口,方便调用
                return nullptr;
            }
    public:
        Thread(func_t func)
        :_joinable(true),
        _tid(-1),
        _status(TSTATUS::NEW),
        _func(func)
        {
            _name = "Thread-" + std::to_string(number++);
            _pid = getpid();
        }
        bool Start()
        {
            if(_status == TSTATUS::RUNNING)
            {
                std::cout<<"线程已经Start..."<<std::endl;
                return true;
            }
            int n = pthread_create(&_tid,NULL,run,this);
            if(n != 0)
            {
                std::perror("线程Start失败...");
                return false;
            }
            std::cout<<"线程Start成功..."<<std::endl;
            return true;
        }
        bool Stop()
        {
            if(_status != TSTATUS::RUNNING)
            {
                std::cout<<"请开启线程..."<<std::endl;
                return false;
            }
            int n = pthread_cancel(_tid);
            if(n!= 0)
            {
                std::perror("线程Stop失败...");
                return false;
            }
            _status = TSTATUS::STOP;
            std::cout<<"线程Stop成功..."<<std::endl;
            return true;
        }
        bool Join()
        {
            if(_joinable == false)
            {
                std::cout<<"请开启线程等待权限..."<<std::endl;
                return false;
            }
            int n = pthread_join(_tid, NULL);
            if(n!= 0)
            {
                std::perror("线程Join失败...");
                return false;
            }
            _status = TSTATUS::STOP;
            std::cout<<"线程Join成功..."<<std::endl;
            return true;
        }
        void Detach()
        {
            if(_status != TSTATUS::RUNNING)
            {
                std::cout<<"请开启线程..."<<std::endl;
                return;
            }
           if(_joinable == true)
           {
            int n = pthread_detach(_tid);
            if(n!= 0)
            {
                std::perror("线程Detach失败...");
                return;
            }
            _joinable = false;
            std::cout<<"线程Detach成功..."<<std::endl;
           }
           else
                std::cout<<"线程已经Detach..."<<std::endl;
           return;
        }
        bool IsJoinable() { return _joinable; }
        std::string Name() { return _name; }
        
        ~Thread(){}

    private:
        pthread_t _tid;
        pid_t _pid;
        bool _joinable;//线程是否可以等待,默认可以等待也就是不可分离
        std::string _name;
        TSTATUS _status;
        func_t _func;//线程执行的函数
    };
}

然后我们就可以使用封装好的线程库:

代码语言:javascript
代码运行次数:0
复制
#include "thread.hpp"//封装好的线程库头文件

int main()
{
ThreadModule::Thread t([](){
        while(true)
        {
            std::cout << "hello world" << std::endl;
            sleep(1);
        }
    });

    t.Start();
    
    sleep(5);

    t.Stop();//线程终止
    
    sleep(1);
    t.Join();//线程等待
    
    return 0;
}

结果如下:

当然对于线程库我们也可以再加一个成员变量,并使用模板,所以该成员变量可以是任意类型的数据给线程绑定的函数使用。注意编译的时候要带上-lpthread选项。

6. 结语

  线程就是一个执行流,一个进程内可以有多个线程。进程是划分资源的最小单位,而线程(Thread)是操作系统能够进行运算调度的最小单位。通过对线程的各种函数的理解与认识,我们就可以封装便于理解和学习的线程库。以上就是今天的所有内容啦~ 完结撒花 ~ 🥳🎉🎉

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-01-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 线程概念
  • 2. 线程优缺点
  • 3.Linux进程VS线程
  • 4. 线程控制
    • 4.1 POSIX线程库
    • 4.2创建线程
    • 4.3 线程终止
    • 4.4 线程等待
    • 4.5 线程分离
  • 5. 线程封装
  • 6. 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档