Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

为什么使用通信来共享内存?· Why's THE Design? #156

Open
draveness opened this issue Oct 24, 2019 · 20 comments
Open

为什么使用通信来共享内存?· Why's THE Design? #156

draveness opened this issue Oct 24, 2019 · 20 comments

Comments

@draveness
Copy link
Owner

@draveness draveness commented Oct 24, 2019

https://draveness.me/whys-the-design-communication-shared-memory

『不要通过共享内存来通信,我们应该使用通信来共享内存』,这是一句使用 Go 语言编程的人经常能听到的观点,然而我们可能从来都没有仔细地思考过 Go 语言为什么鼓励我们遵循这一设计哲学,我们在这篇文章中就会介绍为什么我们应该更倾向于使用通信的方式交换消息,而不是使用共享内存的方式。

@moshiale

This comment has been minimized.

Copy link

@moshiale moshiale commented Oct 25, 2019

这个系列写的非常好。

@draveness

This comment has been minimized.

Copy link
Owner Author

@draveness draveness commented Oct 25, 2019

@moshiale
这个系列写的非常好。

多谢,欢迎提一些感兴趣的问题

@moshiale

This comment has been minimized.

Copy link

@moshiale moshiale commented Oct 28, 2019

@draveness

@moshiale
这个系列写的非常好。

多谢,欢迎提一些感兴趣的问题

为什么Redis 字符串只支持扩容到512MB?

@wingsxdu

This comment has been minimized.

Copy link

@wingsxdu wingsxdu commented Oct 29, 2019

受教了

@lichongsw

This comment has been minimized.

Copy link

@lichongsw lichongsw commented Nov 11, 2019

写的很好啊。总结中的第3点比较重要,但并不容易让人明白这里的设计优势有多明显。

Go 语言选择消息发送的方式,通过保证同一时间只有一个活跃的线程能够访问数据,能够从设计上天然地避免线程竞争和数据冲突的问题

例如加一些细节,说明在chanel维护所有被该chanel阻塞的协程(读,写各一双向链表)来保证有资源的时候只唤醒一个协程来避免竞争之类的

@draveness

This comment has been minimized.

Copy link
Owner Author

@draveness draveness commented Nov 11, 2019

@lichongsw
写的很好啊。总结中的第3点比较重要,但并不容易让人明白这里的设计优势有多明显。

Go 语言选择消息发送的方式,通过保证同一时间只有一个活跃的线程能够访问数据,能够从设计上天然地避免线程竞争和数据冲突的问题

例如加一些细节,说明在chanel维护所有被该chanel阻塞的协程(读,写各一双向链表)来保证有资源的时候只唤醒一个协程来避免竞争之类的

第三点确实是决定性的设计因素,这里其实更想介绍的是一种并发模型,Golang 也只是一个特定的实现,主要作用也是帮助我们理解文中的问题

@draveness

This comment has been minimized.

Copy link
Owner Author

@draveness draveness commented Nov 11, 2019

@lichongsw
写的很好啊。总结中的第3点比较重要,但并不容易让人明白这里的设计优势有多明显。

Go 语言选择消息发送的方式,通过保证同一时间只有一个活跃的线程能够访问数据,能够从设计上天然地避免线程竞争和数据冲突的问题

例如加一些细节,说明在chanel维护所有被该chanel阻塞的协程(读,写各一双向链表)来保证有资源的时候只唤醒一个协程来避免竞争之类的

第三点确实是决定性的设计因素,这里其实更想介绍的是一种并发模型,Golang 也只是一个特定的实现,主要作用也是帮助我们理解文中的问题

BTW:你在这里举的例子确实也非常好

@chapche

This comment has been minimized.

Copy link

@chapche chapche commented Nov 18, 2019

还可以通过回调的方式传递信息;共享数据库支持事务,暂时想到这些。

@draveness

This comment has been minimized.

Copy link
Owner Author

@draveness draveness commented Nov 18, 2019

还可以通过回调的方式传递信息;共享数据库支持事务,暂时想到这些。

非常感谢你的回答,我对他们的看法是这样的:

  • 回调的方式传递信息最后是不是仍然会发送消息;
  • 共享数据库能够在多个机器之间传递消息,它使用了一些序列化和网路在不同的进程之间通信;
@chapche

This comment has been minimized.

Copy link

@chapche chapche commented Nov 18, 2019

@draveness

还可以通过回调的方式传递信息;共享数据库支持事务,暂时想到这些。

非常感谢你的回答,我对他们的看法是这样的:

  • 回调的方式传递信息最后是不是仍然会发送消息;
  • 共享数据库能够在多个机器之间传递消息,它使用了一些序列化和网路在不同的进程之间通信;

感谢大佬,没有想到共享数据库有这个作用,一般不同机器进程的通信都只想到socket;
回调也会发送消息吧,只是把消息以参数的形式发送过去了。比如一个线程池里面,子线程在等待条件,主线程可以bind一个函数并调用cond_signal唤醒子线程,这样就算传递消息了,也不会出现线程之间的竞争

@draveness

This comment has been minimized.

Copy link
Owner Author

@draveness draveness commented Nov 18, 2019

感谢大佬,没有想到共享数据库有这个作用,一般不同机器进程的通信都只想到socket;

我理解通过数据库进行通信其实也间接使用了 Socket,我也没有想到跨机器进行通信除了 Socket 还有什么别的办法,你能想到么?

回调也会发送消息吧,只是把消息以参数的形式发送过去了。比如一个线程池里面,子线程在等待条件,主线程可以bind一个函数并调用cond_signal唤醒子线程,这样就算传递消息了,也不会出现线程之间的竞争

嗯呢,确实也是一种通信的方式,这种场景有点像传递一个 handler,然后等待完成时通过 handler 直接调用目标对象/进程

@514366607

This comment has been minimized.

Copy link

@514366607 514366607 commented Jan 10, 2020

刚转用go写游戏,角色的数据都是存放到sync.map里,就面临这个问题。
但是脑子转不过来,只能想到用map存着多个channel ,单个角色一个channel来控制,使用完再放回去?

@draveness

This comment has been minimized.

Copy link
Owner Author

@draveness draveness commented Jan 10, 2020

刚转用go写游戏,角色的数据都是存放到sync.map里,就面临这个问题。
但是脑子转不过来,只能想到用map存着多个channel ,单个角色一个channel来控制,使用完再放回去?

我不太清楚你的具体场景,可以稍微展开一下吗

@514366607

This comment has been minimized.

Copy link

@514366607 514366607 commented Jan 10, 2020

@draveness

刚转用go写游戏,角色的数据都是存放到sync.map里,就面临这个问题。
但是脑子转不过来,只能想到用map存着多个channel ,单个角色一个channel来控制,使用完再放回去?

我不太清楚你的具体场景,可以稍微展开一下吗

在单进程模式下,角色数据直接放内存里。然后grpc的handler开放各个接口给网关,但是由于grpc还有定时器的原因会引起race。
角色信息放redis里就用网络io,会慢。但是高可用就很容易,但是为了性能就不考虑了。
然后数据就在本地的var roleDatas = sync.Map这里存放着,不知道这个角色数据该怎么能设计成csp模式

@514366607

This comment has been minimized.

Copy link

@514366607 514366607 commented Jan 10, 2020

刚转用go写游戏,角色的数据都是存放到sync.map里,就面临这个问题。
但是脑子转不过来,只能想到用map存着多个channel ,单个角色一个channel来控制,使用完再放回去?

我不太清楚你的具体场景,可以稍微展开一下吗

最后就锁来锁去的,锁的颗粒又大。感觉这不是一条正确的路,但又不知道应该怎么改动。

@draveness

This comment has been minimized.

Copy link
Owner Author

@draveness draveness commented Jan 10, 2020

@draveness

刚转用go写游戏,角色的数据都是存放到sync.map里,就面临这个问题。
但是脑子转不过来,只能想到用map存着多个channel ,单个角色一个channel来控制,使用完再放回去?

我不太清楚你的具体场景,可以稍微展开一下吗

在单进程模式下,角色数据直接放内存里。然后grpc的handler开放各个接口给网关,但是由于grpc还有定时器的原因会引起race。
角色信息放redis里就用网络io,会慢。但是高可用就很容易,但是为了性能就不考虑了。
然后数据就在本地的var roleDatas = sync.Map这里存放着,不知道这个角色数据该怎么能设计成csp模式

可以从数据的生产和消费来思考这个问题,想清楚数据的流向。

@514366607

This comment has been minimized.

Copy link

@514366607 514366607 commented Jan 11, 2020

用生产和消费的思路做的话就会产生数据唯一性的问题,还有性能问题。
因为每个角色都是单独的生产者和消费者,不是生产-》消费就结束了,还有进行数据的修改保存。

好像角色使用一个道具,到grpc表现就是在handler里的一个接口
func (*Item)UseItem( ctx context.Context, req xxx.param ) (resp , error)。
参数带个 RoleID
然后这里角色数据的问题就难搞了

  • 从数据库或者redis之类的cache拿,锁用redis做分布式锁。(这种情况就慢,但是清析)
  • 从数据库或者redis之类的cache拿,存入到内存里,sync.RWMutex做锁,共享内存来做数据存放

现在只想到有这两种处理方式,如果用生产消费者模式做,数据的唯一性和消费者的设计会很大问题。
数据生产出来后,如何存放?
所以才有最开始说的

刚转用go写游戏,角色的数据都是存放到sync.map里,就面临这个问题。
但是脑子转不过来,只能想到用map存着多个channel ,单个角色一个channel来控制,使用完再放回去?

var roleChans = sync.Map里存放着每个角色channel
角色A操作货币-1

roleTmp := <- A channel
roleTmp.货币 -1
 A channel <- roleTmp

中途有其它协程要处理A只能等 A放回到channel才能处理
但是这种说到底其实就是一种共享内存。。。。。只是锁换为了channel。

水平低实在想不懂如何摆脱 共享内存来通信 问题。

@draveness

This comment has been minimized.

Copy link
Owner Author

@draveness draveness commented Jan 11, 2020

用生产和消费的思路做的话就会产生数据唯一性的问题,还有性能问题。
因为每个角色都是单独的生产者和消费者,不是生产-》消费就结束了,还有进行数据的修改保存。

好像角色使用一个道具,到grpc表现就是在handler里的一个接口
func (*Item)UseItem( ctx context.Context, req xxx.param ) (resp , error)。
参数带个 RoleID
然后这里角色数据的问题就难搞了

  • 从数据库或者redis之类的cache拿,锁用redis做分布式锁。(这种情况就慢,但是清析)
  • 从数据库或者redis之类的cache拿,存入到内存里,sync.RWMutex做锁,共享内存来做数据存放

现在只想到有这两种处理方式,如果用生产消费者模式做,数据的唯一性和消费者的设计会很大问题。
数据生产出来后,如何存放?
所以才有最开始说的

刚转用go写游戏,角色的数据都是存放到sync.map里,就面临这个问题。
但是脑子转不过来,只能想到用map存着多个channel ,单个角色一个channel来控制,使用完再放回去?

var roleChans = sync.Map里存放着每个角色channel
角色A操作货币-1

roleTmp := <- A channel
roleTmp.货币 -1
 A channel <- roleTmp

中途有其它协程要处理A只能等 A放回到channel才能处理
但是这种说到底其实就是一种共享内存。。。。。只是锁换为了channel。

水平低实在想不懂如何摆脱 共享内存来通信 问题。

使用共享内存来通信是无法摆脱的,共享内存时最接近硬件的通信方式。作为最接近硬件的通信方式,它有最好的性能,但是也以为着它的抽象层级非常低。

我不可能有你了解的背景和需求,没法帮你解决这个问题。从你的这段描述来看,你现在所有的数据都是放在一个 sync.Map 中的,这说明数据结构可能设计的有问题。从数据的生产和消费来思考这个问题,想清楚数据的流向,重新设计数据结构就是我在看到具体代码之前能给的建议。

很多时候性能问题都是一个伪命题。多数情况下共享内存的方式都会比 CSP 要快,但是这也不绝对的,如果数据都存储在一个桶中,而锁的粒度太大,触发太过频繁,使用 CSP 重新思考应该如何设计可以缓解这个问题。

遵循已有的设计是一个非常便捷、省事的做法,但是这也阻止了我们用好这门语言。既然认为现有的设计不够理想,那么我们想的不应该是现有的结构没法重写,都是通用编程语言,肯定有等价的方法,还是要想想按照 Go 语言提供的抽象怎么梳理和重新构建现在的功能。

希望能够对你有所帮助。

@514366607

This comment has been minimized.

Copy link

@514366607 514366607 commented Jan 11, 2020

用生产和消费的思路做的话就会产生数据唯一性的问题,还有性能问题。
因为每个角色都是单独的生产者和消费者,不是生产-》消费就结束了,还有进行数据的修改保存。
好像角色使用一个道具,到grpc表现就是在handler里的一个接口
func (*Item)UseItem( ctx context.Context, req xxx.param ) (resp , error)。
参数带个 RoleID
然后这里角色数据的问题就难搞了

  • 从数据库或者redis之类的cache拿,锁用redis做分布式锁。(这种情况就慢,但是清析)
  • 从数据库或者redis之类的cache拿,存入到内存里,sync.RWMutex做锁,共享内存来做数据存放

现在只想到有这两种处理方式,如果用生产消费者模式做,数据的唯一性和消费者的设计会很大问题。
数据生产出来后,如何存放?
所以才有最开始说的

刚转用go写游戏,角色的数据都是存放到sync.map里,就面临这个问题。
但是脑子转不过来,只能想到用map存着多个channel ,单个角色一个channel来控制,使用完再放回去?

var roleChans = sync.Map里存放着每个角色channel
角色A操作货币-1

roleTmp := <- A channel
roleTmp.货币 -1
 A channel <- roleTmp

中途有其它协程要处理A只能等 A放回到channel才能处理
但是这种说到底其实就是一种共享内存。。。。。只是锁换为了channel。
水平低实在想不懂如何摆脱 共享内存来通信 问题。

使用共享内存来通信是无法摆脱的,共享内存时最接近硬件的通信方式。作为最接近硬件的通信方式,它有最好的性能,但是也以为着它的抽象层级非常低。

我不可能有你了解的背景和需求,没法帮你解决这个问题。从你的这段描述来看,你现在所有的数据都是放在一个 sync.Map 中的,这说明数据结构可能设计的有问题。从数据的生产和消费来思考这个问题,想清楚数据的流向,重新设计数据结构就是我在看到具体代码之前能给的建议。

很多时候性能问题都是一个伪命题。多数情况下共享内存的方式都会比 CSP 要快,但是这也不绝对的,如果数据都存储在一个桶中,而锁的粒度太大,触发太过频繁,使用 CSP 重新思考应该如何设计可以缓解这个问题。

遵循已有的设计是一个非常便捷、省事的做法,但是这也阻止了我们用好这门语言。既然认为现有的设计不够理想,那么我们想的不应该是现有的结构没法重写,都是通用编程语言,肯定有等价的方法,还是要想想按照 Go 语言提供的抽象怎么梳理和重新构建现在的功能。

希望能够对你有所帮助。

我水平还不够,希望后面用多了会理解这种思路吧。thx了。

@draveness

This comment has been minimized.

Copy link
Owner Author

@draveness draveness commented Jan 11, 2020

我水平还不够,希望后面用多了会理解这种思路吧。thx了。

有问题可以在博客下面留言交流

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
You can’t perform that action at this time.