你缺的东西还挺多的。
第一,同一个进程内部的线程间不存在通信问题,想怎么访问怎么访问;所以我们反而需要做一些事,从而主动“隔离”不同线程,避免数据脏读脏写。
第二,多线程编程(以及多进程编程)都需要操作系统方面的底子。不懂操作系统,多线程协作是做不好的。
具体到你这个案例上,简单说,不要轮询。
轮询这个动作本身就决定了,你的程序必定CPU占用奇高、发热巨大,同时运行缓慢。
这还是程序逻辑过于简单;稍微复杂一点,你这种写法,最终必然是“CPU占用跑满,程序逻辑寸步不前”,和一个死循环的垃圾没有什么差别。
第一步,先设置一个全局的、标准的锁(mutex)。
注意,第一个线程要修改内存数据,需要先申请锁,确保第二个线程不在读取数据;
第二个线程发现数据可用,也要先申请锁,确保第一个线程不会继续修改它。
也就是类似你过去那个“全局变量”的作用;但一定要使用标准的锁、使用标准的acquire系统调用申请锁数据读写权限。
这是因为,标准的mutex是操作系统提供的;当你的某个线程申请mutex失败时,操作系统会把它置于等待队列,在mutex可用前不会继续给它分配时间片,这就避免了忙等;而一旦mutex可用,这个线程就会被移回就绪队列,之后就可能获得时间片了。
这就避免了大量无效的CPU占用。
第二步,认真分析业务逻辑,画出两个线程的状态切换图,确定锁应该有几个、分别是什么状态(比如是否需要读写锁);确保“线程申请到锁就一定可以执行;线程无法执行就一定要进入挂起状态”。
注意,你并不能确定什么时候第二个线程正在读取数据、或者阻塞在哪里长时间没有读取。所以你必须使用足够多的标志位,确保“数据未初始化、数据初始化中、数据初始化完成等待读取、数据读取中、数据读取完成”等状态可清晰区分。否则,数据就可能丢失(线程一产生数据后,线程二尚未得到调度,线程一又用新数据覆盖了之前的数据)或者出现脏读、脏写。
当然,视业务需要,只有ture/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,允许另一个线程访问的时间就会特别特别短;这时候另一个线程实际上是拿不到数据的——除非其中一个线程一口气把缓冲区写满、或者把所有缓冲数据处理完然后陷入阻塞,否则另一个线程可能永远得不到执行机会。这就是术语说的“饿死”,是必须避免的。
因此,请把数据准备放在锁定时间之外;锁定后,只处理一下next/head/tail指针,把节点挂入/取下,然后就马上释放锁。想办法把共享数据弄的“在大部分时候可用”,两个线程才能协作起来。
你可以把这个共享数据结构实现成一个通用的、支持多线程访问的队列,只允许通过pop/push接口访问数据;同时把加解锁放到这两个接口里,从而简化使用逻辑,杜绝错误访问。
事实上,你的这个案例可能还可以进一步优化。
比如,如果只有这么两个线程,且线程1是生产者线程2是消费者(单生产者/消费者模型),那么这里甚至可以不用锁,实现一个标准的环形数组即可——这也是经典的、最简化的无锁编程案例。
但如果参与者更多、逻辑更复杂,那么锁就是必需的;甚至读写锁、旗语、event等东西都必须全面利用起来。
这个就太复杂了,这里一时讲不清,还是自己去看操作系统原理的相关章节吧。
首先纠正一下题主的描述,同一个进程内部的线程不存在“通信”这个问题的,原因很简单,一个进程内部的线程共享该进程的地址空间,因此这些线程天然可以直接访问彼此的数据,因此根本不需要“通信”一说。
题主描述的问题在多线程语境下有一个专门的描述,这不叫线程通信而是叫做线程同步,你在操作系统课上学的晕头转向的并行流的同步互斥问题说的就是这。
回到题主的问题,有一个线程在计算结果,有一个线程要对结果进行处理,那么显然一个线程是数据的“生产者”,另一个线程是数据的“消费者”,这是不是一个非常经典的生产者消费者问题:
注,上图出自《你管这破玩意叫线程?》一文。
操作系统课上是不是有专门讲解过这个问题,上课没好好听讲的要反思了。
实际上很简单,生产者线程生产的数据可以直接放到队列中,消费者线程从队列中取出数据处理即可,这里的难点在于多线程需要同时读写队列,因此这里出现了两个问题:
1,确保队列的互斥访问
2,队列空时消费者线程不可以读,队列满是生产者线程不可以写
PV操作专门用来解决这个问题,随便翻一本操作系统教材都能找到答案。
我的线程学得不太好,不知道线程有没有这样的操作,线程2平时处于休眠状态,当线程1数据生成完毕后,激活线程2,将数据交给线程2去处理,线程2处理完再进入休眠状态,如此循环往复,线程是否有这样的用法呢?
你的思路是对的。如果业务处理比较耗时,更好的模式是用一个线程池。
当计算完成之后,就从线程池里抓出一个线程来,把计算结果交给它处理。
业务处理之后,把空闲的线程又还回到线程池里。这是经典的解决方案。
题主你差了很多东西。由于现代CPU的复杂性,你要是裸着共享数据会以很奇怪的姿势崩的。
线程间通信(或者同步)的基本“套路”,主要有这些:
最常用的跨线程设施,一个用来表达互斥的对象。在某一时刻,只有一个线程能处于“获得了锁”的状态,其它线程都会卡在“去获得锁”的这步,直到锁被释放,才会有另一个线程获得。锁通常用来保护一段代码在同一时刻只被一个线程执行。
锁通常还会有一些特性:
线程可以“等待”在这里,直到别人通知“关门放狗”,才会放过一个或者所有的等待线程。
可以一个(或几个)线程往里面塞东西,另一个(或几个)线程往外取东西。可以依此来分发任务给工作线程。
可以用来表示资源与消费者的数量,比如零为初始状态,整数表示有这么多个可用资源,负数表示有这么多个消费者线程在等待。
一些保证了“一定被同一个线程完成,其它线程要么读到操作前状态,要么读到操作后状态”的组合操作,实际上会调用CPU的一些专用同步指令。主要包含这些:
原子操作拥有最高的编程灵活性,实际上大部分跨线程设施都是用原子操作实现的。
这些玩意里面,很多东西都同时会有操作系统的实现(比如锁和条件变量通常都会作为系统API的一部分),也会有库实现,也会有编译器扩展实现(比如原子操作)。