linux从青铜到王者第十三篇:linux多线程四万字详解(代码片段)

森明帮大于黑虎帮 森明帮大于黑虎帮     2022-12-14     618

关键词:

系列文章目录


文章目录


前言


一、Linux线程概念

1.什么是线程

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列。
  • 一切进程至少都有一个执行线程。
  • 线程在进程内部运行,本质是在进程地址空间内运行。
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

2.线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多。
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
  • 线程占用的资源要比进程少很多。
  • 能充分利用多处理器的可并行数量。
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

3.线程的缺点

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

4.线程的异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	#include<sys/types.h>
     5	using namespace std;
     6	
     7	void* thread_run(void* arg)
     8	
     9	    pthread_detach(pthread_self());
    10	    while(1)
    11	    
    12	        cout<<(char*)arg<<pthread_self()<<" pid:"<<getpid()<<endl;
    13	        sleep(1);
    14	        break;
    15	    
    16	    int a=10;
    17	    a=a/0;
    18	    return (void*)10;
    19	
    20	int main()
    21	
    22	    pthread_t tid;
    23	    int ret=0;
    24	    ret= pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    25	    if(ret!=0)
    26	    
    27	        return -1;
    28	       
    29	    
    30	    sleep(10);
    31	    pthread_cancel(tid);
    32	    cout<<"new thread "<<tid<<" be cancled!"<<endl;
    33	    void* tmp=NULL;
    34	   pthread_join(tid,&tmp);
    35	   cout<<"thread qiut code:"<<(long long )ret<<endl;
    36	    return 100;
    37	


5.线程的用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。

二、进程和线程的对比

1.进程和线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据
  • 进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境。

2.多进程的应用场景有哪些?

三、线程控制

1.POSIX线程库

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

2.创建线程

  • int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
  • pthread_t:线程标识符,本质上是线程在共享区独有空间的首地址。
    -pthread_t:是一个出参,该值由pthread_creat函数赋值的。
    -thread:创建线程的属性,一般情况都指定为NULL,采用默认属性。
  • pthread_attr_t:函数指针,接收一个返回值为void*,参数为void*的函数地址,就是线程入口函数。
  • void *(*start_routine) (void *):给线程入口函数传递的参数;由于参数类型是void*,返回值类型为void*,所以给了程序无限的传递参数的方式(char*,int*,结构体指针,this)
  • 返回值
    失败:< 0


在主线程中创建一个工作线程,主线程和副线程都不退出。
mythead.cpp

     1	#include<iostream>
     2	#include<pthread.h>
     3	using namespace std;
     4	#include<unistd.h>
     5	
     6	void* thread_run(void* arg)
     7	
     8	    while(1)
     9	    
    10	        cout<<"i am "<<(char*)arg<<endl;
    11	        sleep(1);
    12	    
    13	
    14	int main()
    15	
    16	    pthread_t tid;
    17	    int ret=pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    18	    if(ret!=0)
    19	    
    20	        return -1;
    21	    
    22	    while(1)
    23	    
    24	        cout<<"i am main thread"<<endl;
    25	        sleep(2);
    26	    
    27	    return 0;
    28	


makefile

     1	mythread:mythread.cpp
     2		g++ $^ -o $@ -lpthread
     3	.PHONY:clean
     4	clean:
     5		rm -f mythread




在主线程中创建一个副线程,让主线程退出,副线程不退出。

     1	#include<iostream>
     2	#include<pthread.h>
     3	using namespace std;
     4	#include<unistd.h>
     5	
     6	void* thread_run(void* arg)
     7	
     8	    while(1)
     9	    
    10	        cout<<"i am "<<(char*)arg<<endl;
    11	        sleep(1);
    12	    
    13	
    14	int main()
    15	
    16	    pthread_t tid;
    17	    int ret=pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    18	    if(ret!=0)
    19	    
    20	        return -1;
    21	    
    22	    return 0;
    23	

下图可以看到主线程退出来,进程也退出。

  • 传参问题验证:
  • 假设要往创建的工作线程中传入一个参数1,首先要将参数强转为(void*)类型,然后将参数的地址传入,而在工作线程中使用是只需将(void*)转换为(int*)即可。
     1	#include<iostream>
     2	#include<unistd.h>
     3	#include<pthread.h>
     4	using namespace std;
     5	
     6	void* MyThreadStrat(void* arg)
     7	
     8	    int* i=(int*)arg;
     9	    while(1)
    10	    
    11	        cout<<"MyThreadStrat:"<<*i<<endl;
    12	        sleep(1);
    13	    
    14	    return NULL;
    15	
    16	int main()
    17	
    18	    pthread_t tid;
    19	    int i=1;
    20	        int ret=pthread_create(&tid,NULL,MyThreadStrat,(void*)&i);
    21	        if(ret!=0)
    22	        
    23	            cout<<"线程创建失败!"<<endl;
    24	            return 0;
    25	        
    26	    while(1)
    27	    
    28	        sleep(1);
    29	        cout<<"i am main thread"<<endl;
    30	    
    31	    return 0;
    32	


虽然参数可以正常传入,但实际是存在一定的错误的,因为局部变量 i 传入的时候生命周期未结束,而在传递给工作线程的时候生命周期结束了,那么这块局部变量开辟的区域就会自动释放,而此时工作线程还在访问这块地址,就会出现非法访问。

代码改成循环:

     1	#include<iostream>
     2	#include<unistd.h>
     3	#include<pthread.h>
     4	using namespace std;
     5	
     6	void* MyThreadStrat(void* arg)
     7	
     8	    int* i=(int*)arg;
     9	    while(1)
    10	    
    11	        cout<<"MyThreadStrat:"<<*i<<endl;
    12	        sleep(1);
    13	    
    14	    return NULL;
    15	
    16	int main()
    17	
    18	    pthread_t tid;
    19	    int i=0;
    20	    for( i=0;i<4;i++)
    21	    
    22	        int ret=pthread_create(&tid,NULL,MyThreadStrat,(void*)&i);
    23	        if(ret!=0)
    24	        
    25	            cout<<"线程创建失败!"<<endl;
    26	            return 0;
    27	        
    28	    
    29	    while(1)
    30	    
    31	        sleep(1);
    32	        cout<<"i am main thread"<<endl;
    33	    
    34	    return 0;
    35	


因为for循环4次最终开辟4个工作线程,开辟线程传递进去的是 i 的地址,而 i 中的值从0加到4,而 i 到5退出,此时 i 已经被加为4,最终 i 的地址中存的值为 4,使用最终会一直输出4。

问题的解决---->动态内存开辟:

传递this指针:

class MyThread

    public:
        MyThread()
        

        

        ~MyThread()
        

        

        int Start()
        
            int ret = pthread_create(&tid_, NULL, MyThreadStart, (void*)this);
            if(ret < 0)
            
                return -1;
            

            return 0;
        

        static void* MyThreadStart(void* arg)
        
            MyThread* mt = (MyThread*)arg;
            printf("%p\\n", mt->tid_);
        
    private:
        pthread_t tid_;
;

int main()

    return 0;

传递结构体指针:

     1	#include<iostream>
     2	#include<unistd.h>
     3	#include<pthread.h>
     4	using namespace std;
     5	
     6	struct ThreadId
     7	
     8	    int thread_id;
     9	;
    10	void* MyThreadStrat(void* arg)
    11	
    12	    struct ThreadId* tid=(struct ThreadId*)arg;
    13	    while(1)
    14	    
    15	        cout<<"MyThreadStrat:"<<tid->thread_id<<endl;
    16	        sleep(1);
    17	    
    18	    delete tid;
    19	
    20	int main()
    21	
    22	    pthread_t tid;
    23	    int i=0;
    24	    for( i=0;i<4;i++)
    25	    
    26	        struct ThreadId* id=new ThreadId();
    27	        id->thread_id=i;
    28	        int ret=pthread_create(&tid,NULL,MyThreadStrat,(void*)id);
    29	        if(ret!=0)
    30	        
    31	            cout<<"线程创建失败!"<<endl;
    32	            return 0;
    33	        
    34	    
    35	    while(1)
    36	    
    37	        sleep(1);
    38	        cout<<"i am main thread"<<endl;
    39	    
    40	    return 0;
    41	


3.进程ID和线程ID

  • 在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
  • 没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题呢?
  • 多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。进程描述符结

    lua从青铜到王者基础篇第十三篇:lua调试(debug)(代码片段)

    系列文章目录文章目录系列文章目录前言🌲一、Lua调试(Debug)🌳二、另一个实例🌴三、调试类型💬🌲🌳🌴🌵总结前言🌲一、Lua调试(Debug)Lua提供了debug库用于提供创建我们自定义调试器的功... 查看详情

    c++从青铜到王者第二十三篇:c++异常(代码片段)

    系列文章目录文章目录系列文章目录前言一、C语言传统的处理错误的方式二、C++异常概念三、异常的使用1.异常的抛出和捕获2.异常的重新抛出3.异常安全4.异常规范四、自定义异常体系五、C++标准库的异常体系六、... 查看详情

    linux从青铜到王者第二十篇:linux网络基础第三篇之ip协议

    系列文章目录文章目录系列文章目录前言一、IP协议基本概念二、IPv4首部三、网络号和主机号四、早期地址管理方式五、CIDR(ClasslessInterdomainRouting)方式六、特殊的IP地址七、IP地址的数量限制八、路由控制总结前言一、IP协议基本... 查看详情

    linux篇第十三篇——多线程(线程概念+线程控制)(代码片段)

    ⭐️本篇博客开始要给大家介绍多线程相关的知识,多线程的内容比较多,所以我分三篇进行讲述,本篇博客主要讨论多线程的概念和多线程的控制,希望对你认识线程有所帮助。目录🌏Linux下的线程🌲... 查看详情

    linux从青铜到王者第二十一篇:linux网络基础第三篇之数据链路层

    系列文章目录文章目录系列文章目录前言一、数据链路层的以太网协议二、认识MAC地址三、对比理解MAC地址和IP地址四、ARP协议的作用五、ARP数据报格式五、ARP协议工作流程六、ARP缓存表七、DNS(DomainNameSystem)八、NAT协议九、NAPT协... 查看详情

    linux从青铜到王者第十五篇:linux网络编程套接字两万字详解(代码片段)

    系列文章目录文章目录系列文章目录前言一、网络数据的五元组信息1.理解源IP地址和目的IP地址2.理解"端口号"和"进程ID"3.理解源端口号和目的端口号4.理解TCP协议5.理解UDP协议二、主机字节序<===>网络字... 查看详情

    linux从青铜到王者第十八篇:linux网络基础第二篇之tcp协议

    系列文章目录文章目录系列文章目录前言一、TCP面向字节流二、TCP粘包问题1.什么是TCP粘包问题2.TCP粘包问题的解决办法三、TCP异常情况四、TCP协议1.TCP协议段格式2.确认应答(ACK)机制3.超时重传机制4.连接管理机制1、TCP三次握手1.... 查看详情

    linux从青铜到王者第十六篇:linux网络基础第二篇之http协议(代码片段)

    系列文章目录文章目录系列文章目录前言一、HTTP协议的概念二、HTTP协议URL的解释三、HTTP协议的数据流四、HTTP协议格式1.HTTP请求2.HTTP响应五、HTTP协议格式图解六、HTTP协议版本七、HTTP协议请求方法1.GET:获取资源2.POST:... 查看详情

    linux从青铜到王者第十二篇:linux进程间信号第二篇(代码片段)

    系列文章目录文章目录系列文章目录前言一、阻塞信号1.信号其他相关常见概念2.在内核中的表示3.sigset_t信号集4.信号集操作函数5.sigprocmask函数6.sigpending函数二、捕捉信号1.内核实现信号的捕捉2.volatile关键字总结前言一、阻塞信... 查看详情

    linux从青铜到王者第十四篇:linux网络基础第一篇

    系列文章目录文章目录系列文章目录前言一、计算机网络的发展过程1.独立模式2.网络互联模式3.局域网LAN4.广域网WAN二、认识计算机网络协议1.协议的概念2.什么是网络协议3.网络协议簇4.体系结构5.OSI七层模型6.TCP/IP五层(或四层)... 查看详情

    linux从青铜到王者第十七篇:linux网络基础第二篇之udp协议

    系列文章目录文章目录系列文章目录前言一、传输层1.再谈端口号2.端口号范围划分3.认识知名端口号(Well-KnowPortNumber)4.进程和端口号两个问题5.netstat查看网络状态二、UDP协议1.UDP协议端格式2.UDP的特点3.面向数据报4.UDP的缓冲区5.UDP... 查看详情

    linux从青铜到王者第十九篇:linux网络基础第二篇之滑动窗口流量控制拥塞控制延迟应答捎带应答

    系列文章目录文章目录系列文章目录前言👮一、滑动窗口💰一、滑动窗口的由来💰二、滑动窗口存在的问题💷1.滑动窗口的大小💷2.数据包已经传输给对方,但是对方返回的ACK数据包丢失💷3.传输的... 查看详情

    从青铜到王者,你只差这篇全网最全linux命令大全

    〝古人学问遗无力,少壮功夫老始成〞从青铜到王者,你只差这篇全网最全linux命令大全。全网最全linux命令大全,从入门到精通,助力大厂橄榄枝,如果这篇文章能给你带来一点帮助,希望给飞兔小哥哥... 查看详情

    从青铜到王者,你只差这篇全网最全linux命令大全

    〝古人学问遗无力,少壮功夫老始成〞从青铜到王者,你只差这篇全网最全linux命令大全。全网最全linux命令大全,从入门到精通,助力大厂橄榄枝,如果这篇文章能给你带来一点帮助,希望给飞兔小哥哥... 查看详情

    设计模式从青铜到王者第三篇:uml类图(代码片段)

    系列文章目录文章目录系列文章目录前言一、UML类图二、类图中具体类、抽象、接口和包的表示法1.在UML类图中表示具体类2.在UML类图中表示抽象类3.在UML类图中表示接口4.在UML类图中表示包三、在类图中表示关系1.实现关系2.泛化... 查看详情

    linux从青铜到王者第二十四篇:linux网络基础第四篇之websocket协议(代码片段)

    系列文章目录文章目录系列文章目录前言一、WebSocket简介二、WebSocket产生背景三、WebSocket实现原理四、WebSocket协议举例五、WebSocket使用1.WebSocket介绍2.WebSocketAPI3.WebSocket事件1.open2.Message3.Error4.Close4.WebSocket方法1.send()2.close()5.WebSocket... 查看详情

    c++从入门到入土第十三篇:vector的模拟实现

    查看详情

    mysql从青铜到王者第三篇:数据库表的约束

    系列文章目录文章目录系列文章目录前言一、表的约束二、空属性三、默认值四、列描述五、zerofill六、主键七、自增长八、唯一键九、外键总结前言一、表的约束真正约束字段的是数据类型,但是数据类型约束很单一,... 查看详情