C 语言线程间怎么通信_-invalid s
你缺的东西还挺多的。
第一,同一个进程内部的线程间不存在通信问题,想怎么访问怎么访问;所以我们反而需要做一些事,从而主动“隔离”不同线程,避免数据脏读脏写。
第二,多线程编程(以及多进程编程)都需要操作系统方面的底子。不懂操作系统,多线程协作是做不好的。
具体到你这个案例上,简单说,不要轮询。
轮询这个动作本身就决定了,你的程序必定CPU占用奇高、发热巨大,同时运行缓慢。
这还是程序逻辑过于简单;稍微复杂一点,你这种写法,最终必然是“CPU占用跑满,程序逻辑寸步不前”,和一个死循环的垃圾没有什么差别。
第一步,先设置一个全局的、标准的锁(mutex)。
注意,第一个线程要修改内存数据,需要先申请锁,确保第二个线程不在读取数据;
第二个线程发现数据可用,也要先申请锁,确保第一个线程不会继续修改它。
也就是类似你过去那个“全局变量”的作用;但一定要使用标准的锁、使用标准的acquire系统调用申请锁数据读写权限。
这是因为,标准的mutex是操作系统提供的;当你的某个线程申请mutex失败时,操作系统会把它置于等待队列,在mutex可用前不会继续给它分配时间片,这就避免了忙等;而一旦mutex可用,这个线程就会被移回就绪队列,之后就可能获得时间片了。
这就避免了大量无效的CPU占用。
第二步,认真分析业务逻辑,画出两个线程的状态切换图,确定锁应该有几个、分别是什么状态(比如是否需要读写锁);确保“线程申请到锁就一定可以执行;线程无法执行就一定要进入挂起状态”。
注意,你并不能确定什么时候第二个线程正在读取数据、或者阻塞在哪里长时间没有读取。所以你必须使用足够多的标志位,确保“数据未初始化、数据初始化中、数据初始化完成等待读取、数据读取中、数据读取完成”等状态可清晰区分。否则,数据就可能丢失(线程一产生数据后,线程二尚未得到调度,线程一又用新数据覆盖了之前的数据)或者出现脏读、脏写。
当然,视业务需要,只有true/false两个状态的锁也许已经够用了,但你必须认真评估、充分讨论之后再这么做——你的问题描述过于简略,无法确定是否能行。
第三步,重新设计共享数据结构,把“锁定时间”降到最低。
从你的描述中可知,线程1是不能停的,需要“不断的生成计算结果”;但如此一来……
而mutex的默认行为是:申请不到锁,就把申请锁的线程挂起。
于是,线程1生成计算结果时,线程2只能等着;而线程2处理计算结果那5ms,线程1也只能等着……
万一操作系统再安排不了时间片,那线程1可能就得等200ms,线程2才得到执行权;线程2执行时,线程1进了等待队列,等线程2释放锁,线程1才移回就绪队列,又等了200ms才得以执行……也就是出现了一个400ms以上的大卡顿。
这样搞的话,你其实根本就不应该用什么多线程。直接放在同一个线程里,收集50ms的数据,然后执行5ms的处理——简单,又不容易出错,效率高,响应快……
想要借多线程提高吞吐率,那么就必须搞一个更好用的数据结构。
比如,一个链表。
链表的每个节点足够容纳50ms的数据;线程1先申请一个节点,把数据写进去,写50ms后,申请这个全局链表的锁,把数据挂进链表——锁定期间只需执行一条把链表末端next指向新节点的操作(可能还需要维护一下头尾指针,不要每次都顺着链表摸到尾)。
类似的,线程2被调度后,申请锁定链表,然后把链条第一个节点移除、指针记录在本地,随即释放锁;然后就可以不受打扰的处理这个节点携带的数据了。
注意,这时候,如果还用最简单的mutex的话,因为所有关于数据结构(链表)的操作都需要先锁定,再检查有无数据;那么线程2可能就会死循环的不停上锁、检查发现没数据,释放锁,然后马上又上锁……也就是绝大部分执行时间都在加解锁上。
所以,这时候我们就不得不搞一个更复杂的东西,比如,让mutex包含多个值。
当mutex非0时,线程2才可以从链表取出节点、同时把mutex值减一,减到0线程2就必须休眠,不要再去访问链表;而线程1每成功往链表加入一个节点,就把mutex值加一……
但这时候,由于线程1/2的读写可能很频繁,如果锁定之后才读写数据的话,那么锁定时间就会是50ms/5ms,允许另一个线程访问的时间就会特别特别短(比如每50ms/5ms解锁若干个ns,也就是超过90%以上的时间里数据都在锁定状态);这时候另一个线程实际上是拿不到数据的,因为操作系统必须恰巧在第一个线程解锁后的若干纳秒里切换时间片、且刚好轮到它运行——除非其中一个线程一口气把缓冲区写满、或者把所有缓冲数据处理完然后陷入阻塞,否则另一个线程可能永远得不到执行机会。这就是术语说的“饿死”,是必须避免的。
因此,请优化算法、优化数据结构,把数据准备/处理放在锁定时间之外。如此一来,锁定后可以只处理一下next/head/tail指针,把节点挂入/取下,然后就马上释放锁。
换句话说,只有想办法把共享数据弄的“在大部分时候可用”,两个线程才能协作起来。
以上完成后,你可以进一步把这个共享数据结构实现成一个通用的、支持多线程访问的队列,只允许通过pop/push接口访问数据;同时把加解锁放到这两个接口里,从而简化使用逻辑,杜绝错误访问。
事实上,你的这个案例可能还可以进一步优化。
比如,如果只有这么两个线程,且线程1是生产者线程2是消费者(单生产者/消费者模型),那么这里甚至可以不用锁,实现一个标准的环形数组即可——这也是经典的、最简化的无锁编程案例。
当然,这样做之前,请确认你的软件运行平台(CPU)明确声明“指针访问是原子操作”,否则……
如果参与者更多、逻辑更复杂,那么锁就是必需的;甚至读写锁、旗语、event等东西都必须全面利用起来。
这个就太复杂了,这里一时讲不清,还是自己去看操作系统原理的相关章节吧。
评论区
醉卧沙场: 其实我感觉他描述的跟个单进程的顺序操作一样,故意搞成两个,还要考虑同步和互斥,要不就是做练习题,要不就是想卖弄一下似的。况且两个线程谈什么数据通信啊,线程很多都是共享的,先做好临界区的保护吧。非不想用单进程,那简单的用个管道不就得了,不知道有多大数据量。即使这样我也觉得他程序多半写不好,然后会发现写成单进程运行的反而更快。这种事情我以前看过太多新人弄了,分一个大项目的小任务下去,有人就爱表现,能写成多进程的绝不写成单进程,最后我说你把你单进程和多进程的两个版本测试比较一下哪个快。结果总是单进程执行的还更快,维护成本还低[捂脸]不是说多进程编程不好,只是说程序员的宗旨不是“只要最复杂的,不要最合适的”,为了展示自己的“技术”而故意将问题复杂化,不是程序员应该做的事。 👍🏽30 💭N/A IP 🕐2022-03-11 18:18:17
│ └── invalid s: 我见过的一个现实案例才叫搞笑……那位是设计模式高手,实现上一口气用了七八个模式。剥去模式的伪装后,他的实现是这样子的:1、起20个线程同时读20个日志;2、第一个检查点,确保20个线程都读完了数据。3、起20个线程同时分析已经读入内存的20份日志;4、第二个检查点,确保20份日志都分析完了。5、起20个线程同时把20份分析完的日志数据通过网卡存入数据库;6、第三个检查点,确保20份数据都存完了。7、返回1,重复如上逻辑。哎哟把我笑的……你就是完全不控制,起20个线程,每个线程一口气把读磁盘数据、分析日志、存储数据一连贯的坐下来,那么也会随机性的出现“线程A读磁盘时线程B跑CPU线程C写网卡”这样的硬件并行时刻吧?尤其20个线程各自别协调,循环读取属于自己的第20*K+N个文件的话,由于资源限制,它们很容易自然进入“57个读磁盘、57个跑分析、57个写网卡”的状态——这实际上已经非常非常的理想了。当然,如果能人为的控制工作线程,确保磁盘、CPU、网卡同时满负荷工作(且调整服务器硬件,使得三者刚好匹配),那就更好了……结果,人家刻意的,一开始必须让CPU和网卡看戏,都不准动,大家都来挤磁盘,挑战挑战它的寻道算法!好了,磁盘忙完了没?没忙完不准进行下一步!我们要确保磁盘看戏!终于,下一步,磁盘和网卡全程看戏,一堆线程硬挤CPU和内存——把两位大佬累的都要起火了,磁盘和网卡却可以断电休眠!好了,CPU忙完没?下一步我们要确保CPU看戏!然后,照例的,磁盘继续休眠,CPU半休眠,网卡累瘫了……这还真是……没有瓶颈也要制造瓶颈啊……尤其一窝蜂挤兑磁盘……这实现,你乖乖跑单线程,速度提高一两倍都不成问题吧? 👍🏽22 💭N/A IP 🕐2022-03-12 13:14:30
│ └── 醉卧沙场: 额……这checkpoint设计的就很“迷”,我没能理解为什么要这么设计检查点,就起20个线程/进程处理20个日志不行吗?有什么其它原因导致20份日志的读取、分析和结果转存必须在检查点的位置进行同步吗?比如需要保证20份日志的分析结果都存储后才能构成一份完整的可用数据,于是在这个地方设一个检查点,如果在检查点前崩溃,则撤销上一个检查点到这个未完成的检查点之间的所有操作。 👍🏽3 💭N/A IP 🕐2022-03-12 15:20:49
│ │ └── invalid s: 谁知道那位脑子里在想什么……另一个更搞笑。当我指出这个问题时,人家坚持“这样也是对的……单个CPU上面不可能出现真正的并行”……这种认识水平,你还能指望什么…… 👍🏽4 💭N/A IP 🕐2022-03-12 17:08:32
│ │ └── invalid s: 没有任何约束,日志条目是一条条各自独立的。他就是要在某个方法里wait所有thread然后调用join回收掉而已…… 👍🏽0 💭N/A IP 🕐2022-03-12 17:10:08
│ │ └── 醉卧沙场: 哈哈哈,总有人喜欢过度设计[捂脸] 👍🏽3 💭N/A IP 🕐2022-03-12 17:34:45
│ └── 允许说活该: 估计题主看不大懂 👍🏽0 💭N/A IP 🕐2022-03-25 06:33:32
雾猫: 回答的也太良心了。。。 👍🏽12 💭N/A IP 🕐2022-03-11 18:39:30
core dumped: 这是免费能看的回答吗[大哭] 👍🏽7 💭N/A IP 🕐2022-04-01 10:17:25
wyf: 可以给他出个损招吗?用socket本地回环[doge] 👍🏽4 💭N/A IP 🕐2022-03-11 17:13:35
│ └── 末那: 这招损吗?why?我确实这么干过,用来做事件通告,方便一个dpdk轮询线程激活另一个io线程 👍🏽0 💭N/A IP 🕐2022-03-11 18:06:33
│ └── 神羽鸦青: socket是用来网络上不同主机通信的,本机俩程序用这个已经有点过了,然后题主这个是俩线程,同一个程序,用socket相当于左手交右手的东西非要让第三个人转交。。。 👍🏽1 💭N/A IP 🕐2022-03-14 11:19:26
│ │ └── 末那: 本机domain socket很常用啊,只是一般用于进程间 👍🏽1 💭N/A IP 🕐2022-03-14 11:21:16
│ │ └── 神羽鸦青: domain socket就是设计给进程间通讯的啊,socket是设计给网络通信的,这二者又不一样 👍🏽0 💭N/A IP 🕐2022-03-25 09:47:36
│ │ └── 末那: 哦,我懂了,他说的是用网络socket来玩本机跨线程通信……那是有点邪道。我以为是说domain socket做线程间而不是进程间有点损 👍🏽0 💭N/A IP 🕐2022-03-25 10:35:42
│ └── wyf: 这招比较偷懒[害羞] 👍🏽0 💭N/A IP 🕐2022-03-15 07:37:30
│ └── 末那: 其实在某些场景下我确实没想到比domain socket做线程间通信更好的方法,比如io线程同时也在poll其他异步socket(所以会被poll休眠),而轮询线程的消息要尽早处理,这时候弄个domain socket加入到poll里面最省事且时延最小 👍🏽1 💭N/A IP 🕐2022-03-15 07:44:05
│ └── wyf: 我觉得主要就是省事[doge] 👍🏽0 💭N/A IP 🕐2022-03-15 07:53:44
允许说活该: 比较专业的回答。希望题主能认清事实,从基础学起,不要好高骛远。基础不牢,最终房倒屋塌。 👍🏽4 💭N/A IP 🕐2022-03-25 06:29:15
乘海生: 这个必须点赞,问这个问题属于没看过unp的。 👍🏽2 💭N/A IP 🕐2022-03-11 16:31:12
神羽鸦青: 看到一半我就在想做个队列吧 👍🏽3 💭N/A IP 🕐2022-03-11 18:00:17
左江: 都是标准实现 让我回忆了996的那几年哭了 👍🏽1 💭N/A IP 🕐2022-03-11 23:07:11
小微粒: 好家伙,代码在脑子撸了一遍 👍🏽1 💭N/A IP 🕐2022-03-11 19:03:06
│ └── invalid s: 哈哈,见到需求在大脑中把程序实现一遍、该优化的优化到极致,这应该算专业码农的基本操作了。 👍🏽11 💭N/A IP 🕐2022-03-11 19:08:17