一文读懂 | 进程并发与同步

并发 是指在某一时间段内能够处理多个任务的能力,而 并行 是指同一时间能够处理多个任务的能力。并发和并行看起来很像,但实际上是有区别的,如下图(图片来源于网络):

concurrency-parallelism

上图的意思是,有两条在排队买咖啡的队列,并发只有一架咖啡机在处理,而并行就有两架的咖啡机在处理。咖啡机的数量越多,并行能力就越强。

可以把上面的两条队列看成两个进程,并发就是指只有单个CPU在处理,而并行就有两个CPU在处理。为了让两个进程在单核CPU中也能得到执行,一般的做法就是让每个进程交替执行一段时间,比如让每个进程固定执行 100毫秒,执行时间使用完后切换到其他进程执行。而并行就没有这种问题,因为有两个CPU,所以两个进程可以同时执行。如下图:

concurrency-parallelism

原子操作

上面介绍过,并发有可能会打断当前执行的进程,然后替切换成其他进程执行。如果有两个进程同时对一个共享变量 count 进行加一操作,由于C语言的 count++ 操作会被翻译成如下指令:

mov eax, [count]
inc eax
mov [count], eax

那么在并发的情况下,有可能出现如下问题:

concurrency-problem

假设count变量初始值为0:

  • 进程1执行完 mov eax, [count] 后,寄存器eax内保存了count的值0。

  • 进程2被调度执行。进程2执行 count++ 的所有指令,将累加后的count值1写回到内存。

  • 进程1再次被调度执行,计算count的累加值仍为1,写回到内存。

虽然进程1和进程2执行了两次 count++ 操作,但是count最后的值为1,而不是2。

要解决这个问题就需要使用 原子操作,原子操作是指不能被打断的操作,在单核CPU中,一条指令就是原子操作。比如上面的问题可以把 count++ 语句翻译成指令 inc [count] 即可。Linux也提供了这样的原子操作,如对整数加一操作的 atomic_inc()

static __inline__ void atomic_inc(atomic_t *v)
{__asm__ __volatile__(LOCK "incl %0":"=m" (v->counter):"m" (v->counter));
}

在多核CPU中,一条指令也不一定是原子操作,比如 inc [count] 指令在多核CPU中需要进行如下过程:

  1. 从内存将count的数据读取到cpu。

  2. 累加读取的值。

  3. 将修改的值写回count内存。

Intel x86 CPU 提供了 lock 前缀来锁住总线,可以让指令保证不被其他CPU中断,如下:

lock
inc [count]

原子操作 能够保证操作不被其他进程干扰,但有时候一个复杂的操作需要由多条指令来实现,那么就不能使用原子操作了,这时候可以使用  来实现。

计算机科学中的  与日常生活的  有点类似,举个例子:比如要上公厕,首先找到一个没有人的厕所,然后把厕所门锁上。其他人要使用的话,必须等待当前这人使用完毕,并且把门锁打开才能使用。在计算机中,要对某个公共资源进行操作时,必须对公共资源进行上锁,然后才能使用。如果不上锁,那么就可能导致数据混乱的情况。

在Linux内核中,比较常用的锁有:自旋锁信号量读写锁 等,下面介绍一下自旋锁和信号量的实现。

自旋锁

自旋锁 只能在多核CPU系统中,其核心原理是 原子操作,原理如下图:

spinlock

使用 自旋锁 时,必须先对自旋锁进行初始化(设置为1),上锁过程如下:

  1. 对自旋锁 lock 进行减一操作,判断结果是否等于0,如果是表示上锁成功并返回。

  2. 如果不等于0,表示其他进程已经上锁,此时必须不断比较自旋锁 lock 的值是否等于1(表示已经解锁)。

  3. 如果自旋锁 lock 等于1,跳转到第一步继续进行上锁操作。

由于Linux的自旋锁使用汇编实现,所以比较苦涩难懂,这里使用C语言来模拟一下:

void spin_lock(amtoic_t *lock)
{
again:result = --(*lock);if (result == 0) {return;}while (true) {if (*lock == 1) {goto again;}}
}

上面代码将 result = --(*lock); 当成原子操作,解锁过程只需要把 lock 设置为1即可。由于自旋锁会不断尝试上锁操作,并不会对进程进行调度,所以在单核CPU中可能会导致 100% 的CPU占用率。另外,自旋锁只适合粒度比较小的操作,如果操作粒度比较大,就需要使用信号量这种可调度进程的锁。

信号量

与 自旋锁 不一样,当当前进程对 信号量 进行上锁时,如果其他进程已经对其进行上锁,那么当前进程会进入睡眠状态,等待其他进程对信号量进行解锁。过程如下图:

semaphore

在Linux内核中,信号量使用 struct semaphore 表示,定义如下:

struct semaphore {raw_spinlock_t      lock;unsigned int        count;struct list_head    wait_list;
};

各个字段的作用如下:

  • lock:自旋锁,用于对多核CPU平台进行同步。

  • count:信号量的计数器,上锁时对其进行减一操作(count--),如果得到的结果为大于等于0,表示成功上锁,如果小于0表示已经被其他进程上锁。

  • wait_list:正在等待信号量解锁的进程队列。

信号量 上锁通过 down() 函数实现,代码如下:

void down(struct semaphore *sem)
{unsigned long flags;spin_lock_irqsave(&sem->lock, flags);if (likely(sem->count > 0))sem->count--;else__down(sem);spin_unlock_irqrestore(&sem->lock, flags);
}

上面代码可以看出,down() 函数首先对信号量进行自旋锁操作(为了避免多核CPU竞争),然后比较计数器是否大于0,如果是对计数器进行减一操作,并且返回,否则调用 __down() 函数进行下一步操作。__down() 函数实现如下:

static noinline void __sched __down(struct semaphore *sem)
{__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}static inline int __down_common(struct semaphore *sem,long state, long timeout)
{struct task_struct *task = current;struct semaphore_waiter waiter;// 把当前进程添加到等待队列中list_add_tail(&waiter.list, &sem->wait_list);waiter.task = task;waiter.up = 0;for (;;) {...__set_task_state(task, state);spin_unlock_irq(&sem->lock);timeout = schedule_timeout(timeout);spin_lock_irq(&sem->lock);if (waiter.up) // 当前进程是否获得信号量锁?return 0;}...
}

__down() 函数最终调用 __down_common() 函数,而 __down_common() 函数的操作过程如下:

  1. 把当前进程添加到信号量的等待队列中。

  2. 切换到其他进程运行,直到被其他进程唤醒。

  3. 如果当前进程获得信号量锁(由解锁进程传递),那么函数返回。

接下来看看解锁过程,解锁过程主要通过 up() 函数实现,代码如下:

void up(struct semaphore *sem)
{unsigned long flags;raw_spin_lock_irqsave(&sem->lock, flags);if (likely(list_empty(&sem->wait_list))) // 如果没有等待的进程, 直接对计数器加一操作sem->count++;else__up(sem); // 如果有等待进程, 那么调用 __up() 函数进行唤醒raw_spin_unlock_irqrestore(&sem->lock, flags);
}static noinline void __sched __up(struct semaphore *sem)
{// 获取到等待队列的第一个进程struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list);list_del(&waiter->list);       // 把进程从等待队列中删除waiter->up = 1;                // 告诉进程已经获得信号量锁wake_up_process(waiter->task); // 唤醒进程
}

解锁过程如下:

  1. 判断当前信号量是否有等待的进程,如果没有等待的进程, 直接对计数器加一操作

  2. 如果有等待的进程,那么获取到等待队列的第一个进程。

  3. 把进程从等待队列中删除。

  4. 告诉进程已经获得信号量锁

  5. 唤醒进程。


推荐阅读:

专辑|Linux文章汇总

专辑|程序人生

专辑|C语言

我的知识小密圈

关注公众号,后台回复「1024」获取学习资料网盘链接。

欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~

嵌入式Linux

微信扫描二维码,关注我的公众号

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/465532.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

rust卡领地柜权限_RFID智能医疗耗材柜,上海智能高值耗材柜,国药智能医用耗材柜...

近几年因为我们的医疗改革一直在进步并改革,国家对我们的医疗方面的补助也有了明显的加大投入,与此同时让各种公立私立医院如雨后春笋般层出不穷,各大医院为了在医疗市场占有一席之地,都在各个方面开始想办法提升自己医院的水准。…

刚接触电子时,有过哪些百思不得其解的问题?

青少年时期,刚接触电子时,出于好奇,对这方面的东西也比较关注,但同时也衍生了一些百思不得其解的疑问,比如...01物理书里说大地是导体,那为什么我的小灯珠却不亮!?02初三时学了物理的…

建立管理SQL Server登录帐户

1、打开SQL Server 2005的管理工具,选择以windows身份验证模式登陆。然后右击服务器选择属性。2、在打开的服务器属性页面中,选择“安全性”做如下图设置:3.在windows上新建三个组:ReceptionEmployees,ITEmployees。4、然后在SQL …

c++ doxygen 注释规范_利用Doxygen给C程序生成注释文档

利用Doxygen为C程序生成注释文档一、Doxygen工具的安装利用Doxygen工具生成API帮助文档需要下载安装以下三个软件:(1)Doxygen:可以从一套归档源文件开始,生成HTML格式的在线类浏览器,或离线的LATEX、RTF参考手册。本文中所使用的版…

【2021新版】一线大厂 Go 面试题合集

秋天到了,又到了工程师们躁动不安,蠢蠢欲动的季节~这不,金九银十已然到了家门口,现在后台就有不少人问我:现在外边大厂面试都问啥想去大厂又怕面试挂面试应该怎么准备Go 开发前景如何啥样的后端适合切 Go 技术栈...面试…

python开发【基础二】

基本数据类型: 1、数字 在Python2中,分为整形(int)和长整形(long)。 在Python3中,都是int。 #1、将字符串转换为数字 a "123" v int(a) print(v) print(type(v))#2、当前数字的二进制,至少用几位表示 1位等于8个字节.…

can总线资料

应知识星球的同学要求,整理了一些can总线资料。在公众号后台回复 【can总线】获取资料截图推荐阅读:专辑|Linux文章汇总专辑|程序人生专辑|C语言我的知识小密圈

Go语言基础之4--流程控制

一、if else语句 1.1 基本语法1 语法1: if condition { //do something } 语法2: if condition { //do something } else { //do something } 语法3: if condition { //do something } else if condition { //do something } else { //do som…

大家都挺难的​

这是昨天小号发的一个推文小时候觉得读书苦 但是现在想想,如果那时候跟现在一样用功 考上北大清华都没问题然后想到跟我家小云聊天的内容,挺有感触的。小云最近在我们小区的业委会上班,业委会在我家旁边,上班就是从这栋楼到另一栋…

android handler封装_Handler都没搞懂,你拿什么去跳槽啊?!

0. 前言做 Android 开发肯定离不开跟 Handler 打交道,它通常被我们用来做主线程与子线程之间的通信工具,而 Handler 作为 Android 中消息机制的重要一员也确实给我们的开发带来了极大的便利。Handler应用之广泛,可以说只要有异步线程与主线程…

做梦也想有一个这样的实验室

从现在开始,努力攒钱,等有钱了,自己也开一个这样的实验室。推荐阅读:专辑|Linux文章汇总专辑|程序人生专辑|C语言我的知识小密圈关注公众号,后台回复「1024」获取学习资料网盘链接。欢迎点赞,关注&#xff…

努力过头了,其实并不好

昨天下午下班的时候,有一个读者朋友找我聊天。我再说下聊天这个事情,如果大家有事情,可以在群里艾特我,因为重要的群我会置顶,置顶的群我会看消息,你在群里艾特我我一定能看到。当然,在群里发一…

jstack 脚本 自动日志_GitLab从安装到全自动化备份一条龙

原文地址[1]欢迎star需求1.在新服务器上安装并搭建好gitlab2.手动自动将旧服务器上的gitlab备份3.手动自动将gitlab备份包scp到新服务器上4.手动自动恢复新服务器上的gitlab备份包5.在新旧服务器上自动删除过期备份包前提1.版本•gitlab-ce是社区版•gitlab-ee是企业版1.方案•…

纪念音视频界前辈-雷霄骅

这是一篇记录文,纪念一位在音视频领域研究的博士雷霄骅。雷霄骅生前是中国传媒大学通信与信息系统博士在读生,于2016年7月17日凌晨猝死在学校主楼五层,10月就将迎来自己26岁的生日。雷霄骅在音视频领域有很深的造诣和贡献,指导了很…

gcc和g++有什么区别?

来源 | C语言中文网发展至今,GCC 编译器已经更新至 V10 版本,其功能也由最初仅能编译 C 语言,扩增至可以编译多种编程语言,其中就包括 C 。除此之外,当下的 GCC 编译器还支持编译 Go、Objective-C,Objectiv…

druid删除数据_Apache druid 删除数据流程 0.13

背景由于前端时间数据导入出现问题,导致druid 中的数据需要重新导入,但又要防止数据重复,需要把数据彻底清理。问题由于druid 属于时间序列数据库,删除的时候只能时间范围删除。删除流程根据时间范围查询segements 标识(在coordinator节点查询…

使用C语言扩展Python(四)

上一篇里的LAME项目已经展示了python如何与C语言交互,但程序仍不够理想,在python这一端仅仅是传递源文件和目标文件的路径,再调用C模块的encode方法来进行编码,但问题在于你无法控制encode函数,比如你想编码的源文件如…

案例 github_2019年12月Github上最热门的Java开源项目,速来围观!

转眼之间,已经进入了2020年,2019年发生的一切仿佛就在昨天。那么,刚过去不久的12月份GitHub上最热门的Java开源项目排行已经出炉啦。下面我带大家一起来看看上榜详情:1、Alinkhttps://github.com/alibaba/Alink Star 1695Alink 是…

实战CRC校验 | 固件如何校验自身完整性?

来源:公众号【鱼鹰谈单片机】作者:鱼鹰Osprey在一些比较严格的行业里面,不是说你的程序能完成必要功能就可以,还需要添加一些额外的功能,比如最常见的看门狗功能,它可以在程序死机时完成重启,但…

想一个颠覆性技术方向建议,你能想到什么?

如上图,是这次文章的主题。我对这个问题是有想法的,我现在是做音频研究,但是我觉得未来核心的方向一定是能源。试想一下,现在的手机功能越来越多,移动设备将会占领我们未来很长一段时间,那么手机的电池要如…