这句话是一条编程指导建议,和golang本身关系不大。
它适用于所有多线程、多进程程序设计场合,特别是在并发框架设计中是一种非常重要的思想。作为并发编程的初学者,可以把这句话当成一个结论,先记下来,之后在实践中慢慢体会。
而这句不痛不痒的话,却是前辈们走了很多弯路、尝试了很多种通信方法才总结出的经验。
1、“内存共享”有着巨大的潜力和诱惑力
如果在一个系统中,两个线程或进程,都可以读写同一块内存空间,这就叫做“内存共享”。直觉上会觉得这种方式非常方便。
在内存共享的情景下,系统之间不需要做频繁的沟通,所有必要的信息都在内存中,想取就可以随时取。比如A和B两个游戏角色,B想查阅A的数据,就在内存中找到它,直接读取即可。
所有的数据都是最新的,没有复制备份,就不会有过期的困扰。
而且还有更多好处——比如传输超大量的数据时。假设A对象数据量很大,占100MB空间,那么B可以随时访问这100MB空间,不需要在内存中拷来拷去。只要有这100MB内存的首地址指针就可以了。拷贝数据的消耗为0。
收发消息的需求也不难做,A只要在特定的地方写下一个消息内容,B之后来读就可以了。毕竟大家的信息本质上都是共享的。
由于共享内存有这么多好处,所以人们一直没有放弃对内存共享的探索。
2、“内存共享”的天然缺陷
但是,内存共享看起来很美好,实际有着决定性的弊端。
但凡懂一点并发的人都知道——问题在于数据冲突。多线程或多进程场景下,多个对象同时访问同一块数据,几乎一定会产生数据冲突。
为了对抗数据冲突,人们发明了很多机制。比如加锁、信号量、原子锁、巧妙的多线程算法等等。但是这些算法看起来很高级,实际上要么会影响并发性能、要么对使用场景有要求、要么很烧脑很难证明正确性。
在实践中,运行效率再高,也得以正确性为前提才可以。如果因为并发影响了结果正确性,那就毫无意义;如果因为照顾正确性影响了并发性能,那不如直接写成单线程程序。
3、比“内存共享”更合理的解决方案
人们通过多年试错和迭代,最终“通过通信来实现进程/线程间交互”的方案脱颖而出,成为了大多数人的共识。
通过通信让多线程/多进程交互,有多种具体的技术方案。erlang、go等语言在语言核心层提供了相应的功能,其它语言比如c#可以通过Task和相关的库提供类似功能。游戏服务器领域的skynet(用c语言+lua编写)也从零实现了高性能的actor框架。
各种技术方案百花齐放,它们背后的思想高度相似——先提供一个或多个高性能队列,线程/进程/微服务之间需要访问别人时,不能直接读写别人的数据,而要通过队列提出请求,然后在对方处理请求时再做相应处理。
这种设计一定程度上会增加代码的复杂度,但是规范化以后还是比较容易开发的。
通过良好的设计,整个逻辑就理顺了,只要遵循框架的规范,很多对象可以同时运行、高效协作。不会再遇到互相抢数据、读写错乱的情况。
总结
以上从整体的角度解释了“内存共享”与“消息通信”的基本思想。