Linux_线程的使用

news2024/9/20 16:50:10

目录

1、线程与进程的关系 

2、线程的优缺点

3、创建线程

4、查看启动的线程

5、验证线程是共享地址空间的

6、pthread_create的重要形参

6.1 线程id 

6.2 线程实参

7、线程等待  

8、线程退出

9、线程取消 

10、线程tcb 

10.1 线程栈

11、创建多线程 

12、__thread 

13、线程分离 

结语 


前言:

        线程是操作系统进行调度的基本单位,他属于进程的子集。在Linux下,通过实现轻量化进程来实现线程,因此线程具有进程的相关特性,比如线程必须有自己的代码资源,有属于自己独立的数据空间,并且同一个进程下的线程所看到的地址空间是属于该进程的,因为创建线程实际上就是在该进程下创建task_struct结构体(该结构体的作用是方便操作系统对该执行流的调度),这些task_struct结构体跟进程共用空间资源,只不过线程可以在单一进程执行流的基础上实现多执行流并发式的运行代码,以至于提高cpu的效率。

1、线程与进程的关系 

         说到线程就离不开进程的概念,因为线程是在进程的基础上实现的,多线程是底层就是创建了多个task_struct结构体作为进程的执行分支,但是他们依然是共用进程的数据资源,线程示意图如下:

        当系统里创建一个进程,则系统需要给该进程分配新的地址空间、页表、物理内存等等空间资源,所以说进程是系统分配资源的实体。但是当系统里有了新的线程则不会给线程分配新的空间资源,而是给让线程使用进程的空间资源。


        线程的独立部分:     

        线程虽然和进程共用地址空间,但是线程也有自己独立的部分,比如:线程ID, 保存上下文的寄存器,线程栈,errno,block信号集,调度优先级。

2、线程的优缺点

        线程的优点:

        1、当我们需要并发执行代码时,创建一个线程的工作比创建一个进程的工作要小得多。

        2、当cpu切换PCB时,切换线程的效率比切换进程的效率略高。

        3、线程所占用的资源小于进程。

        4、线程之间的通信代价比进程的要小。

        5、提高程序的并发性。

        线程的缺点:

        1、若单个线程收到信号退出,则会导致整个进程都退出。

        2、多线程访问共同资源时是不受保护的,会导致意料之外的错误。

        3、编写多线程的难度很高。 

        4、若单个线程因为异常崩溃,则会导致整个进程崩溃。

3、创建线程

        创建线程需要用到的接口如下:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
//thread是一个执行类型为pthread_t的变量的指针
//attr是一个指针,他指向的结构体包含新线程的各种属性,设为nullptr则表示采用默认属性
//start_routine是新线程要执行的函数,他接收一个void*,返回值一个void*
//arg表示新线程要执行的函数的实参

         创建线程前需要先定义一个类型为pthread_t的变量作为实参传递给函数pthread_create,该变量的作用是让用户可以通过他找到对应的线程,创建线程的代码如下:

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

using namespace std;

void *threadRun(void* args)
{
    while(1)
    {
        cout << "子线程: " << getpid() << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;//先定义一个pthread_t类型的变量
    //该函数调用完成后会赋予tid新的值,表示该线程的id
    pthread_create(&tid, nullptr, threadRun, nullptr);

    while(1)
    {
        cout << "主线程: " << getpid() << endl;
        sleep(1);
    }
}

        测试结果:

        注意:使用线程的接口时要在编译的时候要手动链接pthread库,如下图:

4、查看启动的线程

        在Linux下,使用指令:ps -aL,就可以查看用户启动的线程了。如下图: 

        LWP表示轻量级进程的pid,即线程的pid,LWP是给系统调度线程专门设置的标识符。 

5、验证线程是共享地址空间的

        定义一个全局变量,若所有线程只能看到唯一一份全局变量,那么就可以证明进程下的所有线程用的是同一个地址空间,代码如下:

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

using namespace std;

int g_val = 10;
// 新线程
void *threadRoutine(void *args)
{
    while (true)
    {
        printf("子线程 pid: %d, g_val: %d, \
        &g_val: 0x%p\n", getpid(), g_val, &g_val);
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr); 

    while (true)
    {
        printf("主线程 pid: %d, g_val: %d,\
        &g_val: 0x%p,\n", getpid(), g_val, &g_val);
        sleep(1);
        g_val++;
    }

    return 0;
}

         运行结果:

        从结果可以看到,哪怕主线程对全局变量进行更改,其他线程拿到的值是更改后的值,若是父子进程关系,则另一方会发生写时拷贝,线程之间没有这么做,说明线程是共享地址空间的。 

6、pthread_create的重要形参

6.1 线程id 

        线程有两个标识符,一个是LWP,是给系统看的,另一个是线程id,是给用户看的。线程id就是创建线程时定义的pthread_t类型的变量,该变量作为pthread_create的输出型参数,在调用完pthread_create后该变量保存的就是线程id了。

        查看线程id的代码如下:

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

using namespace std;

int g_val = 10;
// 新线程
void *threadRoutine(void *args)
{
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr); 

    while (true)
    {
        printf("主线程 pid: %d, g_val: %d,\
        &g_val: 0x%p,子线程id:%p \n", getpid(), g_val, &g_val,tid);
        sleep(1);
        g_val++;
    }

    return 0;
}

        运行结果:

        从测试结果发现,线程id实际上就是一串地址,这个地址就是线程在地址空间内的映射,间接说明了线程的管理是在用户空间内的进行,并不是由操作系统像管理进程PCB一般在内核空间进行,具体看下文线程tcb。

6.2 线程实参

        线程的任务就是执行pthread_create的函数指针,并且该函数具有一个void*的形参,那么如何使用该void*的形参呢?

        测试代码如下:

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

using namespace std;

// 新线程
void *threadRoutine(void *args)
{
    char *name = static_cast<char *>(args);//需要强转
    while (true)
    {
        cout << name << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void *)"线程1");//需要强转

    while (true)
    {
    }

    return 0;
}

         运行结果:

7、线程等待  

        子线程退出时主线程也要对其进行等待,等待的原因和父子进程一样,防止内存泄漏和回收退出信息,若不等待线程,则线程的tast_struct会一直存在,会造成不必要的资源浪费,进行线程等待的接口介绍如下:

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
//thread表示要等待的线程id
//retval是一个二级指针,是一个输出型参数,目的是拿到线程返回的void*

         线程等待的测试代码如下:

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

using namespace std;

// 新线程
void *threadRoutine(void *args)
{
    int count = 5;
    while (count)
    {
        count--;
        sleep(1);
    }
    return (void*)1;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);

    void* retval;
    pthread_join(tid,&retval);//等待子线程

    cout<<"线程等待成功:"<<(long long)retval<<endl;
    return 0;
}

        运行结果:

        由于线程执行的函数虽然可以返回一个void*的变量,但是我们却没有办法接收该返回值,因为这不是一个简单的函数调用。所以只能通过调用pthread_join,然后传递一个二级指针给他,pthread_join就可以通过输出型参数把void*变量给带出来。

8、线程退出

        进程退出常常用exit函数,只要一个进程调用了exit函数,则该进程就直接结束了。但是若想仅仅退出一个线程,则不能用exit,因为当一个线程用exit退出,就会把整个进程退出。线程退出有专门的退出函数,该函数介绍如下:

#include <pthread.h>

void pthread_exit(void *retval);
//该函数会退出当前线程,并且返回一个void*变量

         线程退出测试代码如下:

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

using namespace std;

// 新线程
void *threadRoutine(void *args)
{
    int count = 5;
    while (count)
    {
        count--;
        sleep(1);
    }
    //return (void*)1;
    pthread_exit((void*)100);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);

    void* retval;
    pthread_join(tid,&retval);

    cout<<"线程等待成功:"<<(long long)retval<<endl;
    return 0;
}

        运行结果:

9、线程取消 

        可以调用函数pthread_cancel可以在线程退出前取消该线程,该函数介绍如下:

#include <pthread.h>

int pthread_cancel(pthread_t thread);
//thread表示要取消的线程

         线程取消测试代码如下:

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

using namespace std;

// 新线程
void *threadRoutine(void *args)
{
    int count = 5;
    while (count)
    {
        count--;
        sleep(1);
    }
    pthread_exit((void*)100);
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);

    pthread_cancel(tid);

    void* retval;
    pthread_join(tid,&retval);

    cout<<"线程等待成功:"<<(long long)retval<<endl;
    return 0;
}

        运行结果:

        如果一个线程被取消,则该线程的退出码为-1。 

10、线程tcb 

         上文讲到线程id是给用户看的,通过上述代码打印线程id,可以观察到线程id实际上就是一串虚拟地址,线程id的值如下:

        分析id的地址,可以发现该地址数属于进程地址空间的共享区内,说明线程本身就是存储在用户空间内的,但是我们自己并没有对线程做任何管理,那么线程是如何被管理的呢?可以从函数pthread_create得知,当我们调用pthread_create后,该函数就会返回一个线程id给到我们,说明该函数内部会自己维护线程,而该函数的实现是存储在线程库(pthread.so)里的,而线程库会在程序运行起来时加载到内存并映射在共享区内,所有可以得出一个结论:线程由线程库维护,并映射在地址空间的共享区内


        具体示意图如下:

        从上图可以发现,tcb就是管理线程的结构体,线程库以维护tcb从而维护线程,而tid就是线程id,他就是tcb的首地址,这也就很好的解释了为什么可以通过线程id找到对应的线程了,这个地址是线程库为用户申请,也就是用户调用函数pthread_create后所得到的地址。并且不同的进程创建的线程都会被线程库在内存中统一管理,只是这些线程会分别映射到他们的进程共享区中

10.1 线程栈

         从上图可以发现除了地址空间的栈空间外,线程tcb中也维护一个名为线程栈的空间,而线程栈是采用数组的方式模拟出来的,这些模拟栈被保存在共享区,由线程库来维护。栈与栈之间相互独立,不可直接访问,但是同一进程下的其他线程采用特别的方式也可以访问到对方的栈,因为毕竟都在同一个地址空间内。

11、创建多线程 

        创建多线程的思路:利用循环定义多个线程id,但是由于新的循环会覆盖旧的线程id,因此需要把每个线程id存入容器中,方便后续等待线程时能够找到他们的线程id,创建多线程代码如下:

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

using namespace std;
#define NUM 4
struct threadData//给每个线程做标记
{
    string threadname;
};

// 所有的线程,执行的都是这个函数?
void *threadRoutine(void *args)
{
    threadData *td = static_cast<threadData *>(args);
    int i = 5;
    while (i)
    {
        cout << "我是" << td->threadname << ", pid: " << getpid() << endl;
        sleep(1);
        i--;
    }

    delete td;
    return nullptr;
}

void InitThreadData(threadData *td, int number)
{
    td->threadname = "线程-" + to_string(number);
}

int main()
{
    // 创建多线程!
    vector<pthread_t> tids;
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        threadData *td = new threadData;
        InitThreadData(td, i);//标记线程

        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
    }

    //线程等待  
    for (int i = 0; i < tids.size(); i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

         运行结果:

12、__thread 

         __thread只能修饰内置类型,不能修饰自定义类型,他修饰的变量对于所有线程是可见的,有点类似全局变量,但是他跟全局变量的区别在于:__thread修饰的变量对于每个线程而言是独立的,换句话说,线程对该变量的修改不会影响其他线程所看到的值。

        测试代码如下:

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

using namespace std;

__thread int a = 10;

void *threadRun(void* args)
{
    while(1)
    {
        cout << "子线程: " << getpid() <<" a的值:"<<a++<<endl;//线程对a进行++
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;//先定义一个pthread_t类型的变量
    //该函数调用完成后会赋予tid新的值,表示该线程的id
    pthread_create(&tid, nullptr, threadRun, nullptr);

    while(1)
    {
        cout << "主线程: " << getpid() <<" a的值:"<<a<< endl;//此处a的值还是10吗?
        sleep(1);
    }
}

        运行结果:

        从结果可以看到,主线程的a是独立于子线程的a,原因就是a作为全局变量被__thread修饰了,因此所有线程都有一份独立的a。 

13、线程分离 

        主线程创建的子线程退出后,主线程若想拿到子进程退出的void*返回值,则主线程要对其进行join等待操作,但是若主线程不关心其返回值,则就没必要进行等待,因为等待也是一种负担。因此在这种情况下,可以让该子线程自行分离,即分离的子线程在退出后会自动释放空间资源。

        分离的函数介绍如下:

#include <pthread.h>

int pthread_detach(pthread_t thread);
//thread表示要分离的线程

         线程分离测试代码如下:

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

using namespace std;


void *threadRun(void* args)
{
    pthread_detach(pthread_self());//pthread_self返回该线程的id
    int count = 3;
    while(count--)
    {
        cout << "子线程: " << getpid() <<endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, nullptr);

    sleep(1);//要保证pthread_detach在pthread_join之前触发
    int n = pthread_join(tid, nullptr);
    printf("n = %d, who = 0x%x, why: %s\n", n, tid, strerror(n));
}

        测试结果:

        n = 22表示等待失败了,说明这些线程已经被分离了。 

结语 

         以上就是关于使用线程的讲解,线程是核心思想是创建多个执行流让程序实现并行运行,目的就是提高程序执行的效率,本文主要讲述如何创建线程和使用线程,包括线程的基本用法和概念,线程本身涉及的知识非常广,细节也特别多,在复杂的多线程下往往要考虑更多的东西。

        最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!

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

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

相关文章

学生信息管理系统设计

学生信息管理系统的设计是一个综合性的项目&#xff0c;涉及到数据的存储、检索、更新和删除等基本操作&#xff0c;同时也需要考虑系统的易用性、安全性和扩展性。以下是一些关键步骤和要素&#xff0c;用于指导设计这样一个系统&#xff1a; 1. 需求分析 目标用户&#xff…

wls2下的centos使用桥接模式连接宿主机网络独立静态ip

前提&#xff1a;wsl2已安装&#xff0c;可正常更新 1.在控制面板中&#xff0c;打开开启或关闭windows功能&#xff0c;将里面的 Hyper-V功能打开&#xff0c;此处涉及重启 2. 按一下win键&#xff0c;输入hy&#xff0c;上面可以看到Hyper-V Manager,点进去 3.选择右边的 Vi…

Redis系列命令更新--Redis有序集合命令

Redis有序集合&#xff08;sorted set&#xff09; &#xff08;1&#xff09;说明&#xff1a; A、Redis有序集合和集合一样也是string类型元素的集合&#xff0c;且不允许重复的成员&#xff1b;不同的是每个元素都会关联一个double类型的分数&#xff1b;redis正式通过分数…

Java语言程序设计——篇五(1)

数组 概述数组定义实例展示实战演练 二维数组定义数组元素的使用数组初始化器实战演练&#xff1a;矩阵计算 &#x1f4ab;不规则二维数组实战演练&#xff1a;杨辉三角形 概述 ⚡️数组是相同数据类型的元素集合。各元素是有先后顺序的&#xff0c;它们在内存中按照这个先后顺…

【ProtoBuf】通讯录实现(网络版)

Protobuf 还常用于通讯协议、服务端数据交换场景。那么在这个示例中&#xff0c;我们将实现一个网络版本的通讯录&#xff0c;模拟实现客户端与服务端的交互&#xff0c;通过 Protobuf 来实现各端之间的协议序列化。 需求如下&#xff1a; 客户端可以选择对通讯录进行以下操…

电脑文件恢复哪个好?分享四个建议记住常备的方法!

当我们发现电脑误删文件的时候&#xff0c;一定会感到焦虑和困惑&#xff0c;但是一味地焦虑和困惑是没有任何帮助的。我们需要保持冷静&#xff0c;然后通过以下几个方法找回。 电脑文件恢复的方法有很多&#xff0c;选对适合自己的数据恢复软件很重要&#xff0c;本文罗列了几…

引领小模型潮流!OpenAI发布功能强大且成本低的GPT-4o mini

GPT-4o mini的成本比GPT-3.5 Turbo低了超过60%&#xff0c;其聊天表现优于Google的Gemini Flash和Anthropic的Claude Haiku。该模型从周四开始对ChatGPT的免费用户、ChatGPT Plus用户和团队订阅用户开放&#xff0c;并将在下周向企业用户开放。OpenAI计划未来将图像、视频和音频…

Linux——五种IO模型

目录 一、I/O的理解 二、五种IO模型 1.阻塞式IO 2.非阻塞式IO 3.信号驱动IO 4.多路复用IO 5.异步IO 一、I/O的理解 I/O的本质就是输入输出&#xff0c;C语言的stdio&#xff0c;C的iostream&#xff0c;添加了这两个库&#xff0c;我们才能够进行printf、scanf、cin、c…

UDP网口(1)概述

文章目录 1.计算机网络知识在互联网中的应用2.认识FPGA实现UDP网口通信3.FPGA实现UDP网口通信的方案4.FPGA实现UDP网口文章安排5.传送门 1.计算机网络知识在互联网中的应用 以在浏览器中输入淘宝网为例&#xff0c;介绍数据在互联网是如何传输的。我们将要发送的数据包称作A&a…

人工智能AI合集:1、嵌入式LinuxAI开发套件OrangePI AIPRO初体验

前言 随着人工智能技术的飞速发展&#xff0c;AI已经不再是遥不可及的高科技概念&#xff0c;而是逐渐融入到我们的日常生活中。从智能手机的语音助手到家庭中的智能音箱&#xff0c;再到工业自动化和医疗诊断&#xff0c;AI的应用无处不在。然而&#xff0c;要想真正掌握并应用…

数学建模学习(111):改进遗传算法(引入模拟退火、轮盘赌和网格搜索)求解JSP问题

文章目录 一、车间调度问题1.1目前处理方法1.2简单案例 二、基于改进遗传算法求解车间调度2.1车间调度背景介绍2.2遗传算法介绍2.2.1基本流程2.2.2遗传算法的基本操作和公式2.2.3遗传算法的优势2.2.4遗传算法的不足 2.3讲解本文思路及代码2.4算法执行结果&#xff1a; 三、本文…

基于java的设计模式学习

PS &#xff1a;以作者的亲身来看&#xff0c;这东西对于初学者来说有用但不多&#xff0c;这些东西&#xff0c;更像一种经验的总结&#xff0c;在平时开发当中一般是用不到的&#xff0c;因此站在这个角度上用处不大。 1.工厂模式 1.1 简单工厂模式 我们把new 对象逻辑封装…

SpringBoot缓存注解使用

背景 除了 RedisTemplate 外&#xff0c; 自Spring3.1开始&#xff0c;Spring自带了对缓存的支持。我们可以直接使用Spring缓存技术将某些数据放入本机的缓存中&#xff1b;Spring缓存技术也可以搭配其他缓存中间件(如Redis等)进行使用&#xff0c;将某些数据写入到缓存中间件…

【Linux】信号(signal)

目录 一、信号概念&#xff1a; 二、信号的常见状态&#xff1a; 信号递达&#xff1a; 信号未决&#xff1a; 阻塞信号&#xff1a; 忽略信号&#xff1a; 信号在内核中的表示&#xff1a; 三、信号相关函数&#xff1a; sigset_t &#xff08;类型&#xff09;&…

2024.7.19 作业

1.链表的排序 int list_sort(NodePtr L) {if(NULLL || L->len<1){printf("排序失败");return -1;}int lenL->len1;NodePtr p;int i,j;for( i1;i<len;i){for( j0,pL;j<len-i;j,pp->next){if( p->data > p->next->data ){datatype tp-&…

基于51单片机的步进电机控制系统proteus仿真

地址&#xff1a;https://pan.baidu.com/s/1jFlIJ9I5qxjW8sYKd6vrBQ?pwd9d6q 提取码&#xff1a;1234 仿真图&#xff1a; 芯片/模块的特点&#xff1a; AT89C52/AT89C51简介&#xff1a; AT89C52/AT89C51是一款经典的8位单片机&#xff0c;是意法半导体&#xff08;STMic…

阿里开源的音频模型_原理与实操

英文名称: FunAudioLLM: Voice Understanding and Generation Foundation Models for Natural Interaction Between Humans and LLMs 中文名称: FunAudioLLM: 人与LLMs之间自然互动的语音理解和生成基础模型 论文地址: http://arxiv.org/abs/2407.04051v3 相关论文&#xff1a;…

1、springboot3 vue3开发平台-后端-项目构建

文章目录 1. 创建项目1.1 前置环境条件1.2 项目创建 2. 模块配置2.1 父工程配置概述2.2 配置启动模块2.3 父工程相关依赖管理 1. 创建项目 1.1 前置环境条件 idea2023, jdk17 1.2 项目创建 创建父工程并删除不需要的文件目录&#xff1a; 右键父工程依次创建其他模块 最…

Java | Leetcode Java题解之第260题只出现一次的数字III

题目&#xff1a; 题解&#xff1a; class Solution {public int[] singleNumber(int[] nums) {int xorsum 0;for (int num : nums) {xorsum ^ num;}// 防止溢出int lsb (xorsum Integer.MIN_VALUE ? xorsum : xorsum & (-xorsum));int type1 0, type2 0;for (int n…

vue2.0结合使用 el-scrollbar 和 v-for实现一个横向滚动的元素列表,并且能够自动滚动到指定元素(开箱即用)

效果图&#xff1a; 代码&#xff1a; <div class"gas-mode-item-body"><el-scrollbar style"width: 300px;height: 100%;" wrap-style"overflow-y:hidden" ref"scrollbarRef"><div style"display: flex&quo…