linux线程,线程控制与线程相关概念

news2024/11/20 11:07:08

线程概念

线程这个词或多或少大家都听过,今天我们正式的来谈一下线程;

在我一开始的概念中线程就是进程的一部分,一个进程中有很多个线程,这个想法基本是正确的,但细节部分呢我们需要细细讲解一下;

什么是线程

1.线程是进程执行流中的一部分,就是说线程是进程内部的一个控制序列;

2.线程是操作系统调度的基本单位;

3.在linux中没有真正意义上的线程,也就是操作系统中说的tcb(thread ctrl block),但是其他的操作系统是有的不同的操作系统实现不同(如windows就是在pcb下再次构建了tcb的数据结构);为什么linux下没有真正意义的线程呢?因为线程再操作系统中也是需要被管理的,可是线程的管理一定得创建数据结构,创建复杂的数据结构一定需要增加维护的成本与难度,而线程的管理其实和进程是相似的;所以聪明的linux程序员将线程管理设计为了轻量化的进程,将线程与进程统一管理,减轻了代码的复杂度,便于维护提高效率;(线程粒度细于进程

4.线程其实是进程的一部分,所以线程运行的地方就是在进程的虚拟地址空间中的;因为线程本身也是属于进程的一部分的,只是被加载到了进程队列中运行而已;(进程就像是一个家庭,线程就像是家庭中的每一个人,每个人都有自己的工作,所以需要分开执行,也就是处于进程队列中),进程会分配的资源给线程(家庭中的资源会分配给每个人,比如爸爸要去远的地方工作需要开车,那车子这个资源就会分配给父亲),这个资源包括代码和数据,之前我们理解的进程可以当作是主线程,通过分配自己的代码给它内部的线程,内部的线程拿到数据和代码资源区执行分配给它的工作,从而执行相应的操作;

重谈虚拟地址空间

页表如何映射

计算页表大小

 所以一个页表最大为4mb,并且一个页表的二级页表不一定为1024个,因为页表的映射也不是一次就完成的,而已页表的映射使用完之后还会释放等;所以一个页表大小不会大于4mb;

就是这样的页表完成了我们的映射;那我们的数据和代码都是存储在这个地址空间上的;而函数就是一个现成的地址,所以我们分配给线程代码数据,是不是可以直接将这个函数分给线程呢?这样不就等于把线程需要执行的工作划分给了线程吗?

所以线程划分资源本质上是将地址空间中的资源进行分配

为什么我们要创建线程?线程优点

1.同一进程中线程之间的切换更加轻量化;

在我们的内存中最快的是寄存器,,cpu之间拿寄存器中的数据进行计算,寄存器也需要获取数据,而寄存器不是之间从内存中拿数据的,因为内存相较于寄存器还是太慢了,所以它们之间还有一个cache缓存,这个cache中存放的是当前进程的数据和指令,寄存器可以很快的就从cache中拿到一个进程中的数据(cache命中率会很高,因为都在同一进程,都是热数据);因为同一进程中的线程是共享数据的,所以cache切换时只需要切换task_struct,而进程之前切换所有数据都需要切换(进程切换了,进程间具有独立性,cache中的数据一定都需要被切换,所咦数据会变冷重新去命中数据),这样的切换消耗会大的多;

2.创建和销毁线程的代价要小很多;因为线程的数据已经在内存中了,线程只需要从它所在的进程中获取数据即可;

3.io密集型程序,通过多线程可以提高很大的效率,在进行io的时候进程可以让其他线程进行计算等操作,不需要等待io结束再操作;相比单线程的等待要优化非常多;

4.计算密集型程序,在单核cpu中多线程没有什么提升,想法,线程之间的切换还会降低效率;但是在多核cpu中,多线程可以在多个核上进行计算(计算线程数要小于等于核的数量),也是大大提高了计算的效率的;

线程缺点:

由于线程之前没有独立性,共享进程代码数据,代码的健壮性要低一些,所以需要进行同步于互斥;缺乏访问控制->健壮性低;相应的调试也会更难;

线程数据 

每个线程虽然都是进程的一部分,从进程中获得数据的,但是线程一定需要包含自己的数据;

线程自己的数据:

1.线程对应的上下文数据(寄存器)

2.线程运行时数据(独立的栈空间)

3.线程id

4.信号屏蔽字

5.调度优先级

 6.errno

线程操作

上面讲解了线程的基本内容,下面我们来对线程进行操作来理解线程;

我们需要先了解这些linux中posix标准中的原生线程库中的函数; 

线程创建

pthread_create

这个函数是用来创建子线程的;

第一个参数是一个输出型参数,用来输出创建线程的tid;

第二个参数是用来设置线程的属性的,其实这是一个指向线程属性对象的指针,通过传递我们设置好的对象传递给线程从而改变线程的默认属性,一般我们都传递NULL使用默认属性即可;

第三个参数是一个回调函数,用来提供给线程运行的代码,可以理解为让线程执行此函数;

第四个参数就是一个传递给函数(第三个参数——回调函数)的参数,这个参数既可以是普通的内置类型,也可以是结构体,这样可以很多的数据;

返回值返回0为成功创建,创建失败返回返回错误码,不设置errno;

 下面可以看到我们的代码成功运行了;

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

void *routine(void *data)
{
    for (int i = 0; i < 5; i++)
    {
        usleep(100000);
        cout << "线程1, pid: " << getpid() << endl;
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, nullptr);
    for (int i = 0; i < 7; i++)
    {
        usleep(200000);
        cout << "线程0, pid: " << getpid() << endl;
    }
    return 0;
}

 从上面的现象我们可以清楚的知道线程是一个独立的执行流虽然routine函数和main函数它们两个再同一个程序中且是两个循环但是,这两个循环同时跑起来了,所以证明了线程的独立性;

编译时需要加-lpthread选项

在linux中使用原生线程库进行编程时我们编译选项总是需要带上-lpthread,这个选项在我们前面学习动静态库的时候就很熟悉了,用来连接指定的库;而似乎我们在以往的编程中除开我们自己创建动静态库的情况之外,我们从未出现过主动连接动静态库的情况;

为什么我们不需要主动去连接呢?这是因为编译器自动去帮我们连接了,我们的c,c++语言级别的库也好,linux的系统库也罢,它们库的路径都是已经存储在编译器的配置文件中的,编译器可以自动的找到库(第一步),然后编译器会自动连接这些库(第二步);为什么会自动连接呢?我们可以认为这些系统库和标准库是编译器自己的库,所以编译器会自动的连接;而pthread这个库是posix标准中的原生线程库;它是属于第三方库的,而第三方库即使它被放到系统,标准库的路径之下,它也是不会被自动连接的;所以我们需要带上-lpthread选项去主动连接这个库;

查看线程

我们看到的线程的现象接下来,我们从系统的角度的入手,使用系统的指令来查看我们的线程的体现;

ps -aL

lwp的全称是light weight process轻量级进程; 

线程的等待与tid获取函数

pthread_join

子进程被创建,父进程需要等待进程返回,而线程被创建也需要被等待,但是这里只有主线程和其他线程的区别,主线程需要等待其他所有线程,防止内存泄漏的问题;

 这里的第一个参数是指向被等待线程的tid;

第二个参数是一个输出型参数可以用来接收线程的返回值,这个返回值可以是任意类型的数据(自定义类型也可以);

返回值为0代表等待成功,非0则返回错误值,不设置errno码;

pthread_self

可以获得线程的tid;

这是一个无参函数和getpid的使用方式是一样的;

代码实现 

 知道了这些基本的函数后,我们下面用代码实践来展示现象并解释:

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

struct thread_data
{
    string threadName;
    string threadReturn;
};

void *routine(void *data)
{
    thread_data *d1 = static_cast<thread_data *>(data);
    // thread_data *d1 = (thread_data *)(data);
    int count = 3;
    for (int i = 0; i < count; i++)
    {
        printf("tid: %p threadName: %s count: %d\n", pthread_self(), d1->threadName.c_str(), i);
        sleep(1);
    }
    // int a=5/0;//除0错误 这里说明了当进程中的某个线程出现异常时,整个进程都会退出
    // exit(0);//使用exit退出 这里也说明了使用exit会退出整个进程
    d1->threadReturn="return_"+d1->threadName;
    return d1;
}

void initThread(thread_data *data, int num)
{
    data->threadName = "thread_" + to_string(num);
}


int main()
{
    pthread_t tid;
    thread_data *data = new thread_data;
    initThread(data, 1);
    int ret_create = pthread_create(&tid, nullptr, routine, (void *)data);
    void *ret_thread;
    printf("我是主线程tid: %p\n",pthread_self());
    pthread_join(tid, &ret_thread);
    cout << ((thread_data *)ret_thread)->threadReturn << endl;//证明获得了一个类返回值

    delete data;
    return 0;
}

 使用return正常退出的情况:

下面是使用exit和异常退出的情况: 

 

通过代码和现象我们可以知道这些细节:

1. 我们可以使用join获取线程的返回值,线程返回值可以为任意类型的指针,所以可以传递任意值;

2.我们的子线程退出的时候不能使用exit退出这样会导致整个进程都退出,我们可以使用return,pthread_exit(后面讲),使用cacel取消joined(后面讲),这3种方式退出;

3.进程中的任意一个线程出现异常整个进程都会退出

4.线程的tid是一个地址,这个地址是进程堆栈之间的内存区域(通过上面的现象也可清楚的明白)

由此我们可以知道这些函数的大致使用;

线程结构体位置

上面我们通过概念与实现基本的了解了线程,接下来我们通过图像来了解线程的结构体:

其实我们的线程是这样存在在我们的进程中的,因为linux程序员为了减轻代码的维护效率linux中没有真正的线程,而是将线程作为轻量级进程,而用来描述轻量级进程的结构体是存储在用户层的,存储的位置就是共享区的原生线程库,线程库中维护了线程的属性数据,内核的执行流(tcb控制块)通过找到进程中的线程库中的线程结构体从而找到线程代码执行线程; 

所以线程的属性是由线程库来维护的,而tid之所以是共享区之中的代码的原因就是因为tid指的是共享区中线程库中的某个线程结构体所在的首地址;

线程空间的特点

1.线程之间的栈空间是独立的;

这一点非常好理解,因为函数在被调用的时候就会创建自己的栈帧嘛;而线程执行其实就是执行了分给他的函数;所以线程栈空间是独立的;

2.线程之间是没有秘密的;

为什么线程之间独立但是又没有秘密呢?因为线程总是在一个进程中的嘛,栈之间的数据,只需要通过一个指针就可以获得了;

代码示例:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <vector>
#include <string>
using namespace std;

struct threadData
{
    string threadName;
    threadData(int num)
    {
        threadName = "thread" + to_string(num);
    }
    threadData() = default;
};

int *g_index;

void *routine(void *args)
{
    int val = 0;
    threadData *data = (threadData *)args;
    for (int i = 1; i <=3 ; i++)
    {
        printf("%s tid: %p val: %d\n", data->threadName.c_str(), pthread_self(), val);
        val++;
    }
    if(data->threadName=="thread1")
    {
        val=10000;
        g_index=&val;
        sleep(5);
    }
    return (void *)0;
}

int main()
{
    vector<pthread_t> tids;
    for (int i = 0; i < 3; i++)
    {
        pthread_t tid;
        threadData *td = new threadData(i);
        pthread_create(&tid, nullptr, routine, td);
        tids.push_back(tid);
        sleep(1);
    }
    cout<<"这是thread2的val值: "<<*g_index<<endl;
    for(auto t:tids)
    {
        void *retData;
        pthread_join(t,&retData);
    }
    return 0;
}

但是如果我们想要获得某个栈空间的数据时这也是可以轻松做到的:

我们在routine函数中加入一段这样的代码,并在main函数中读取数据;

  routine函数中:

    if(data->threadName=="thread1")
    {
        val=10000;
        g_index=&val;
        sleep(5);
    }
main函数中:
    
       cout<<"这是thread2的val值: "<<*g_index<<endl;

 

线程的变量:__thread选项 

int *g_index;
//int g_val;
__thread int g_val;

void *routine(void *args)
{
    int val = 0;
    threadData *data = (threadData *)args;
    for (int i = 1; i <=3 ; i++)
    {
        //printf("%s tid: %p val: %d\n", data->threadName.c_str(), pthread_self(), val);
        //val++;
        printf("%s g_val: %d\n",data->threadName.c_str(),g_val);
        g_val++;
    }
    // if(data->threadName=="thread1")
    // {
    //     val=10000;
    //     g_index=&val;
    //     sleep(5);
    // }
    return (void *)0;
}

int main()
{
    vector<pthread_t> tids;
    for (int i = 0; i < 3; i++)
    {
        pthread_t tid;
        threadData *td = new threadData(i);
        pthread_create(&tid, nullptr, routine, td);
        tids.push_back(tid);
        sleep(1);
    }
    //cout<<"这是thread2的val值: "<<*g_index<<endl;
    for(auto t:tids)
    {
        void *retData;
        pthread_join(t,&retData);
    }
    return 0;
}

我们线程在使用 g_val全局变量时:

g_val带上__thread编译选项时:

 

__thread是编译选择,不是c,c++的语法是编译器的选项;

特点:

 1.将进程全局数据变为线程全局数据

2.只能给内置类型带上这个选项

C++线程库说明

在我们的C++中是有语言级别的线程库的(C语言没有),C++中的线程库是跨平台的,但是我们在使用C++线程库时,我们还是会发现,我们需要带上编译选项-lpthread所以说明C++的线程库是封装了原生线程库的,而原生线程库在linux中是posix标准的,在windows中又有不同的标准;但是C++的线程库是跨平台的,所以说明C++的线程库不仅封装了linux的posix标准线程库还封装了windows下的线程库;

clone系统调用的封装

我们前面说线程是轻量级的进程,为什么这么说呢?其实我们在创建线程时使用的pthread_create函数和创建子进程的fork函数都是封装了clone的系统调用;

int clone(int (*fn)(void *), void *child_stack
, int flags, void *arg
, ... /* pid_t *ptid, void *tls, pid_t *ctid */);

这个系统系统调用会指定一片栈空间给新开辟的线程,我们不需要懂clone调用的细节,我们只需要知道,linux中其实在底层上线程的接口也是和进程用的一样的调用,所以它们在内核层面上是处于同一级别的执行流的,所以线程被称为轻量级进程;

小提示:

线程如何使用进程替换的调用会将当前的整个进程替换掉

线程终止

前面我们说了线程的3个正常退出方式;我们下面来详细的讲解一下:

pthread_exit

这个函数就是和return一样的作用,返回一个retval给主线程;这里需要注意的是retval最好是堆上的指针,线程终止栈帧也会销毁,会导致栈上的数据被释放,所以返回值一定要是不被释放的数据;

pthread_cancel

这是一个线程终止函数,我们可以通过此函数终止掉tid的线程:

这里终止了就不需要再join了,如果join了会发返回非0值; 

这是gpt给出的提示:

尽管 pthread_cancel 函数可以请求取消另一个线程,但是线程是否真正被取消,以及何时被取消,是由目标线程自身来决定的。目标线程可以选择忽略取消请求,或者在适当的时机响应取消请求并执行清理操作。

 线程分离

pthread_detach

我们的主线程永远是最后退出的,因为需要等待所有创建进程退出,我们常见的服务器一般都是死循环不退出的程序;而当主线程不关系创建的线程的结果时,可以使用detach来断开创建线程与主线程之间的关系;,主线程就不需要等待子线程了;

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include<cstring>
using namespace std;

void* routine(void*args)
{
    cout<<"我是被创建线程"<<endl;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,routine,nullptr);
    void* ret;
    pthread_detach(tid);
    int ret_join=pthread_join(tid,&ret);
    printf("%s\n",strerror(ret_join));
    return 0;
}

当没有detach时:

当创建的线程被detach时

 所以说明线程不能被同时detach和join;

此外线程可以自己detach自己;

以上就是线程的控制与基本概念,线程部分未完待续; 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1709603.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OKR 实践:来自一位信息技术部主管的成功秘诀

OKR 实践&#xff1a;来自一位信息技术部主管的成功秘诀 为什么选择OKR 公司信息技术部为38个各地分公司、12,000名员工的IT需求提供服务。庞大而多样的客户群常常使我们的团队分散&#xff0c;许多团队都在各自为政&#xff0c;以个案为基础解决问题&#xff0c;而不是采用企业…

CSS文本粒子动画特效之爱心粒子文字特效-Canvas

1. 效果图 2.完整代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><style>body,html {margin: 0;paddin…

Sqoop的安装与测试

这里写目录标题 什么是Sqoop?Sqoop的安装与配置安装测试 什么是Sqoop? Sqoop就是hadoop和mysql的一个中间介质 , 作用就是可以将hadoop中的数据传到mysql中 , 或将mysql中的数据导入到hadoop中 Sqoop的安装与配置 安装 详细代码 //解压安装 [roothadoop soft]# tar -zxv…

国产数据库替代加速 助力数字中国建设

5月24日&#xff0c;随着第七届数字中国建设峰会在福州的成功举办&#xff0c;释放数据要素价值、发展新质生产力成为当下热议的话题。 数据作为新型生产要素&#xff0c;是数字化、网络化、智能化的重要基础。北京人大金仓信息技术股份有限公司&#xff08;以下简称人大金仓&a…

AI智能体研发之路-模型篇(三):中文大模型开、闭源之争

博客导读&#xff1a; 《AI—工程篇》 AI智能体研发之路-工程篇&#xff08;一&#xff09;&#xff1a;Docker助力AI智能体开发提效 AI智能体研发之路-工程篇&#xff08;二&#xff09;&#xff1a;Dify智能体开发平台一键部署 AI智能体研发之路-工程篇&#xff08;三&am…

10.RedHat认证-Linux文件系统(上)

10.RedHat认证-Linux文件系统(上) ⽂件系统&#xff0c;顾名思义&#xff0c;是⼀个组织⽂件的“系统(system)”。file system ⽂件系统是⽤来组织⽂件的&#xff0c;通俗⼀点理解的话&#xff0c;⽂件系统是⽤来存储⽂件的。 硬盘是不能直接存放⽂件或数据。 我们通过将硬…

【ARM+Codesys案例】T3/RK3568/树莓派+Codesys绕线机控制方案—运动控制器,支持定制

绕线机控制方案 SC 系列运动控制器 绕线机就是把线状的物体缠绕到特定的工件上的机器。凡是电器产品大多需要用漆包铜线(简称漆包线)绕制成电感线圈。绕线机从线圈设计、参数分析、数控编程、到自动补偿技术的实现、整个绕线工艺过程自适应诊断及控制、排线部分运动自适应干涉…

鸿蒙OS开发:【一次开发,多端部署】(一多天气)项目

一多天气 介绍 本示例展示一个天气应用界面&#xff0c;包括首页、城市管理、添加城市、更新时间弹窗&#xff0c;体现一次开发&#xff0c;多端部署的能力。 1.本示例参考一次开发&#xff0c;多端部署的指导&#xff0c;主要使用响应式布局的栅格断点系统实现在不同尺寸窗…

IMU应用于评估脊髓损伤患者的膝关节痉挛

近日&#xff0c;美国西北大学团队利用便携式IMU系统精确量化脊髓损伤&#xff08;SCI&#xff09;患者膝关节伸肌痉挛的程度&#xff0c;不仅验证了IMU系统的可靠性与准确性&#xff0c;还强调了其在动态评估痉挛变化方面的独特贡献。 研究团队创新性地将IMU技术引入到经典的…

web前端之vue动态访问静态资源、静态资源的动态访问、打包、public、import、URL、Vite

MENU 静态资源与打包规则动态访问静态资源直接导入将静态资存放在public目录中动态导入URL构造函数结束语实践与坑附文 静态资源与打包规则 介绍 Vite脚手架在打包代码的时候&#xff0c;会把源代码里对于静态资源的访问路径转换为打包后静态资源文件的路径。主要的区别是文件指…

neo4j详细安装教程

前言 最近开始学习知识图谱&#xff0c;现整理Neo4j的详细安装教程&#xff0c;Neo4j是一个高性能的,NOSQL图形数据库&#xff0c;它将结构化数据存储在网络上而不是表中。由于知识图谱中存在大量的关系型信息&#xff08;实体—关系—实体&#xff09;, 使用结构化数据库进行存…

推送镜像到私有harbor仓库

本地已制作镜像&#xff1a;tomcat-8.5.100-centos7.9:1.0。 本地已经搭建私有仓库&#xff1a;harbor.igmwx.com。 现在需要把镜像 tomcat-8.5.100-centos7.9:1.0 推送到harbor。 &#xff08;1&#xff09;查看本地镜像&#xff1a;sudo docker images zhangzkzhangzk:~/d…

服务器数据恢复—RAID5阵列崩溃如何恢复上层OA和oracle数据库的数据?

服务器数据恢复环境&故障&#xff1a; 某公司的一台服务器中的raid5磁盘阵列有两块磁盘先后掉线&#xff0c;服务器崩溃。故障服务器的操作系统为linux&#xff0c;操作系统部署了oa&#xff0c;数据库为oracle。oracle数据库已经不再对该oa系统提供后续支持&#xff0c;用…

企业如何实现数据采集分析展示一体化

在当今数字化时代&#xff0c;企业越来越依赖于数据的力量来驱动决策和创新。通过全量实时采集各类数据&#xff0c;并利用智能化工具进行信息处理&#xff0c;企业能够借助大数据分析平台深入挖掘数据背后的价值&#xff0c;从而为企业发展注入新动力。 一、企业痛点 随着数字…

卢文岩博士受邀参与中国科学院大学校友论坛 解码DPU核心价值

近日&#xff0c;第五届中国科学院大学校友创新论坛正式举行&#xff0c;本次论坛聚焦科技前沿领域&#xff0c;旨在搭建高端对话平台&#xff0c;促进产学研深度融合。在大算力时代——AI技术前沿沙龙上&#xff0c;中科驭数高级副总裁、CTO卢文岩博士受邀分享《DPU——连接算…

【IC】partial good

假设单core良率80%&#xff0c;core pass 数量分布呈二项分布。 16个core全pass的概率为&#xff1a; 有n个core pass的概率为&#xff1a; 分布如下&#xff1a; 当np>5且nq>5时&#xff0c;二项分布近似服从正态分布

索引下推详情-简单入手

一.概念 索引下推&#xff08;Index Pushdown&#xff09;MySQL5.6添加的&#xff0c;是一种优化技术&#xff0c;用于在查询执行时将部分计算移动到存储引擎层&#xff0c;从而减少数据传输和计算的开销&#xff08;减少回表查询次数&#xff09;&#xff0c;提高查询性能。 …

Java核心: Stream流的实现原理

Java 8之后我们对Stream的使用都已经习以为常了&#xff0c;它帮助我们从怎么做的细节里脱身&#xff0c;只要告诉它做什么即可。这一篇文章我们主要讲Java Stream的实现原理&#xff0c;手写一个Stream框架&#xff0c;然后再来讲解Java Stream的核心类&#xff0c;做到知其然…

一分钟学习数据安全——数字身份的三种模式

微软首席身份架构师金卡梅隆曾说&#xff1a;互联网的构建缺少一个身份层。互联网的构建方式让你无法得知所连接的人和物是什么。这限制了我们对互联网的使用&#xff0c;并让我们面临越来越多的危险。如果我们坐视不管&#xff0c;将面临迅速激增的盗窃和欺诈事件&#xff0c;…

富唯智能镀膜上下料设备采用最新的技术

现代工业竞争日趋激烈&#xff0c;高效生产已成为企业持续发展的关键。我们的设备不仅实现了高速上下料&#xff0c;更通过智能化控制系统实现了对生产流程的精准监控和调整&#xff0c;轻松应对高强度生产需求。 1、快速响应&#xff0c;高效生产 富唯智能镀膜上下料设备采用…