多线程编程

2023-11-12

Linux线程概述

内核线程和用户线程

线程是程序中完成独立任务的完整执行序列。即一个可调度的实体,根据运行环境和调度者身份,线程分为内核线程和用户线程

  • 内核线程:在有的系统上也称为LWP(轻量级进程),运行在内核空间,由内核调度;
  • 用户线程:运行在用户空间,由线程库调度;

当进程的一个内核线程获得CPU的使用权时,它就加载并运行一个用户线程,可见,内核线程相当于用户线程运行的“容器”。

一个进程可以拥有M个内核线程和N个用户线程,其中M≤N,并且在一个系统的所有进程中,M和N的比值都是固定的,按照M:N的取值,线程的实现方式可分为三种模式:

  • 完全在用户空间实现;
  • 完全由内核调度;
  • 双层调度;

完全在用户空间实现的线程无须内核的支持,内核甚至不知道这些线程的存在。线程库负责管理所有执行线程,比如线程的优先级、时间片等。线程库利用longjmp 来切换现成的执行,使它们看起来像是“并发”执行的。但实际上内核仍然是把整个进程作为最小单位来调度的。换句话说,一个进程的所有执行线程共享该进程的时间片,它们对外表现出相同的优先级,因此,对于这种实现方式而言,M个用户线程对应1个内核线程,而该内核线程就是进程本身。完全在用户空间实现的线程:

  • 优点是:创建和调度线程无须内核的干预,因此速度相当快,并且它不占用额外的内核资源,所以即使创建了很多线程,也不会对系统性能造成明显的影响。

  • 缺点是:对于多处理器系统,一个进程的多个线程无法运行在不同的CPU上,因为内核是按照其最小调度单位来分配CPU的。此外线程的优先级只对同一个进程的线程有效,比较不同进程中的线程优先级是无意义的。

完全由内核调度的模式将创建、调度线程的任务都交给了内核,运行在用户空间的线程库无须执行管理任务。较早的Linux内核对内核线程的控制能力有限,线程库通常还要提供额外的控制,尤其是线程同步机制,不过现代Linux内核大大增强了对线程的控制,完全由内核调度的这种实现方式满足M:N = 1:1,即1个用户空间线程对应1个内核线程;


双层调度模式是前两种实现模式的混合体:内核调度M个内核线程,线程库调度N个用户线程,这样不但不会消耗过多的内核资源,而且线程切换速度也较快,同时它可以充分利用多处理器的优势;

线程操作函数
#include <pthread.h>
int pthread_create(pthread_t* thread,    //unsigned long 整形类型
                   const pthread_attr_t* attr,  //线程属性
                   void* (*start_routine)(void *),//线程运行函数
                   void* arg);//函数的参数



void pthread_exit(void* retval);  //retval 传递退出信息

int pthread_join(pthread_t thread, void** retval); //阻塞等待线程结束

int pthread_cancel(pthread_t thread);  //取消线程,接收到取消请求的目标线程可以决定是否被取消以及如何取消

int pthread_detach(pthread_t thread);  //分离释放线程

线程相关函数详解

同步函数

/*   信号量   */
#include <semaphore.h>
int sem_init(sem_t* sem,
             int pshared,   //为0表示是当前进程的局部信号量
             unsigned int value);  //初始化信号量

int sem_destroy(sem_t* sem);
int sem_wait(sem_t* sem);
int sem_post(sem_t* sem);


/*   互斥锁   */
#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr);

int pthread_mutex_destroy(pthread_mutex_t* mutex);
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);

//这样也可以初始化锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
/*   条件变量   */
int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* cond_attr);
int pthread_cond_destroy(pthread_cond_t* cond);
int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

线程安全的定义

一个线程安全的类应满足以下三个条件:

  • 多个线程同时访问时,其表现出正确的行为;
  • 无论OS如何调度这些线程,无论这些线程的执行顺序如何交织;
  • 调用端代码无须额外的同步或其他协调动作;

根据这个定义,C++标准库里面的大多数class都不是线程安全的,包括string、vector、map等,因为这些类需要外部加锁才能供多个线程同时访问。
间不要泄露this指针。**

线程同步原则
  1. 尽量最低限度的共享对象,减少需要同步的场合。一个对象能不暴露给别的线程就不要暴露,如果要暴露,优先使用immutable对象;实在不行才暴露可修改的对象,并用同步措施来充分保护它;
  2. 其实是使用高级的并发编程构件,如TaskQueue,Producter-Consumer Queue等等;
  3. 最后不得已必须使用底层同步原语时,只用非递归的互斥器和条件变量,慎用读写锁,不要用信号量;
Singleton实现
template<typename T>
class Singleton{
public:
    static T& instance(){
        pthread_once(&ponce_, &Singleton::init);
        return *value_;
    }
private:
    Singleton();
    ~Singleton();

    static void init(){
        value_ = new T();
    }   
private:
    static pthread_once_t ponce_;
    static T* value_;
};

template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;

template<typename T>
T* Singleton<T>::value_ = NULL;
sleep()不是同步原语

生产代码中线程的等待可分为两种:

  • 等待资源可用(要么等在select/poll/epoll_wait上,要么等在条件变量上);
  • 等待进入临界区(等在mutex)以便读写共享数据;

后一种等待通常非常短,否则程序的性能和伸缩性就会有问题;

在程序的正常执行中,如果需要等待一段已知的时间,应该往event loop里注册一个timer,然后在timer的回调函数里接着干活,因为线程是个珍贵的共享资源,不能轻易浪费(阻塞也是浪费)。如果等待某个事件发生,那么应该采用条件变量或I/O事件回调,不能用sleep来轮询,不要采用这样的业余做法:

while(true){
    if(!dataAvailable)
        sleep(some_time);
    else
        consumeData();
}

如果多线程的安全性和效率要靠代码主动调用sleep来保证,这显然是设计出了问题。等待某个事件发生,正确的做法是用select()等价物或Condition,亦或高层同步工具,在用户态做轮询是低效的。

适用多线程程序的场景

多线程的应用场景是:提高响应速度,让IO和“计算”相互重叠,降低latency。虽然多线程不能提高绝对性能,但能提高平均响应性能;

一个程序要做成多线程的,大致要满足:

  • 有多个CPU可用;
  • 线程间有共享数据,即内存中的全局状态;
  • 共享数据是可以修改的,而不是静态的常量表;
  • 提供非均质的服务,即,事件的响应有优先级差异,可以用专门的线程来处理优先级高的事件,防止优先级反转;
  • latency和throughput同样重要,不是逻辑简单的IO bound或CPU bound程序,换言之,程序要有相当的计算量;
  • 利用异步操作,比如logging,无论往磁盘写log file,还是往log server发送消息都不应该阻塞critical path;
  • 能scale up,一个好的多线程程序应该能享受增加CPU数目带来的好处;
  • 具有可预测的性能,随着负载增加,性能缓慢下降,超过某个临界点之后急速下降,线程数目一般不随负载变化;
  • 多线程能优先的划分责任和功能,让每个线程的逻辑比较简单,任务单一,便于编码。而不是把所有逻辑都塞到一个event loop里,不同类别的时间之间相互影响。
示例:

假设要管理一个Linux服务器机群,机群里面有8个计算节点,1个控制节点,机器的配置都是一样的,双路四核CPU,千兆网互联。现在需要编写一个简单的机群管理软件,这个软件由三部分组成:

  1. 运行在控制节点的master,这个程序监视并控制整个机群的状态;
  2. 运行在每个计算节点的salve,负责启动和终止job,并监控本机的资源;
  3. 供最终用户使用的client命令行,用于提交job;

可知,slave是个“看门狗进程”,它会启动别的job进程,因此必须是个单线程进程,另外它不应该占据太多CPU资源,这也适合单线程模型,master应该是个多线程程序:

  • 它占用一个8核机器,如果用单线程,浪费87.5%的CPU资源;
  • 整个机群的状态应该完全放在内存中,这些状态是共享且可变的;
  • master的主要性能指标不是throughput,而是latency,即尽快的响应各种事件,它几乎不会出现把IO或CPU跑满的情况;
  • master监控的事件有优先级区别,一个程序正常运行结束和异常崩溃的处理优先级不同,计算节点的磁盘满了和机箱温度过高这两种报警条件的优先级也不相同,如果用单线程,可能出现优先级反转;
  • 假设master和每个salve之间用一个TCP连接,那么master采用2个或4个IO线程来处理8个CPU connections能有效降低延迟;
  • master要异步的往磁盘上写log,这要求logging library有自己的IO线程;
  • master有可能读写数据库,那么数据库连接这个第三方library有可能有自己的线程,并回调master的代码;
  • master要服务于多个clients,用多线程也能降低客户响应时间,其可以再用2个IO线程专门处理和clients的通信;
  • master还可以提供一个monitor接口,用来广播推送机群的状态,这样用户不用主动轮询,这个功能如果用单独的线程来做,会比较容易实现,不会搞乱其他主要功能;

  • master一共开了10个线程:

    • 4个用于和slaves通信的IO线程;
    • 1个logging线程;
    • 一个数据库IO线程;
    • 2个和clients通信的IO线程;
    • 1个主线程,用于做些背景工作,如job调用等;
    • 1个pushing线程,用于主动广播机群的状态;
  • 虽然线程数目略多于core数目,但是这些线程很多时候都是空闲的,可以依赖OS的进程调度来保证可控的延迟;
线程的分类

一个多线程服务程序的线程大致可分为3类:

  • IO线程:这类线程的主循环是IO multiplexing,阻塞的等在select/poll/epoll_wait系统调用上,这类线程也处理定时事件,当然它的功能不止IO,有些简单的计算也可以放入其中;
  • 计算线程:这类线程的主循环是blocking queue,阻塞的等在condition variable上。这类线程一般位于thread pool中,通常不涉及IO,一般要避免任何阻塞操作;
  • 第三方库所用的线程,比如logging、database connection;
线程池大小的阻抗匹配原则

如果池中线程在执行任务时,密集计算所占的时间比重不过为P(0 < p ≤ 1),而系统一共有C个CPU,为了让C个CPU跑满又不过载,线程池大小的经验公式为T = C / P;考虑到P的值估计不是很准确,T的最佳值可以上下浮动50%。

当P < 0.2,这个公式就不适用了,T可以取一个固定值,比如5*C,C不一定是CPU总数,可以是“分配给这项任务的CPU数”;

多线程系统编程精要

学习多线程编程面临的最大的思维方式的转变有两点:

  • 当前线程可能随时会被切换出去,或者说被强占了;
  • 多线程程序中事件发生顺序不再有全局统一的前后关系;

在没有适当同步的情况下,多个CPU上运行的多个线程中的事件发生先后顺序是无法确定的;

多线程的正确性不能依赖于任何一个线程的执行速度,不能通过原地等待(sleep())来假定其他线程的事件已经发生,而必须通过适当的同步来让当前线程能看到其他线程的事件的结果。无论线程执行的快与慢(被OS切换出去得越多,执行越慢),程序都应该正常运行;

不必担心系统调用的线程安全性,因为系统调用对于用户态程序来说是原子的。但是要注意系统调用对于内核状态的改变可能影响其他线程。

编写一个线程安全程序的难点在于线程安全是不可组合的,尽管单个函数是线程安全的,但两个或多个函数放到一起就不再安全了。

可参考内容

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

多线程编程 的相关文章

  • 在 /dev/input/eventX 中写入事件需要哪些命令?

    我正在开发一个android需要将触摸事件发送到 dev input eventX 的应用程序 我知道C执行此类操作的代码结构如下 struct input event struct timeval time unsigned short
  • arm-linux-gnueabi 编译器选项

    我在用 ARM Linux gnueabi gcc在 Linux 中为 ARM 处理器编译 C 程序 但是 我不确定它编译的默认 ARM 模式是什么 例如 对于 C 代码 test c unsigned int main return 0x
  • diff 文件仅比较每行的前 n 个字符

    我有2个文件 我们将它们称为 md5s1 txt 和 md5s2 txt 两者都包含a的输出 find type f print0 xargs 0 md5sum sort gt md5s txt 不同目录下的命令 许多文件被重命名 但内容保
  • 在 Linux 上的 Python 中使用受密码保护的 Excel 工作表

    问题很简单 我每周都会收到一堆受密码保护的 Excel 文件 我必须解析它们并使用 Python 将某些部分写入新文件 我得到了文件的密码 当在 Windows 上完成此操作时 处理起来很简单 我只需导入 win32com 并使用 clie
  • .NET Core 中的跨平台文件名处理

    如何处理文件名System IO以跨平台方式运行类以使其在 Windows 和 Linux 上运行 例如 我编写的代码在 Windows 上完美运行 但它不会在 Ubuntu Linux 上创建文件 var tempFilename Dat
  • 并行运行 make 时出错

    考虑以下制作 all a b a echo a exit 1 b echo b start sleep 1 echo b end 当运行它时make j2我收到以下输出 echo a echo b start a exit 1 b star
  • ansible 重新启动 2.1.1.0 失败

    我一直在尝试创建一个非常简单的 Ansible 剧本 它将重新启动服务器并等待它回来 我过去在 Ansible 1 9 上有一个可以运行的 但我最近升级到 2 1 1 0 并且失败了 我正在重新启动的主机名为 idm IP 为 192 16
  • bluetoothctl 到 hcitool 等效命令

    在 Linux 中 我曾经使用 hidd connect mmac 来连接 BT 设备 但自 Bluez5 以来 这种情况已经消失了 我可以使用 bluetoothctl 手动建立连接 但我需要从我的应用程序使用这些命令 并且使用 blue
  • 为什么我收到“无法进行二进制日志记录”的信息。在我的 MySQL 服务器上?

    当我今天启动 MySQL 服务器并尝试使用以下命令进行一些更改时用于 MySQL 的 Toad http www quest com toad for mysql 我收到此消息 MySQL 数据库错误 无法进行二进制日志记录 消息 交易级别
  • 在 Linux 上更快地分叉大型进程?

    在现代 Linux 上达到与 Linux 相同效果的最快 最好的方法是什么 fork execve combo 从一个大的过程 我的问题是进程分叉大约 500MByte 大 并且一个简单的基准测试只能从进程中实现约 50 个分叉 秒 比较最
  • 强制卸载 NFS 安装目录 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 Locked 这个问题及其答案是locked help locked posts因为这个问题是题外话 但却具有历史意义 目前不接受新的答案
  • 通过特定分隔符删除字符串

    我的文件中有几列 其中第二列有 分隔符 我想删除第二列中的第一个 第三个和第四个字符串 并将第二个字符串留在该列中 但我有正常的分隔符空间 所以我不知道 input 22 16050075 A G 16050075 A G 22 16050
  • 使用 grep 查找包含所有搜索字符串的行

    我有一个文件 其中包含很多与此类似的行 id 2796 some model Profile message type MODEL SAVE fields account 14 address null modification times
  • 如何在 Linux 中编写文本模式 GUI? [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 当我编写脚本 程序时 我经常想弹出一个简单的文本 gui 来提示输入 我该怎么做 例如 来自 Shel
  • Linux TUN/TAP:无法从 TAP 设备读回数据

    问题是关于如何正确配置想要使用 Tun Tap 模块的 Linux 主机 My Goal 利用现有的路由软件 以下为APP1和APP2 但拦截并修改其发送和接收的所有消息 由Mediator完成 我的场景 Ubuntu 10 04 Mach
  • fopen 不返回

    我在 C 程序中使用 fopen 以只读模式 r 打开文件 但就我而言 我观察到 fopen 调用没有返回 它不返回 NULL 或有效指针 执行在 fopen 调用时被阻止 文件补丁绝对正确 我已经验证过 并且不存在与权限相关的问题 任何人
  • 域套接字“sendto”遇到“errno 111,连接被拒绝”

    我正在使用域套接字从另一个进程获取值 就像 A 从 B 获取值一样 它可以运行几个月 但最近 A 向 B 发送消息时偶尔会失败 出现 errno 111 连接被拒绝 我检查了B域套接字绑定文件 它是存在的 我也在另一台机器上做了一些测试 效
  • arm64和armhf有什么区别?

    Raspberry Pi Type 3 具有 64 位 CPU 但其架构不是arm64 but armhf 有什么区别arm64 and armhf armhf代表 arm hard float 是给定的名称Debian 端口 https
  • Discord.net 无法在 Linux 上运行

    我正在尝试让在 Linux VPS 上运行的 Discord net 中编码的不和谐机器人 我通过单声道运行 但我不断收到此错误 Unhandled Exception System Exception Connection lost at
  • 如何在 Linux shell 中将十六进制转换为 ASCII 字符?

    假设我有一个字符串5a 这是 ASCII 字母的十六进制表示Z 我需要找到一个 Linux shell 命令 它将接受一个十六进制字符串并输出该十六进制字符串代表的 ASCII 字符 所以如果我这样做 echo 5a command im

随机推荐

  • configure: error: C preprocessor "/lib/cpp" fails

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 错误代码 root localhost libevent 2 0 21 stable configure make make install checking for a
  • ARTS挑战打卡第十八周

    Algorithm 一周至少一道算法题 Review 阅读并点评至少一篇英文技术文章 Tip 学习至少一个技术技巧 总结和归纳在日常工作中所遇到的知识点 Share 分享一篇有观点和思考的技术文章 01 Algorthm https lee
  • 推荐一个开源虚拟化及云管管理平台

    能解决哪些问题 将几台物理服务器虚拟化成一个私有云平台 需要一个紧凑而且功能相对完整的物理机全生命周期管理工具 将 VMware vSphere 虚拟化集群转换为一个可以自服务的私有云平台 存在使用多云场景 能够在一个界面管理私有云和公有云
  • IDEA配置.gitignore不生效

    问题描述 我在 gitignore里面添加了日志文件不进行追踪 但是每次还是都上传到了云端 gitignore并没有生效 原因 gitignore只能忽略未被track的文件 而git本地缓存 如果某些文件已经被纳入了版本管理中 则修改 g
  • C/C++ 程序自删除

    文章目录 前言 一 代码 二 部分代码解释 前言 一般病毒之内的可能都带有自删除功能 而目前可进行完美自删除的方法并不多 其中一种较好的解决方法就是利用批处理文件 批处理文件一个优点就是 即使自身在运行的情况下也可以删除自己 所以实现的逻辑
  • 【Opencv&Cpp】12 像素统计:最大/小值、平均值、标准差

    minMaxLoc 找到全局最小和最大值 meanStdDev 计算矩阵的均值和标准偏差 找到全局最小和最大值 minmaxloc minMaxLoc InputArray src double minVal double maxVal 0
  • 大家来讨论怎么写概要设计

    http blog csdn net sunwill chen article details 7864904 笔者声明 本文讲述笔者浅薄的观点 意在抛砖引玉 望网友一起发表观点共同切磋 目前网络上的概要设计格式繁多 质量也是参差不齐 许多
  • 单链表C语言代码实现

    一 代码 include
  • sqli-labs-master靶场搭建以及报错解决

    一 前提准备 1 下载 sqli labs master mirrors audi 1 sqli labs GitCode 2 安装PHP study Windows版phpstudy下载 小皮面板 phpstudy xp cn 二 搭建靶
  • 华为OD机试 - 最小传输时延(Java)

    题目描述 某通信网络中有N个网络结点 用1到N进行标识 网络通过一个有向无环图表示 其中图的边的值表示结点之间的消息传递时延 现给定相连节点之间的时延列表times i u v w 其中u表示源结点 v表示目的结点 w表示u和v之间的消息传
  • Pycharm中修改注释文本的颜色(详细设置步骤)

    下面是在Pycharm中设置注释文本颜色的详细步骤 下面是修改前后对比 修改前注释行的颜色 修改后注释行的颜色 以上就是Pycharm中修改注释文本颜色的详细步骤 希望能帮到你
  • 小程序真机调试连接本地服务器进行调试

    小程序连接本地服务器 开发小程序时经常会遇到需要连接本地服务器进行调试的时候 但是总是连接不上 这里就说一下本菜鸟连接本地服务器的方法 第一步 把下图红框的地方勾选住 好多方法都得选这一步 第二步 设置里面代理按图中勾选 第三步是连接的方法
  • JavaScript避免使用return跳出多重循环从而继续执行函数;使用break跳出多重for循环

    一 先来看一下使用break仅跳出一层for循环的用法 const foo function for let i 1 i lt 3 i for let j 1 j lt 3 j if i 2 break console log 输出j的值
  • Mac上使用GPU加速训练模型

    文章目录 前言 使用GPU 前言 上一篇文章中我介绍了使用pytorch的一个完整模型训练套路 其中没有使用gpu 如果要使用gpu的话 win上我们可以使用cuda mac上可以使用mps 而我自己是mac电脑 需要进行如下修改 使用GP
  • ubuntu下docker配置国内镜像源

    既然都看到这篇文章了 就不解释为什么需要配置国内镜像源了 直接上步骤 此文使用ubuntu环境为Ubuntu 18 04 4 LTS 使用 sudo vim etc docker daemon json 命令新建或编辑文件 输入以下内容 r
  • 大话设计模式C++实现-第23章-命令模式

    一 UML图 二 概念 命令模式 Command 将一个请求封装为一个对象 从而使你可用不同的请求对客户进行参数化 对请求进行排队或记录请求日志 以及支持可撤销的操作 三 说明 角色 1 Command类 用来声明执行操作的接口 2 Con
  • 漫谈 SLAM 技术(下)

    转自 https zhuanlan zhihu com p 135958593 3 视觉SLAM系统关键问题 结合上述介绍的SLAM系统 我们从以下几个方面分析视觉SLAM系统需要考虑的关键问题 1 图像信息使用 视觉SLAM方法根据使用图
  • 模块""可能与您正在运行的Windows版本不兼容。检查该模块是否与regsvr32.exe的x86或x64版

    本人最近在研究mencoder 转换视频格式 发现转换rmvb需要 1 把drv43260 dll拷贝到系统的system32文件夹下 2 开始 gt 运行 gt regsvr32 drv43260 dll 来自 http topic cs
  • C++容器之 vector map set查找元素

    前面两篇基本上讲解容器的增加删除 其实现实世界中对数据的查找才是最大的需求 下面主要围绕着容器的查找来讲解 首先 由于vector没有实现find 方法 只能使用algorithm提供的find 方法 所以 直接在vector查找节介绍al
  • 多线程编程

    Linux线程概述 内核线程和用户线程 线程是程序中完成独立任务的完整执行序列 即一个可调度的实体 根据运行环境和调度者身份 线程分为内核线程和用户线程 内核线程 在有的系统上也称为LWP 轻量级进程 运行在内核空间 由内核调度 用户线程