C++实战-Linux多线程(入门到精通)(一)

简介: C++实战-Linux多线程(入门到精通)(一)

线程的概念

1.与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同的程序,并且共享同一份全局内存区域,其中包括初始化数据段(.data),未初始化数据段(.bss),栈内存段。

【注意:没有共享栈内存和代码段】

2.进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位

3.线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍然是进程

4.查看指定进程的LWP:ps -lf pid   (注:lwp不是线程的id)

Linux内核线程实现原理

1.轻量级进程(light-weight process)也有PCB,创建线程使用的底层函数和进程一样,都是clone

2.从内核里看进程和线程是一样的,都有各自不同PCB,但是PCB中指向内存资源的三级页表是相同的

三级映射:进程PCB-->页目录(可以看出是数组,首地址位于PCB中)-->页表-->物理页面-->内存单元

3.进程可以蜕变成线程(看上面的虚拟地址空间就能分析出来)

4.线程可看作寄存器和栈的集合

5.LWP和进程ID的区别:LWP号是Linux内核划分时间轮片给线程的依据,线程ID是在进程内部区分线程的

a

6.对于进程而言,相同地址在不同的进程中,反复使用而不冲突。原因是他们虽然虚拟地址一样,但页目录、页表、物理页面各不相同。相同的虚拟地址,映射到不同的物理页面内存单元,最终访问不同物理页面。

但,线程不同。两个线程虽然有独立的PCB,但是共享同一个页目录、页表、物理页面。所以两个PCB共享同一个地址空间。

7.无论是创建进程的fork()还是创建线程的pthread_creat(),底层实现都是调用一个内核函数clone()。如果是复制对方的地址空间,那么就产生一个“进程”,如果是共享对方的地址空间,就产生一个"线程"。因此,Linux内核是不区分进程和线程的。只是在用户层面上进行区分。所以,线程所有的操作函数pthrad_*是库函数,而非系统调用。

进程和线程区别总结

1.进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。

2.调用fork()来创建进程的代价相对较高(复制一份地址空间),即便采用"写时复制"机制,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着fork()调用在时间上的开销依然不菲

3.线程之间能够方便、快速地共享信息,只需要将数据复制到共享(栈不行,见上图)变量中即可

4.创建线程比创建进程通常要快10倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表(之前说过虚拟地址空间的实现,并不是真的去分配4G的内存,只需要实现相应的数据结构,页表、页目录)

线程之间共享和非共享资源

这个图太重要了,我们再看一下

共享资源 非共享资源
文件描述符表 线程ID
每种信号的处理方式 处理器现场和栈指针(内核栈)
当前工作目录、文件权限 独立的栈空间(用户空间栈)
用户ID和组ID和会话ID errno变量
虚拟地址空间(除栈  .txt) 信号屏蔽字
调度优先级

介绍下NPTL(第三方库):

       当Linux最初开发时,在内核中并不能正真支持线程。通过clone()系统调用进程作为可调度的实体。这个调用创建了调用进程的一个拷贝,这个拷贝于调用共享相同的地址空间。但这个符合POSIX的要求,在信号处理、调度间同步等方面存在问题。

       NPTL或称为Native POSIX Thread Library。是Linux线程的一个新实现,它克服了LinuxThreads的缺点,同时符合标准。

       查看当前进程库版本:getconf GNU_LIBPTHREAD_VERSION

线程优缺点

优点:1.提高程序并发性   2.开销小   3.数据通信、共享数据方便

缺点:1.库函数、不稳定   3.调试、编写困难(gdb不支持)  4.对信号支持不好

优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大

线程相关函数(线程控制原语)

一般情况下,main函数所在的线程称为主线程。其余创建的线程称为子线程。

fork()函数--->创建进程

程序中默认只有一个线程,pthread_create()函数调用-->2个线程


pthread_create函数

       int pthread_create(pthread_t *thread,const pthread_attr_t *attr,

                                       void*(*start_routine)(void*),void *arg);

       功能:创建一个子线程

       参数:

                  thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中

                  attr设置线程的属性,一般使用默认值 NULL

                  start_routine:函数指针,这个函数是子线程需要处理的逻辑代码

                  arg:给第三个参数使用

       返回值:

                  成功:0

                  失败:返回错误号

补充说明:因为线程是通过NPTL库是第三方库,跟我们的系统调用不一样哦。

                 之前我们一直使用perror(ret),现在可行不通. 使用 char *strerror(int errnum)

 

#include <iostream>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
using namespace std;
void *func(void *arg)
{
    printf("pthread:%lu\n",pthread_self());
    return NULL;
}
int main(void)
{
    //typedef unsigned long 
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,func,NULL);
    if(ret)
    {
        printf("%s\n",strerror(ret));
        exit(-1);
    }
    sleep(1);
    printf("我是主线程:%d\n",getpid());
    return 0;
}

编译注意事项:线程椒通过第三方库实现的

g++ createPthread.cpp -o createPthread -lpthread(-pthread)


pthread_exit函数

       int pthread_exit(void *retval);

       功能:终止一个线程,在哪个线程中调用就代表终止哪个线程

       参数:

               retval:需要传递一个指针,作为一个返回值,在pthread_join()中可以获取到。

                          线程退出的状态,通常传NULL

      补充:当主线程退出时,不会影响其他正常运行的线程

                 在线程中禁止使用exit函数,会导致进程内所有线程全部退出

       

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
void *func(void *arg)
{
   int i = (int)arg;
   if(i == 2)
   {
        pthread_exit(NULL);
   }
   sleep(2);
    printf("我是第%d号线程,线程ID:%lu\n",i,pthread_self());
   return NULL;
}
int main(void)
{
    pthread_t tid;
    for(int i=0;i<5;++i)
    {
        pthread_create(&tid,NULL,func,(void *)i);
    }
    sleep(5);
    printf("我是主线程,线程ID:%lu\n",pthread_self());
    return 0;
}

pthread_self函数

       pthread_t pthread_self(void)

       功能:获取当前的线程的线程ID


pthread_equal函数

       int pthread_equal(pthread_t t1,pthread_t t2);

       功能:比较两个线程ID是否相等

       返回值:相等非0,不相等0

       补充:不同操作系统pthread_t类型的实现不一样,有的时无符号长整型,有的可能是结构体      

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_t tid_one;
void *func(void *arg)
{
    if(pthread_equal(tid_one,pthread_self()))
    {
        printf("相等\n");
    }
    else
    {
        printf("不相等\n");
    }
}
int main(void)
{
    pthread_t tid;
    pthread_create(&tid_one,NULL,func,NULL);
    pthread_join(tid_one,NULL);
    return 0;
}

pthread_join函数

       int pthread_join(pthread_t thread,void ** retval);

       功能:和一个已经终止的线程进行连接,回收资源

                  回收子进程的资源

                  这个函数是阻塞的,调用一次只能回收一个子进程

                  一般在主线程中使用

       参数:thread  需要回收的子进程ID

                  retval   接收子进程退出时的返回值

       返回值:

                  0   成功

                  非0,失败,返回错误号


线程的分离

       int pthread_detach(pthread_t thread);

       功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统

       注意事项:

               1).不能多次分离,会产生不可预测的行为

               2).不能去连接一个已经分离的线程,会报错【pthread_join】

       参数:

               需要分离的线程的ID

       返回值:

               成功0   失败错误号

重点说明:

       线程分离状态:指定该状态,线程主动与主控制线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。

       进程如果有这样的机制,将不会产生僵尸进程,僵尸进程的产生主要由于进程死亡后,大部分资源被释放,一点残留资源仍然在系统中,导致内核认为该进程仍然存在

       一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取它的状态为止,但是线程也可以被设置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join这样的调用将返回EINVAL错误。

#include <stdio.h>
#include <pthread.h>
void* func(void *arg)
{
    printf("我是子线程:%lu\n",pthread_self());
    return NULL;
}
int main(void)
{
    pthread_t tid;
    pthread_create(&tid,NULL,func,NULL);
    //设置线程分离
    pthread_detach(tid);
    sleep(3);
    return 0;
}

线程的取消

       int pthread_cancel(pthread_t  thread);

       功能:取消线程(让线程终止)

                  取消某个进程,可以终止某个线程的运行。

                  但不是立马终止,而是当子线程执行到一个取消点,线程才会终止

                  取消点:系统调用(从用户态切换到内核态的时候)  creat open pause read write..

              如果线程终没有取消点,可以通过调用pthread_testcancel函数自行设置一个取消点

       被取消的线程,退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值是一个 -1。可在pthread.h中找到它的定义。

       

       因此我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值-1

#include <stdio.h>
#include <pthread.h>
void *func(void *arg)
{
    //printf("我是子线程:%lu\n",pthread_self()); //printf会产生一个系统调用
    pthread_testcancel();   //自己设置一个取消点
    return NULL;
}
int main(void)
{
    pthread_t tid;
    pthread_create(&tid,NULL,func,NULL);
    //取消这个线程
    pthread_cancel(tid);
    //回收
    int childRet;
    pthread_join(tid,(void **)&childRet);   //阻塞的
    printf("回收的返回值:%d\n",childRet);  
    return 0;
}
相关文章
|
7月前
|
监控 Linux 测试技术
C++零拷贝网络编程实战:从理论到生产环境的性能优化之路
🌟 蒋星熠Jaxonic,技术宇宙中的星际旅人。深耕C++与零拷贝网络编程,从sendfile到DPDK,实战优化服务器性能,毫秒级响应、CPU降60%。分享架构思维,共探代码星辰大海!
|
10月前
|
程序员 编译器 C++
【实战指南】C++ lambda表达式使用总结
Lambda表达式是C++11引入的特性,简洁灵活,可作为匿名函数使用,支持捕获变量,提升代码可读性与开发效率。本文详解其基本用法与捕获机制。
352 78
|
10月前
|
C语言 C++
【实战指南】 C/C++ 枚举转字符串实现
本文介绍了在C/C++中实现枚举转字符串的实用技巧,通过宏定义与统一管理枚举名,提升代码调试效率并减少维护错误。
545 88
|
11月前
|
存储 SQL 安全
Java 无锁方式实现高性能线程实战操作指南
本文深入探讨了现代高并发Java应用中单例模式的实现方式,分析了传统单例(如DCL)的局限性,并提出了多种无锁实现方案。包括基于ThreadLocal的延迟初始化、VarHandle原子操作、Record不可变对象、响应式编程(Reactor)以及CDI依赖注入等实现方式。每种方案均附有代码示例及适用场景,同时通过JMH性能测试对比各实现的优劣。最后,结合实际案例设计了一个高性能配置中心,展示了无锁单例在实际开发中的应用。总结中提出根据场景选择合适的实现方式,并遵循现代单例设计原则以优化性能和安全性。文中还提供了代码获取链接,便于读者实践与学习。
213 0
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
12月前
|
设计模式 运维 监控
并发设计模式实战系列(4):线程池
需要建立持续的性能剖析(Profiling)和调优机制。通过以上十二个维度的系统化扩展,构建了一个从。设置合理队列容量/拒绝策略。动态扩容/优化任务处理速度。检查线程栈定位热点代码。调整最大用户进程数限制。CPU占用率100%
631 0
|
7月前
|
Java 调度 数据库
Python threading模块:多线程编程的实战指南
本文深入讲解Python多线程编程,涵盖threading模块的核心用法:线程创建、生命周期、同步机制(锁、信号量、条件变量)、线程通信(队列)、守护线程与线程池应用。结合实战案例,如多线程下载器,帮助开发者提升程序并发性能,适用于I/O密集型任务处理。
685 0
|
监控 Linux C++
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展(2)
本文是《4步实现C++插件化编程》的延伸,重点介绍了新增的插件“热拔插”功能。通过`inotify`接口监控指定路径下的文件变动,结合`epoll`实现非阻塞监听,动态加载或卸载插件。核心设计包括`SprDirWatch`工具类封装`inotify`,以及`PluginManager`管理插件生命周期。验证部分展示了插件加载与卸载的日志及模块状态,确保功能稳定可靠。优化过程中解决了动态链接库句柄泄露问题,强调了采纳用户建议的重要性。
530 103
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展(2)
|
9月前
|
数据采集 消息中间件 并行计算
Python多线程与多进程性能对比:从原理到实战的深度解析
在Python编程中,多线程与多进程是提升并发性能的关键手段。本文通过实验数据、代码示例和通俗比喻,深入解析两者在不同任务类型下的性能表现,帮助开发者科学选择并发策略,优化程序效率。
673 1