一个例子让你看清线程调度的随机性

2023-11-14

  1. 粉丝提问|c语言:如何定义一个和库函数名一样的函数,并在函数中调用该库函数
  2. 一个端口号可以同时被两个进程绑定吗?
  3. 两个线程,两个互斥锁,怎么形成一个死循环?
  4. 一个例子让你看清线程调度的随机性

线程调度的几个基本知识点

多线程并发执行时有很多同学捋不清楚调度的随机性会导致哪些问题,要知道如果访问临界资源不加锁会导致一些突发情况发生甚至死锁。

关于线程调度,需要深刻了解以下几个基础知识点:

  1. 调度的最小单位是轻量级进程【比如我们编写的hello world最简单的C程序,执行时就是一个轻量级进程】或者线程;
  2. 每个线程都会分配一个时间片,时间片到了就会执行下一个线程;
  3. 线程的调度有一定的随机性,无法确定什么时候会调度;
  4. 在同一个进程内,创建的所有线程除了线程内部创建的局部资源,进程创建的其他资源所有线程共享;
    比如:主线程和子线程都可以访问全局变量,打开的文件描述符等。

实例

再多的理论不如一个形象的例子来的直接。

预期代码时序

假定我们要实现一个多线程的实例,预期程序执行时序如下:

期待时序

期待的功能时序:

  1. 主进程创建子线程,子线程函数function();
  2. 主线程count自加,并分别赋值给value1,value2;
  3. 时间片到了后切换到子线程,子线程判断value1、value2值是否相同,如果不同就打印信息value1,value2,count的值,但是因为主线程将count先后赋值给了value1,value2,所以value1,value2的值应该永远相同,所以不应该打印任何内容;
  4. 重复2、3步骤。

代码1

好了,现在我们按照这个时序编写代码如下:

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <pthread.h>
  5 #include <unistd.h>
  6 
  7 unsigned int value1,value2, count=0;
  8 void *function(void *arg);
  9 int main(int argc,  char *argv[])
 10 {
 11     pthread_t  a_thread;
 12 
 13     if (pthread_create(&a_thread, NULL, function, NULL) < 0)
 14     {
 15         perror("fail to pthread_create");
 16         exit(-1);
 17     }
 18     while ( 1 )
 19     {
 20         count++;
 21         value1 = count;
 22         value2 = count;
 23     }
 24     return 0;
 25 }
 26 
 27 void  *function(void *arg)
 28 {
 29     while ( 1 )
 30     {
 31         if (value1 != value2)
 32         {                                                                                                                                                                                         
 33             printf("count=%d , value1=%d, value2=%d\n",  count, value1, value2);
 34             usleep(100000);
 35         }     
 36     }
 37     return  NULL;
 38 }  

乍一看,该程序应该可以满足我们的需要,并且程序运行的时候不应该打印任何内容,但是实际运行结果出乎我们意料。

编译运行:

gcc test.c -o run -lpthread
./run

代码1执行结果

执行结果:
代码1执行结果
可以看到子程序会随机打印一些信息,为什么还有这个执行结果呢?
其实原因很简单,就是我们文章开头所说的,线程调度具有䘺随机性,我们无法规定让内核何时调度某个线程。
有打印信息,那么这说明此时value1和value2的值是不同的,那也说明了调度子线程的时候,是在主线程向value1和value2之间的位置调度的。

代码1执行的实际时序

实际上代码的执行时序如下所示:
代码1实际时序

如上图,在某一时刻,当程序走到**value2 = count;**这个位置的时候,内核对线程进行了调度,于是子进程在判断value1和value2的值的时候,发现这两个变量值不相同,就有了打印信息。

该程序在下面这两行代码之间调度的几率还是很大的。

value1 = count; 
value2 = count;

解决方法

如何来解决并发导致的程序没有按预期执行的问题呢?
对于线程来说,常用的方法有posix信号量、互斥锁,条件变量等,下面我们以互斥锁为例,讲解如何避免代码1的问题的出现。

互斥锁的定义和初始化:

pthread_mutex_t  mutex;
pthread_mutex_init(&mutex, NULL)

申请释放锁:

pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);

原理:
进入临界区之前先申请锁,如果能获得锁就继续往下执行,
如果申请不到,就休眠,直到其他线程释放该锁为止。

代码2

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <pthread.h>
  5 #include <unistd.h>
  6 #define _LOCK_
  7 unsigned int value1,value2, count=0;
  8 pthread_mutex_t  mutex;
  9 void *function(void *arg);
 10 
 11 int main(int argc,  char *argv[])
 12 {
 13     pthread_t  a_thread;
 14          
 15     if (pthread_mutex_init(&mutex, NULL) < 0)                                                                                                                                                          
 16     {
 17         perror("fail to mutex_init");
 18         exit(-1);
 19     }
 20 
 21     if (pthread_create(&a_thread, NULL, function, NULL) < 0)
 22     {
 23         perror("fail to pthread_create");
 24         exit(-1);
 25     }
 26     while ( 1 )
 27     {
 28         count++;
 29 #ifdef  _LOCK_
 30         pthread_mutex_lock(&mutex);
 31 #endif
 32         value1 = count;
 33         value2 = count;
 34 #ifdef  _LOCK_
 35         pthread_mutex_unlock(&mutex);
 36 #endif
 37     }
 38     return 0;
 39  }
40 
 41 void  *function(void *arg)
 42 {
 43      while ( 1 )
 44      {
 45 #ifdef _LOCK_
 46         pthread_mutex_lock(&mutex);
 47 #endif           
 48 
 49         if (value1 != value2)  
 50         {
 51             printf("count=%d , value1=%d, value2=%d\n",  count, value1, value2);
 52             usleep(100000);
 53         }     
 54 #ifdef _LOCK_
 55         pthread_mutex_unlock(&mutex);
 56 #endif
 57      }
 58      return  NULL;
 59  }     

如上述代码所示:主线程和子线程要访问临界资源value1,value2时,都必须先申请锁,获得锁之后才可以访问临界资源,访问完毕再释放互斥锁。
该代码执行之后就不会打印任何信息。
我们来看下,如果程序在下述代码之间产生调度时,程序的时序图。

value1 = count; 
value2 = count;

时序图如下:
代码2加锁后时序图

如上图所示:

  1. 时刻n,主线程获得mutex,从而进入临界区;
  2. 时刻n+1,时间片到了,切换到子线程;
  3. n+2时刻子线程申请不到锁mutex,所以放弃cpu,进入休眠;
  4. n+3时刻,主线程释放mutex,离开临界区,并唤醒阻塞在mutex的子线程,子线程申请到mutex,进入临界区;
  5. n+4时刻,子线程离开临界区,释放mutex。

可以看到,加锁之后,即使主线程在value2 =count; 之前产生了调度,子线程由于获取不到mutex,会进入休眠,只有主线程出了临界区,子线程才能获得mutex,访问value1和value2,就永远不会打印信息,就实现了我们预期的代码时序。

总结

实际项目中,可能程序的并发的情况可能会更加复杂,比如多个cpu上运行的任务之间,cpu运行的任务和中断之间,中断和中断之间,都有可能并发。

有些调度的概率虽然很小,但是不代表不发生,而且由于资源同步互斥导致的问题,很难复现,纵观Linux内核代码,所有的临界资源都会对应锁。

多阅读Linux内核源码,学向大神学习,与大神神交。
正所谓代码读百遍,其义自见!
熟读代码千万行,不会编写也会抄!

关于内核和应用程序的同步互斥的知识点,可以查看一口君的其他文章。

更多Linux干货,请关注一口Linux

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

一个例子让你看清线程调度的随机性 的相关文章

随机推荐

  • Jenkins:(看起来挺好看的)邮件模板样式

    Jenkins 邮件模板样式目录导航 邮件模板样式一 根据样式三改编 背景图自定义 邮件模板样式二 邮件模板样式三 邮件模板样式四 邮件模板样式一 根据样式三改编 背景图自定义
  • Linux上安装和使用Wireshark

    CentOS下安装Wireshark相当简单 两条命令就够了 这里 主要是记录写使用方面的东西 安装 1 yum install wireshark 注意这样并无法使用wireshark命令和图形界面 但提供了抓包基本功能 2 yum in
  • Dlib库中实现正脸人脸关键点(landmark)检测的测试代码

    Dlib库中提供了正脸人脸关键点检测的接口 这里参考dlib examples face landmark detection ex cpp中的代码 通过调用Dlib中的接口 实现正脸人脸关键点检测的测试代码 测试代码如下 referenc
  • 2014年1月14日星期二(DEMO7-2,加载3D线框立方体物体模型)

    上个DEMO 是渲染列表 这个DEMO 进行了加载PLG模型 仍然是一步步地进行 PLG模型首行包含了物体名称 顶点数和多边形数3部分组成 加载模型时可以每次读取一行 并对其中的数字进行分析 现在开始进行代码 先设置摄像机坐标和位置 朝向
  • 利用cin和cout完成信息的输入输出(TOZJ练习5681)

    项目场景 问题描述 在dev c 上运行正确 在TZOJ出现Presentation Error 答案和标准结果非常接近 在输出结果中 多了或少了不必要的空格或者回车或者其他 的代码 include
  • Java集合排序

    一 概述 1 集合排序概述 数组排序 int arr 1 2 3 Arrays sort arr 集合排序 使用Collections类中 sort 方法对List集合进行排序 sort List list 根据元素的自然顺序对指定列表按升
  • 基于内容的图像检索(CBIR) ——以图搜图

    文章目录 一 实现原理 二 基于内容的图像检索的特征提取 三 代码实现 打赏 在CBIR中 图像通过其视觉内容 例如颜色 纹理 形状 来索引 一 实现原理 首先从图像数据库中提取特征并存储它 然后我们计算与查询图像相关的特征 最后 我们检索
  • use MinGW compile googletest on windows

    table of contents enviornments brief description of software installation MinGW installation cmake installation googlete
  • word文档墨迹工具的笔不能用_CourseMaker微课制作教程43:手写设备在Word、PPT、PDF里的使用方法大全...

    首先我们要有个概念 手写设备 数位板 纸笔手写板 数位屏 在各个软件里能否书写 跟这些设备硬件本身并没有什么关系 不是说这个牌子的手写板在A软件里能用 那个牌子的手写板在A软件里不能用 能否在软件里手写 主要还是看软件里的手写功能组件是否完
  • linux驱动12:主设备号和次设备号

    dev目录下执行ls l 设备文件项的最后修改日期前的用逗号分割的两个数 对设备文件来说就是相应的主设备号和次设备号 第一个字符c表示字符设备 b表示块设备 主设备号标识设备对应的驱动程序 次设备号由内核使用 用于正确确定设备文件所指的设备
  • [答疑]《软件方法》自测题为什么不直接给出答案?

    软件方法 下 分析和设计第8章连载 20210518更新 gt gt 问题 很多同学说 软件方法 各章的自测题要扫码到全对才知道答案 比较费劲 能不能直接给出答案 统一回答如下 这是有意为之的 这些题是多年积累下来 围绕着书中的知识点精心准
  • 普通光照模型:unityshader

    我们都知道物体表面的光照是由 自发光 镜面光 高光 环境光 漫反射得出来的 环境光 光照系数 环境光颜色 Ambient K GlobalAmbient 漫反射 Diffuse K LightColor max dot N L 0 反射光线
  • 【linux系统安装nvm】

    linux系统安装nvm 直接用脚本一键安装 sudo apt install curl curl https raw githubusercontent com creationix nvm master install sh bash
  • React Antd HelloWorld

    react antdesign helloworld 安装antd 第一个示例HelloWorld 报错解决 快速解决 安装antd 使用 npm 或 yarn 安装 我们推荐使用 npm 或 yarn 的方式进行开发 不仅可在开发环境轻松
  • visio 2010激活教程

    一 下载office2010toolkit zip 若下载链接失效 手动搜索office2010toolkit http ys c ys168 com 605279628 o4W138W45JIPI5SiuWf5 office2010too
  • NLP 做词频矩阵时,遇到特大矩阵触发memoryerror的处理方式

    昨天做NLP词频矩阵处理时候 遇到内存不足的问题 遇到memoryerror的情况 查了不少资料 都让我在大的机器上跑 但是有时候资源有限 由于我的句子中的每个词语都是重要的 所以不设置停用词 也就是countvectoirze才符合我的需
  • ffmpeg--使用命令+EasyDarwin推流笔记本摄像头

    手头没有网络摄像头 采用ffmpeg EasyDarwin 笔记本摄像头模拟一个网络摄像头用来开发程序 有一些小细节记录一下 EasyDarwin安装使用 流媒体服务器easydarwin的安装还是非常方便的 参考官方给的readme 几分
  • 三层架构实现增删改查操作封装

    文章目录 概要 整体架构流程 技术名词解释 技术细节 小结 概要 三层架构 三层架构分为 数据 dao 层 业务 service 层 控制 controller 层 1 表示层 USL 即User Show Layer 视图层 a 前台 对
  • 学习材料收集

    记一个好帖子 http www wowotech net
  • 一个例子让你看清线程调度的随机性

    粉丝提问 c语言 如何定义一个和库函数名一样的函数 并在函数中调用该库函数 一个端口号可以同时被两个进程绑定吗 两个线程 两个互斥锁 怎么形成一个死循环 一个例子让你看清线程调度的随机性 线程调度的几个基本知识点 多线程并发执行时有很多同学