• linux篇【11】:linux下的线程<前序>


    目录

    一.linux下的线程

    1.linux下的线程概念

    (1)教材上粗略的 线程 定义

    (2)线程的引入

    (3)线程真正定义 以及 示意图

     (4)linux 和 windows等其他操作系统的线程对比

    (5)LWP

    (6)轻量级进程ID与进程ID之间的区别

    2.重新定义进程

    轻量级进程解释:

    (1)线程的优点

    (2)线程的缺点

    (3)线程异常

    (4)线程用途

    3.线程和进程的共享/私有资源

    4.进程和线程的关系

    二.页表理解——虚拟到物理地址之间的转化

    1.页表理解

    2.页表的好处

    三.线程的接口

    1.pthread_create 创建线程

    ps -aL (all light)查看所有的轻量级进程

    return 退出演示

    2.pthread_self

    3.thread_join 

    4.pthread_exit

    (1)pthread_exit 对比 exit

    (2)线程退出有3种:

    pthread_exit 退出演示

    5.pthread_cancel

    pthread_ cancel(tid); 退出

    四.用户级线程概念

    1.线程异常了怎么办?—线程健壮性问题

    2.理解 pthread_ t

    3.线程栈

     (1)代码区有三类代码

    (2)解释 pthread_create 创建线程的返回值pthread_t

    (3)线程局部存储

    五.分离线程

    1.概念

    2.示例

    (1)pthread_ detach(pthread_ self()); 新线程自我分离。

    (2)pthread_ detach(tid1);主线程分离新线程

    3.线程分离可以理解为线程退出的第四种方式


    一.linux下的线程

    1.linux下的线程概念

    (1)教材上粗略的 线程 定义

    1.在进程内部运行的执行流(线程在进程的虚拟地址空间中运行)
    2.线程比进程力度更细调度成本更低
    3.线程是CPU调度的基本单位

    (2)线程的引入

    fork之后,父子是共享代码的
    可以通过if else判断,让父子进程执行不同的代码块——>引出不同的执行流,可以做到进行对特定资源的划分

    (3)线程真正定义 以及 示意图

     线程(执行流)是系统调度的基本单位!linux下没有真正的线程,Linux的线程是用进程模拟的,他叫做 轻量级进程。线程执行力度比进程更细,调度成本更低(进程切换时不需要切换页表,电地址空间等,只需要切换线程的上下文数据),因为他执行的是进程的一部分,访问的是进程的一部分资源,使用进程的一部分数据

     (4)linux 和 windows等其他操作系统的线程对比

    Linux下认为: 

    进程和线程在概念上没有区别,他们都叫做执行流
    Linux的线程是用进程模拟的(实际上用进程的PCB模拟的)

    linux下的tcb就是pcb,因为他们的逻辑结构是一样的

    其他操作系统(例如windows)认为:

    进程和线程在执行流层面是不一样的,新增了TCB这个结构体,会导致维护成本变高
    线程:进程=n:1        进程——PCB;线程——TCB(thread control block)

    现在CPU看到的所有的task_ struct都是一个执行流(线程)

    (5)LWP

    LWP——light wait process:轻量级进程。LWP=PID的执行流是主线程,俗称进程

    详细概念:LWP是轻量级进程,在Linux下进程是资源分配的基本单位,线程是cpu调度的基本单位,而线程使用进程pcb描述实现,并且同一个进程中的所有pcb共用同一个虚拟地址空间,因此相较于传统进程更加的轻量化

    (6)轻量级进程ID与进程ID之间的区别

    因为Linux下的轻量级进程是一个pcb,每个轻量级进程都有一个自己的轻量级进程ID(pcb中的pid),而同一个程序中的轻量级进程组成线程组,拥有一个共同的线程组ID

    2.重新定义进程

    曾经:进程——内核数据结构+进程对应的代码和数据
    现在:进程——内核视角:承担分配系统资源的基本实体 (进程的基座属性),即:向系统申请资源的基本单位!

    内部只有一个执行流 task_struct 的进程——单执行流进程
    内部有多个执行流 task_struct 的进程——多执行流进程
            线程(执行流)是调度的基本单位!

    下面紫色框起来的进程PCB,虚拟内存,页表,内存中的数据和代码,这一组资源的集合叫做一个进程。

    轻量级进程解释:

    task_ struct <= 传统的进程PCB,如果是单执行流的进程(只有一个task_ struct时),task_ struct = 传统的进程PCB;多执行流的进程task_ struct < 传统的进程PCB

    (1)线程的优点

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

    使用多线程可以更加充分利用cpu资源,使任务处理效率更高,进而提高程序响应,即:耗时的操作使用线程,提高应用程序响应

    对于多核心cpu来说,每个核心都有一套独立的寄存器用于进行程序处理,因此可以同时将多个执行流的信息加载到不同核心上并行运行,充分利用cpu资源提高处理效率,即:多CPU系统中,使用线程提高CPU利用率,CPU线程调度程序中的不同线程来共同执行整个程序,并非让CPU线程独立执行程序

    (2)线程的缺点

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

    (3)线程异常

    单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
    线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该
    进程内的所有线程也就随即退出

    (4)线程用途

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

    3.线程和进程的共享/私有资源

    进程是资源分配的基本单位 ,线程是调度的基本单位(不管系统中是否有线程,进程都是拥有资源的独立单位)
    进程的多个线程共享 同一地址空间,因此 Text Segment Data Segment 都是共享的 , 如果定义一个函数 , 在各线程中都可以调用, 如果定义一个全局变量 , 在各线程中都可以访问到 , 除此之外 ,各线程还共享以下进程资源和环境: 文件描述符表 ,每种信号的处理方式(SIG_ IGN SIG_ DFL 或者自定义的信号处理函数 ) ,当前工作目录 ,用户id 和组 id
    线程共享进程数据,但也拥有自己私有的一部分数据: 线程ID , 一组寄存器 , 栈 , errno ,信号屏蔽字 ,调度优先级(多个线程各自有各自的栈区,但是堆区共用;每个线程在进程虚拟地址空间中会分配 拥有相对独立的栈空间而并不是共享栈空间,这样会导致运行时栈混乱

    4.进程和线程的关系

    ①在linux 中进程比线程安全的原因是每个进程有独立的虚拟地址空间,每个进程有自己独有的数据,具有独立性,不会数据共享这个说法是错误的,太过宽泛与片面

    多进程之间的数据共享比多线程编程复杂。线程之间的通信简单(共享地址空间和页表信息,因此传参以及全局数据都可以实现通信),而不同进程之间的通信更为复杂,通常需要调用内核(系统调用)实现

    多线程的创建,切换,销毁速度快于多进程。因为线程之间共享了进程中的大部分资源,因此共享的数据不需要重新创建或销毁,因此消耗上低于进程,反之也就是速度快于进程

    对于大量的计算优先使用多线程。大量的计算使用多进程和多线程都可以实现并行/并发处理,而线程的资源消耗小于多进程,而稳定相较多进程有所不如,因此还要看具体更加细致的需求场景

    “一个进程至少有一个线程”正确,但是“一个程序至少有一个进程”是错的,因为程序是静态的,不涉及进程,进程是程序运行时的实体,是一次程序的运行

    线程自己不拥有系统资源。因为 进程是资源的分配单位,所以线程并不拥有系统资源,而是共享使用进程的资源,进程的资源由系统进行分配

    任何一个线程都可以创建或撤销另一个线程。

    对比维度

    多进程

    多线程

    总结

    数据共享、同步

    数据共享复杂,需要用IPC;数据是分开的,同步简单

    因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂

    各有优势

    内存、CPU

    占用内存多,切换复杂,CPU利用率低

    占用内存少,切换简单,CPU利用率高

    线程占优

    创建销毁、切换

    创建销毁、切换复杂,速度慢

    创建销毁、切换简单,速度很快

    线程占优

    编程、调试

    编程简单,调试简单

    编程复杂,调试复杂

    进程占优

    可靠性

    进程间不会互相影响

    一个线程挂掉将导致整个进程挂掉

    进程占优

    分布式

    适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单

    适应于多核分布式

    进程占优

    二.页表理解——虚拟到物理地址之间的转化

    1.页表理解

    虚拟地址在被转化的过程中,不是直接转化的
    虚拟地址是32位的:32bit 分成 10+10+12 
    0101   0101  00  0100 0111 11 0000 1110 0101
    XXXX XXXX xx   yyyy yyyy  yy zzzz  zzzz zzzz

    虚拟地址的前10位在一级页表——页目录中找对应的二级页表;找到对应的二级页表后,中间10位在二级页表中找对应的page的起始地址(物理内存);找到对应的page的起始地址后,后12位作为偏移量在物理内存中的一个page(4KB)中找对应数据的地址,因为后12位有2^12=4096字节=4KB,正好物理内存管理单位是一个page,一个page是4KB,则后12位正好可以覆盖一个page的所有的地址。找到地址后CPU读取物理内存的数据

      

    2.页表的好处

    (1)进程虚拟地址管理和内存管理,通过页表+page进行解耦
    (2)节省空间:分页机制+按需创建页表

    页表也要占据内存,页表分离了,可以实现页表的按需创建,比如页目录的第3个地址从来没使用过,就可以不创建对应的二级目录,需要时再创建。一个页表大小是2^32/2^12=2^20字节(页目录和二级页表)

    虚拟地址到物理地址的转化——硬件MMU做的,软(页表)硬(MMu)件结合的方式

    三.线程的接口

    1.pthread_create 创建线程

    pthread_create是一个库函数,功能是在用户态创建一个用户线程,而这个线程的运行调度是基于一个轻量级进程LWP实现的。

    创建一个新线程的接口,不是系统接口,是linux带的 原生线程库,所以他是第三方库函数,需要在makefile中链接此库,g++ -o $@ $^ -lpthread -std=c++11

     int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);    (pthread_t 就是unsigned long int

    thread:输出型参数,线程id。attr:线程属性,现在不考虑,设成nullptr。start_routine:线程执行时的回调函数(该线程要执行的函数方法)。arg:通过这个参数传递线程的属性—现在可以传的属性:“要创建的线程的名称”,arg会传给start_routine函数的形参

    返回值:成功返回0;失败返回错误码errno 

    例如:int n = pthread_create(&tid, nullptr, startRoutine, (void *)"thread1"); 新线程会从startRoutine函数进入执行,主线程会拿到n继续执行下面的代码

    ps -aL (all light)查看所有的轻量级进程

    LWP——light wait process:LWP就是轻量级进程,描述的是一个进程中的一个pcb

    makefile: 

    1. mythread:mythread.cc
    2. g++ -o $@ $^ -lpthread -std=c++11
    3. .PHONY:clean
    4. clean:
    5. rm -f mythread

     mythread.cc

    return 退出演示

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. void printTid(const char* name,const pthread_t &tid)//引用
    7. {
    8. printf("%s 正在运行,thread id:0x%x\n",name,tid);
    9. }
    10. void* startRoutine(void* args)
    11. {
    12. const char* name=static_cast<const char*>(args);
    13. int cnt=5;
    14. while(true)
    15. {
    16. printTid(name,pthread_self());
    17. sleep(1);
    18. if(!(cnt--))
    19. break;
    20. }
    21. cout<<"新线程退出…………"<
    22. return nullptr;
    23. }
    24. int main()
    25. {
    26. pthread_t tid;
    27. int n=pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    28. sleep(10);
    29. pthread_join(tid,nullptr);
    30. while(true)
    31. {
    32. printTid("main thread",pthread_self());
    33. sleep(1);
    34. }
    35. return 0;
    36. }

    2.pthread_self

     man 3 pthread_self

     pthread_t pthread_self(void);

    线程获取自己的线程id

    3.thread_join 

     man 3 pthread_join

    int pthread_join(pthread_t thread, void **retval);        thread:线程id。retval:输出型参数,线程退出的退出码。(join 不需要退出信号)

    线程退出的时候,一般必须要进行join等待,如果不进行join,就会造成类似于进程那样的内存泄露问题。(即:作用是:释放线程资源——前提是线程退出了。并获取线程对应的退出码)

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

    4.pthread_exit

    终止线程。线程终止--只考虑正常终止

    (1)pthread_exit 对比 exit

    exit(1):代表退出进程,任何一个 主/新线程调用exit,都表示整个进程退出。pthread_exit()仅仅是代表退出线程。

    (2)线程退出有3种:

    1. 线程退出的方式,return —— return (void*)111;

    2. 线程退出的方式,pthread_exit —— pthread_exit((void*)1111);

    3. 线程退出的方式:线程取消请求,pthread_cancel —— pthread_ cancel(tid);

    void pthread_exit(void *retval);          retval:线程退出码

    pthread_exit 退出演示

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. void printTid(const char* name,const pthread_t &tid)//引用
    7. {
    8. printf("%s 正在运行,thread id:0x%x\n",name,tid);
    9. }
    10. void* startRoutine(void* args)
    11. {
    12. const char* name=static_cast<const char*>(args);
    13. int cnt=5;
    14. while(true)
    15. {
    16. printTid(name,pthread_self());
    17. sleep(1);
    18. if(!(cnt--))
    19. break;
    20. }
    21. cout<<"新线程退出…………"<
    22. //return nullptr;
    23. pthread_exit((void*)1111);
    24. }
    25. int main()
    26. {
    27. pthread_t tid;
    28. int n=pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    29. sleep(10);
    30. pthread_join(tid,nullptr);
    31. while(true)
    32. {
    33. printTid("main thread",pthread_self());
    34. sleep(1);
    35. }
    36. return 0;
    37. }

    5.pthread_cancel

    取消一个线程

    int pthread_cancel(pthread_t thread);        thread:线程id

    线程退出的方式,给线程发送取消请求, 如果线程是被取消的,退出结果是:-1

    pthread_ cancel(tid); 退出

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. void printTid(const char* name,const pthread_t &tid)//引用
    7. {
    8. printf("%s 正在运行,thread id:0x%x\n",name,tid);
    9. }
    10. void* startRoutine(void* args)
    11. {
    12. const char* name=static_cast<const char*>(args);
    13. int cnt=5;
    14. while(true)
    15. {
    16. printTid(name,pthread_self());
    17. sleep(1);
    18. if(!(cnt--))
    19. break;
    20. }
    21. cout<<"新线程退出…………"<
    22. //return nullptr;
    23. //pthread_exit((void*)1111);
    24. }
    25. int main()
    26. {
    27. pthread_t tid;
    28. int n=pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    29. sleep(10);
    30. pthread_cancel(tid);
    31. (void)n;
    32. cout<<"new thread been canceled"<
    33. void* ret=nullptr; //void* -> 64 -> 8byte ->空间
    34. pthread_join(tid,&ret); //void **retval是 一个输出型参数
    35. cout<<"main thread join success,*ret:"<<(long long)ret<
    36. sleep(3);
    37. return 0;
    38. }

    四.用户级线程概念

    据操作系统内核是否对线程可感知,可以把线程分为内核线程用户线程。用户级线程由应用程序所支持的线程实现, 内核意识不到用户级线程的实现内核级线程又称为内核(系统)支持的线程。

    1.线程异常了怎么办?—线程健壮性问题

    线程异常了——>整个进程整体异常退出。 线程异常==进程异常
    线程会影响其他线程的运行一新线程会影响主线程main thread 一健壮性/鲁棒性 较低,

    2.理解 pthread_ t

    是一个地址
    1.线程是一个独立的执行流
    2.线程一定会在自己的运行过程中,产生临时数据(调用函数,定义局部变量等)在新线程中修改全局变量后,新线程和主线程都能看到被修改后的结果
    3.线程一定需要有自己的独立的栈结构

    3.线程栈

     (1)代码区有三类代码

    我们使用的线程库,用户级线程库,库的名字叫pthread

    代码区有三类代码:

    ①你自己写的代码。

    ②库的接口代码。(例如动态库libpthread. so会写入内存,通过页表映射到进程的共享区,代码区的库接口代码通过跳转到共享区执行完库中的代码,然后再跳转回代码区继续执行)

    ③系统接口代码。(通过身份切换 用户—>内核 执行代码)

    所有的代码执行,都是在进程的地址空间当中进行执行的


    (2)解释 pthread_create 创建线程的返回值pthread_t

    用户要用线程,但是OS没有线程的概念,libpthread. so线程库起承上启下的作用。

    共享区内: 

    线程的全部实现,并没有全部体现在OS内,而是OS提供执行流,具体的线程结构由库来进行管理。库可以创建多个线程->库也要管理线程->管理:先描述,在组织

    struct thread_ info

            pthread_ t tidh .
            void *stack; //私有栈
            ……
    }  

    libpthread. so线程库映射进共享区中。创建线程时,线程库中也会创建一个 结构体struct thread_ info叫做 线程控制块线程控制块内部是描述线程的信息,内部有一个指针指向mm_struct用户空间的一块空间——线程栈。创建线程成功后,返回一个pthread_t类型的地址,pthread_t类型的地址保存着我们共享区中对应的用户级线程的线程控制块的起始地址!

     结论:主线程的独立栈结构,用的就是地址空间中的栈区;新线程用的栈结构,用的是库中提供的栈结构(这个线程栈是库维护的,空间还是用户共享区提供的)

    Linux中,线程库用户级线程库,和内核的LWP是1:1(LWP(类比PID)——light wait process:轻量级进程编号。LWP=PID的执行流是主线程,俗称进程

    (3)线程局部存储

    线程库中的结构体struct thread_ info,内部是描述线程的信息,struct thread_ info中还有一个叫做线程局部存储的区域。作用:可以把全局变量私有化

    正常情况全局变量是多个线程可以同时修改的:

    加上__thread,把全局变量拷贝给每个进程各一份,使全局变量私有化,各自修改自己的全局变量

    五.分离线程

    1.概念

    ①默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
    线程分离:如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
            int pthread_detach(pthread_t thread);
    可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离 :
            pthread_detach(pthread_self());
    一个线程如果分离了就不能再join,joinable和分离是冲突的,一个线程不能既是 joinable又是分离的。
     int pthread_detach(pthread_t thread);

    2.示例

    (1)pthread_ detach(pthread_ self()); 新线程自我分离。

    如果没有sleep(1),就会出现新线程一直循环的情况。因为有可能主线程先执行 int n = pthread_ join(tid1, nullptr); 此时新线程还没有pthread_ detach分离,则主线程会一直阻塞在pthread_ join这里,不会返回。

    当有sleep(1)时,新线程会先pthread_ detach自我分离。主线程后执行 int n = pthread_ join(tid1, nullptr); 此时新线程已经pthread_ detach分离,则主线程会直接pthread_ join返回错误码,证明了一个线程如果分离了就不能再join

    (2)pthread_ detach(tid1);主线程分离新线程

    建议主线程分离新线程,因为这样主线程一定是先分离了新线程,再pthread_ join。

    3.线程分离可以理解为线程退出的第四种方式

    (1)线程分离分为立即分离,延后分离,要保证线程还活着。线程分离意味着,我们不在关心这个线程的死活。线程分离可以理解为线程退出的第四种方式——延后退出

    (2)主线程的退出,并不会导致进程退出,也不会影响其他线程的运行。进程中的所有线程退出了,进程才会退出。—— 一般我们分离线程,对应的main thread不要退出(常驻内存的进程)

  • 相关阅读:
    我使用延迟队列实现商品的竞拍成交功能
    redis 登录客户端命令
    k8s--基础架构--租赁(Lease )
    【计算机网络】Socket的OOBInline选项与UrgentData
    WSA工具箱安装应用商店提示无法工作怎么解决?
    nginx配置server
    获取insert生成的主键
    【连通性的初步分析】
    springboot基础(34):Elasticsearch的基本操作
    牛客网前端刷题(一)
  • 原文地址:https://blog.csdn.net/zhang_si_hang/article/details/127901437