【Linux】线程概念与控制

发布于:2025-09-09 ⋅ 阅读:(16) ⋅ 点赞:(0)

一. 线程的概念

1.什么是线程

线程是进程内部的一个执行流,是进程调度的基本单位。它具有轻量的特点,它的创建和销毁所消耗的资源更少,线程间切换比进程间切换消耗的资源更少;它与进程共享一张虚拟地址空间表,通过进程来给线程执行流分配资源;同时,每个线程都是独立执行的,拥有自己的程序计数器和上下文切换。

简单来说在Linux下,一个进程由多个 task_struct ,一张虚拟地址空间表和页表构成。而线程就是一个 task_struct ,进程内部的一个执行流,所有的线程都指向同一张虚拟地址空间表,让进程共同管理。这样我们就可以对所有线程的资源进行划分,划分为堆区栈区共享区等等。通过一张页表映射到物理内存当中。

总的来说,线程就是一个 task_struct ,通过共同指向同一张虚拟地址空间的方式实现了共同管理,降低了创建调度销毁成本。

2.深刻理解虚拟地址空间

在虚拟地址空间中,页表用于映射虚拟地址空间到实际的物理内存。我们在管理虚拟地址空间的时候,它的地址是连续的,而物理地址空间则是可以分散碎片的。在虚拟地址空间中,我们存储同一个资源的时候地址空间需要连续,但在物理地址当中,我们会将同类型的资源尽可能放到一处(这样可以节省空间),无论是哪个线程都可以将数据进行整合。

有了虚拟地址空间和物理地址空间,那么我们如何将他们连接起来呢?再加一层页表就好。

在 Linux 当中,页表是由,三级页表组成一级页表是页表目录,其中存储着各个页表的地址;二级目录是各个页表,页表指向各个页帧的地址(4 KB);三级页表就是页帧,每个页帧由 4 KB 构成。在虚拟地址空间中,每个数据在虚拟地址空间下都有一个 32 字节的地址,这 32 个字节需要分为 10 + 10 + 12 来进行阅读,首先定位到页表目录当中,前10个字节,用于在页表目录当中找到对应的页表;中间的10个字节用于在当前页表当中找到对应的页帧;最后的12个字节用于对页帧的起始位置的偏移量,这样我们就能通过虚拟地址找到相对于的物理地址

下面是一个简化图

有了页帧,该如何管理呢? 先描述再组织,操作系统引入了 struct page 结构。对于每个页帧,都有一个 struct page 对它进行相应的管理。

下面我来介绍一下 struct page 的结构构成。

该结构主要用于管理记录,跟踪页帧的使用状态,页针的归属,管理页帧的映射关系,回收页帧等等。

首先是状态标识(flags),用于记录页帧的基本状态,是被锁定被修改还是内核保留;引用计数(_refcount)记录该页帧被引用的次数;映射关系(mapping + index)mapping 指向该文件的存储页,index 用于指向在该页下的偏移量。


总结:

1. 虚拟地址和物理地址管理,通过页表进行映射,使得其完成了解耦的操作。

2. 页表按需创建和分页机制有效的节省了空间消耗。

3.线程的优缺点

(1)优点

线程的创建相比于进程的创建代价要小很多且占用资源少,线程只需要创建 task_struct 挂接到虚拟地址空间上即可,而进程的创建就要涉及虚拟地址空间页表等等资源;线程切换比进程切换效率高,如果要进行进程间的切换,就需要连同虚拟地址空间等进行统一切换,而线程只需要切换 task_struct 和上下文资源即可;线程可以利用多处理器进行并发运行,提高 IO 效率和计算效率;

(2)缺点

线程共享进程的地址空间,因此可以访问到当前的共享资源,这就导致,若缺乏同步机制,线程会引发数据竞争,导致程序异常;当线程过多时,容易导致资源限制,首先每个线程都有自己的独立线程栈在内存当中,大小为 8 MB ,若线程过多就容易导致内存空间耗尽。其次,若线程过多,CPU的调度开销也对应的增加,CPU 将时间花在了不断调度线程中,导致 CPU 利用率下降。最后,每个线程创建都会在内核当中创建一个 TCB 资源,这也就导致了高频创建销毁会给内核增加负担。进程拥有较高的独立性,即使程序出错进程崩溃,这也不会影响其他的进程运行,但如果线程崩溃,可能会导致整个进程都退出。

二. 线程的控制

1.线程创建

pthread_create:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void*),void *arg);

参数说明:

thread:获取创建成功的线程 ID ,该参数是一个输出型参数。

attr:用于设置进程属性,传入NULL 表示使用默认值。

start_routine:返回值和参数均为 void* 的函数指针。该参数表示线程例程,即后续线程需要执行的函数。

arg:传给线程实例的参数。

返回值:

成功返回0,失败返回错误码。

下面我们来看一个示例,让一个主线程创建一个新线程

当一个程序启动时,就有一个进程被操作系统创建,于此同时一个线程也立刻运行,这个线程就是主线程。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *startRoutine(void* args)
{
    while(true)
    {
        cout<<"线程正在运行"<<endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread-1");
    cout<<"new thread id:"<<tid<<endl;
    while(true)
    {
        cout<<"main pthread 正在运行"<<endl;
        sleep(1);
    }
    return 0;
}

运行结果:


当我们想获取线程 id 时,可以使用 pthread_self 函数,我们来看下面的代码

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

static void Print(const char* name,pthread_t tid)
{
    cout<<name<<" 正在运行"<<tid<<endl;
}

void *routine(void* argv)
{
    const char* name = static_cast<const char*>(argv);
    while(true)
    {
        Print(name,pthread_self());
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,routine,(void*)"thread");
    while(true)
    {
        cout<<"main thread run"<<endl;
        sleep(1);
    }
    return 0;
}

下面是运行结果:

在线程运行中,调用了 pthread_self 函数将当前线程的tid传给了函数进行调用。


2.线程终止

终止一个线程有三种方法:

1.从线程函数 return

2.在线程中调用

3.在线程中调用 pthread_exit 终止其它进程中的另一个线程

方法一:(从线程return)

方法较简单不详细讲解

方法二:(pthread_exit

pthread_exit 的功能就是终止线程

#include <pthread.h>
void pthread_exit(void* retval);

参数说明:

retval:线程退出码

注意:

pthread_exit return 返回的指针所指向的内存单元必须是全局的或者是 malloc 分配的,若是在线程内部创建的指针返回会导致访问结果不可控。因为随着 pthread_exit 线程栈上存储的数据也会被销毁

下面看一下正确使用:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

static void printTid(const char* name,const pthread_t &tid)
{
    cout<<name<<" 正在运行 "<<tid<<"  "<<endl;
}

void* Routine(void* argv)
{
    const char* name = static_cast<const char*>(argv);
    int cnt = 5;
    while(true)
    {
        printTid(name,pthread_self());
        if(!(cnt--))
        {
            break;
        }
        sleep(1);
    }
    cout<<"线程退出"<<endl;
    pthread_exit((void*)11111);
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,Routine,(void*)"thread");
    void* ret = nullptr;
    pthread_join(tid,&ret);
    cout<<"main pthread success   "<<(long long)ret<<endl;
    sleep(5);
    while(true)
    {
        printTid("main othread",pthread_self());
        sleep(2);
    }
    return 0;
}

运行结果:


3.线程等待

pthread_join:

类比于进程等待,线程创建也是需要被等待的,如果一个新线程被创建出来,主线程不进行等待,那么这个新线程的资源就无法被回收,就会导致资源泄露。在线程中等待的函数叫 pthread_join 

#include <pthread.h>
int pthread_join(pthread_t thread,void **retval);

参数说明:

thread:被等待的线程tid

retval:线程退出时的信息码

返回值:

成功返回0,失败返回信息码

下面是代码样例:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

void *thread_func(void *arg) 
{
    printf("子线程任务完成\n");
    pthread_exit((void*)100);  
    sleep(200);
}

int main() 
{
    pthread_t tid;
    void *ret;
    pthread_create(&tid, NULL, thread_func, NULL);
    pthread_join(tid, &ret);  
    printf("子线程退出状态:%ld\n", (long)ret);
    return 0;
}

运行结果:

主线程进行阻塞,等待子线程完成任务后,主线程才会继续运行

4.线程分离

pthread_detach:

线程分离与线程等待是一对互斥关系,当我们主线程不需要关心子线程的返回值时,我们可以将子线程进行分离(也可以是子线程自行分离),分离后的线程会继续执行自己的内容。一个线程被分离了,这个进程依旧需要管理这个线程的资源,若被分离的线程出现故障也有可能会影响其他的线程或者当前进程。分离的线程可以减轻 join 的负担,意味着主线程不需要再关注子线程了,而子线程执行完毕后也会自行释放资源。

#include <pthread.h>
int pthread_detach(pthread_t thread);

参数说明:

thread:被分离的线程 ID 

返回值:

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


5.POSIX线程库

在Linux当中,站在内核角度实际上并没有关于线程相关的接口,但是用户希望创建线程时可以调用接口,这样可以使编码更加便捷。于是,便有了第三方的线程库,基于这个第三方库,它为用户提供了线程相关的接口,构成了线程有关的完整系列。

这些接口大多数都是以 pthread_  打头,在使用前需要包含头文件  <pthread.h> ,链接库时需要包含  -lpthread 选项。

6.线程栈和 pthread_t

线程是一个独立的执行流,在运行的过程中也会产生自己的数据,所以线程拥有自己的独立的栈,线程栈会随着线程的销毁被回收。

在 Linux 中,基于线程的接口都是通过外部库封装后进行调用的,pthread_t 是线程的身份证,用于识别和操作线程。在外部库中,pthread_t 是由 thread_info 结构体进行管理的。

struct thread_info
{
    pthread_t tid;
    void *stack;
}

与其一同管理的便是线程栈。每当用户创建一个线程时,就会在动态库中创建一个线程控制块 thread_info ,给用户返回一个 pthread_t ,也就是该结构体的起始虚拟地址。

主线程中的栈区使用的是地址空间中的栈区,而创建的子线程用的是库中提供的栈结构。

7.线程的局部存储

在线程中,全局变量是共享的,所有的线程可以共用一份全局变量,如果想让全局变量私有那么可以进行线程变量的局部存储

下面我们来验证一下,线程可以共用同一份全局变量

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

int global_val = 100;

void *Routine(void *argv)
{
    const char* name = static_cast<const char*>(argv);
    while(true)
    {
        cout<<"thread: "<<name<<"  global_value"<<global_val<<" new: "<<global_val++<<"address: "<<&global_val<<endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1,nullptr,Routine,(void*)"thread1");
    pthread_create(&tid2,nullptr,Routine,(void*)"thread2");
    pthread_create(&tid3,nullptr,Routine,(void*)"thread3");
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    return 0;
}

运行结果:

我们发现,全局变量在主线程和子线程下该变量的地址都是一致的,它们所用的是同一个变量

若我们希望在每一个子线程下都创建一份变量我们可以这样操作

样例代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

__thread int global_val = 100;

void *Routine(void *argv)
{
    const char* name = static_cast<const char*>(argv);
    while(true)
    {
        cout<<"thread: "<<name<<"  global_value"<<global_val<<" new: "<<global_val++<<"address: "<<&global_val<<endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1,nullptr,Routine,(void*)"thread1");
    pthread_create(&tid2,nullptr,Routine,(void*)"thread2");
    pthread_create(&tid3,nullptr,Routine,(void*)"thread3");
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    return 0;
}

运行结果:

我们只要在全局变量前加上  __thread  ,此时所有的线程都在自己的栈上拿到了一份数据,我们可以观察到,此时全局变量打印出的地址是不同的,且变量是肚子增加的。


三. 线程的封装

线程封装

我们简单的对线程进行封装,使其能进行创建分离等待终止等功能

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <functional>
#include <pthread.h>
using namespace std;

static uint32_t number = 1;

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

private:
    void EnableDetach()
    {
        std::cout << "线程被分离了" << std::endl;
        _isdetach = true;
    }
    void EnableRunning()
    {
        _isrunning = true;
    }

    static void *Routine(void *argv)
    {
        Thread<T> *self = static_cast<Thread<T> *>(argv);
        self->EnableRunning();
        if (self->_isdetach)
            self->Detach();
        self->_func(self->_data); // 回调处理
        return nullptr;
    }

public:
    Thread(func_t func, T Data)
        : _tid(0), _isrunning(false), _isdetach(false), _Data(Data), _func(func)
    {
        _name = "Thread - " + to_string(_number++);
    }

    void Detach()
    {
        if (_isdetach)
            return;
        int n = pthread_detach(_tid);
        if (n != 0)
        {
            cerr << "fail to detach" << strerror(n) << endl;
        }
        else
        {
            cout << "success to detach" << endl;
            _isdetach = true;
        }
    }

    void Join()
    {
        if (_isdetach)
        {
            cout << "线程已经分离,无法进行等待" << endl;
            return;
        }
        int n = pthread_join(_tid, &res);
        if (n != 0)
        {
            cerr << "fail to join" << strerror(n) << endl;
        }
        else
        {
            cout << "success to join" << endl;
        }
    }

    bool Start()
    {
        if (_isrunning)
            return false;
        int n = pthread_create(&tid, nullptr, Routine, this);
        if (n != 0)
        {
            cerr << "fail to create pthread" << strerror(n) << endl;
            return false;
        }
        else
        {
            cout << "success to create pthread" << strerror(n) << endl;
            return true;
        }
    }

    bool Stop()
    {
        if (!_isrunning)
            return false;
        int n = pthread_cancel(tid);
        if (n != 0)
        {
            cerr << "fail to Stop" << strerror(n) << endl;
            return false;
        }
        else
        {
            cout << "success to Stop" << endl;
            _isrunning = false;
            return true;
        }
    }

    ~Thread()
    {
    }

private:
    string _name;
    pthread_t _tid;
    bool _isrunning;
    bool _isdetach;
    T _Data;
    void *res;
    func_t _func;
};

感谢各位观看,望多多支持!!!


网站公告

今日签到

点亮在社区的每一天
去签到