使用go的并发性来解决Hilbert酒店问题
译自:Designing for Concurrency: the Hilbert’s Hotel Problem in Go,本文使用go的并发性来解决Hilbert酒店问题。本文比较有意思的是它对问题的描述很吸引人,在看完文字描述之后,代码实现逻辑也基本顺理成章,当然代码本身的实现也相当优雅。
文章一开始叙述了并发和并行的区别和联系,此处略去该部分。
Hilbert酒店
Hilbert酒店是一个与无限有关的问题。
假设Hilbert是一个酒店的所有者,且该酒店有无限个房间。
一天一个大巴达到了Hilbert的酒店,假设大巴上有无限个旅客想要住在Hilbert的酒店,假设Hilbert的酒店有无限个房间,因此能够容纳无限个旅客。
但有一天有无限辆大巴达到了Hilbert的酒店,每辆大巴上载有无限个旅客,且所有大巴的旅客都要在酒店过夜。
Hilbert想了一会说"没问题",然后他画了一个数字三角形,然后把房间的钥匙递给沿着三角形对角线的所有乘客。通过这种方式,Hilbert能够给每辆大巴的每个乘客一把唯一的钥匙。
Hilbert酒店的非并发算法
我们可以通过重复Hilbert的做法来解决Hilbert酒店问题。
例如,可以创建数组的数组来代表三角形的数字,然后遍历该数组的数组来构建对角线并沿着对角线将房间钥匙交给每辆大巴的乘客。文章末给出了这种方式的实现,当然也存在其他的非并发实现。下面换个角度看下这个问题。
Hilbert酒店的递归并发算法
递归并发算法中,每辆大巴所需的钥匙位于三角形的最右侧对角线。对角线中的钥匙所在的位置等于三角形高度,这一点就是判定某把钥匙是不是本大巴的关键。
假设Hilbert的酒店中有无限个雇员,每个雇员负责给一辆大巴的所有旅客发放钥匙。负责第一辆大巴的雇员(Bus 1 Clerk)靠近负责第二辆大巴的雇员(Bus 2 Clerk),负责第二辆大巴的雇员(Bus 2 Clerk)靠近负责第三辆大巴的雇员(Bus 3 Clerk),以此类推。
此外还有一个雇员(Room Key Clerk),他负责将所有房间的钥匙依次交给第一个雇员(Bus 1 Clerk),即房间一是第一把钥匙,房间二是第二把钥匙,房间三是第三把钥匙,以此类推。
(从Room Key Clerk接收到钥匙的)Bus 1 Clerk知道接收到的第一把钥匙是给他负责的大巴的第一个乘客,因此Bus 1 Clerk会为第一辆巴士的第一位乘客准备一号房间的含钥匙在内的欢迎礼包(welcome kit),此外他还知道接收到的第二把钥匙不是给他负责的大巴的,而是给下一个雇员的,第三把钥匙是给Bus 1的,因此由Bus 1 Clerk负责,但第四把和第五把钥匙不是给Bus 1的,因此Bus 1 Clerk会将它们传给下一个雇员。当欢迎礼包发放给Bus 1的所有乘客之后(此时需要假设顾客是有限的,防止程序无法结束),Bus 1 Clerk会将它们返还给Hilbert。后面将会看到,将钥匙还给Hilbert是一个有用的步骤,可以并发实现最终的类似reduce的操作。
下一个雇员Bus 2 Clerk的行为和第一个雇员相同,他知道接收到的第一个钥匙需要交给他负责的大巴的第一个乘客,而第二把钥匙则需要交给下一个雇员,以此类推。
在某种程度上,由于我们的程序不能像原来的Hilbert问题那样永远继续下去,因此需要能够停止移交钥匙,并通知所有雇员停止工作。此时需要将雇员准备的欢迎礼包还给Hilbert。最终,所有的雇员都会停止工作,并在Hilbert接收到准备好的欢迎礼包之后就可以通知Hilbert也停止工作。
Go实现
这里提供了一个Go实现的并发算法。使用goroutine来代表每一个角色,包括Hilbert和雇员。
- Hilbert是主goroutine,用于启动整个流程并收集大巴雇员创建的欢迎礼包
- Key Handler Clerk是一个由Hilbert启动的goroutine,负责依次一系列房间钥匙,并交给Bus 1 Clerk,直到达到钥匙上限
- Bus 1 Clerk是另一个由Hilbert启动的goroutine,它从Key Handler Clerk接收钥匙,并实现对应的逻辑:为自己负责的大巴准备欢迎礼包,并将其他钥匙交给下一个雇员
- Bus 2 Clerk是由Bus 1 Clerk启动的goroutine,处理逻辑和Bus 1 Clerk相同
- 其他Bus Clerks都执行相同的逻辑,每一个都是一个独立的goroutine,且共享相同的代码
下面是Hilbert的实现:
func Hilbert(upTo int) {
keysCh := make(chan int)
go RoomKeysClerk(upTo, keysCh) //1
make(chan []hilberthotel.WelcomeKit)
go BusClerk(1, keysCh, welcomeKitCh) //2
for envelope := range welcomeKitCh { //3
fmt.Println(envelope)
}
}
其代码比较简单,入参upTo
表示房间钥匙的上限:
- 首先它会为钥匙创建channel
keysch
,并使用keysch
作为参数来启动Room Key Clerk的goroutine。 - 然后它会创建另一个channel
welcomeKitCh
(雇员会从该channel中接收钥匙,并在雇员结束工作后发送欢迎礼包),用于接收Welcome Kits(欢迎礼包),并使用keysch
和welcomeKitCh
作为参数来启动第一个BusClerk(大巴雇员) - 最后,它会通过
welcomeKitCh
循环接收雇员准备的礼包
Room Key Clerk 的实现也很简单,它通过keysCh
将钥匙分发出去,在钥匙到达上限upTo
之后,关闭keysCh
:
func RoomKeysClerk(upTo int, keysCh chan<- int) {
for i := 0; i < upTo; i++ {
keysCh <- i + 1
}
close(keysCh)
}
Bus Clert的实现要复杂一些:
func BusClerk(busNumber int, roomKeysCh <-chan int, welcomeKitsCh chan<- []hilberthotel.WelcomeKit, buffer int, delay time.Duration) { //1
var count = 0 //2
var keyPosition = 0
var nextClerkCh chan int
welcomeKits := []hilberthotel.WelcomeKit{}
for roomKey := range roomKeysCh {
if nextClerkCh == nil { //3
nextClerkCh = make(chan int, buffer)
go BusClerk(busNumber+1, nextClerkCh, welcomeKitsCh, buffer, delay)
}
if count == keyPosition { //4
kit := hilberthotel.NewWelcomeKit(busNumber, keyPosition, roomKey, delay)
welcomeKits = append(welcomeKits, kit)
keyPosition++
count = 0
continue
}
nextClerkCh <- roomKey
count++
}
if nextClerkCh != nil { //5
welcomeKitsCh <- welcomeKits
close(nextClerkCh)
} else {
close(welcomeKitsCh) //6
}
}
- 每个Bus Clert对应一个goroutine。BusClerk的第一个参数是其所属的大巴号,
welcomeKitCh
用于在处理结束之后发送欢迎礼包(welcomeKits
),roomKeysCh
用于读取钥匙号 - 在初始化内部计数器
count
之后,使用一个变量keyPosition
来保存下一个乘客的钥匙位置,使用channelnextClerkCh
通知下一个BusClerk。通过循环读取roomKeysCh
来启动整个处理逻辑。 - 在接收到第一个钥匙之后,此时
nextClerkCh
为nil,之后会初始化nextClerkCh
并启动下一个BusClerk - 对比
count
和keyPosition
,用于确定此时的钥匙是给本大巴的还是给其他大巴的。当钥匙是给本大巴的,它会创建一个WelcomeKit并将其保存在它的集合中,反之,将钥匙传给下一个BusClerk - 当钥匙接收完之后(即
roomKeysCh
被关闭),它会关闭用于通知下一个BusClerk的nextClerkCh
channel - 最后一个BusClerk将不会再接收到任何房间钥匙,因此它会关闭
welcomeKitCh
并通知Hilbert其将结束任务
相对独立的处理流程
上述解决方案基于"相对独立"的处理流程:
- Room Key Clerk的任务
相对独立
于Bus Clerk 1,这里说的"相对独立"并不是完全独立,因为Room Key Clerk还需要等待Bus Clerk 1从keyCh
channel接收钥匙(即使用了带缓存的 keysCh channel,缓冲区仍然会被填满,从而迫使 Room Key Clerk 等待Bus Clerk 1从通道读取数据) - 类似地,Bus Clerk 1也会依赖Bus Clerk 2来从它们共享的keysCh中读取数据,以此类推
- 最终,所有的Bus Clerk 都会依赖Hilbert从
welcomeKitCh
channel中接收welcomeKits
因此,我们可以说所有的Bus Clerk和Hilber执行的都是"相对独立"的逻辑,需要通过调整channel的发送/接收来达到期望的结果。
并发是有代价的,但启用并行可以带来好处
虽然我们的并发设计实现的方案很优雅,但它也带来了如下开销:
- 生成的goroutine数目等于大巴的数目 + Hilbert + Room Key Clerk
- 需要不断在可用的核上调度goroutines
- 需要初始化和goroutines一样多的channels
- 需要通过这些channels执行发送和接收操作
另一方面,这种设计的好处在于,如果在多核硬件上运行该并发算法,算法中固有的并行逻辑会带来性能上的提升。
并发goroutine的任务越重,并行的提升幅度越大
每个大巴雇员都有一些核心的工作需要完成,此外还有一些并发编排的工作(即配置goroutine和通过channel发送/接收钥匙)。
并行可以提升核心工作的性能。在Hilbert的例子中,核心工作就是为每个客户准备欢迎礼包(即函数NewWelcomeKit执行的内容)。能够并行执行的核心工作越多,就越能在相同的时间内服务更多的顾客。
为了实现并行处理,我们需要执行一些并发编排工作。与并发编排工作相比,核心工作占主导地位越高,从并行性中获得的好处就越多。在Hilbert的例子中,每个大巴雇员的核心工作是准备欢迎礼包。因此,准备欢迎礼包的工作占比越高,多核硬件上运行并发设计的效率就越高。另一方面,处理的顾客越多,并发编排工作也就越重(由于需要在更多的goroutines之间进行切换和发送/接收钥匙),因此并发的成本也会越高。
可以使用样例代码提供的benchmarks,通过变更顾客数目来对性能进行验证。
附录
也可以使用上面所使用的并发方案的基本设计导出非并发递归方案:
- 将RoomKeysClerk 转变为一个生成钥匙并将其传递给第一个BusClerk的循环
- 使用闭包(即一个封装了上下文状态的函数(当前计数器,keyPosition和 nextClerk))来实现BusClerk,
- 使用Hilbert函数用来触发整个执行逻辑,并收集各个BusClerk构造的welcomeKits
使用go的并发性来解决Hilbert酒店问题的更多相关文章
- 违反并发性: UpdateCommand影响了预期 1 条记录中的 0 条 解决办法
本文转载:http://www.cnblogs.com/litianfei/archive/2007/08/16/858866.html UpdateCommand和DeleteCommand出现DB ...
- SQL锁表解决并发性
在数据库开发过程中,不得不考虑并发性的问题,因为很有可能当别人正在更新表中记录时,你又从该表中读数据,那你读出来的数据有可能就不是你希望得到的数据.可以说有些数据同时只能有一个事物去更新,否则最终显示 ...
- 深入了解 Scala 并发性
2003 年,Herb Sutter 在他的文章 “The Free Lunch Is Over” 中揭露了行业中最不可告人的一个小秘密,他明确论证了处理器在速度上的发展已经走到了尽头,并且将由全新的 ...
- Flume-NG中Transaction并发性探究
我们曾经在Flume-NG中的Channel与Transaction关系(原创)这篇文章中说了channel和Transaction的关系,但是在source和sink中都会使用Transaction ...
- Java并发编程:进程和线程之由来__进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能
转载自海子:http://www.cnblogs.com/dolphin0520/p/3910667.html Java多线程基础:进程和线程之由来 在前面,已经介绍了Java的基础知识,现在我们来讨 ...
- 读写分离提高 SQL Server 并发性
转自:http://www.canway.net/Lists/CanwayOriginalArticels/DispForm.aspx?ID=476 在一些大型的网站或者应用中,单台的SQL Serv ...
- Java并发性和多线程
Java并发性和多线程介绍 java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题, ...
- Java并发性和多线程介绍
java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题,多开线程就好: 快速响应,异步式 ...
- SQL Server数据库读写分离提高并发性
在一些大型的网站或者应用中,单台的SQL Server 服务器可能难以支撑非常大的访问压力.很多人在这时候,第一个想到的就是一个解决性能问题的利器——负载均衡.遗憾的是,SQL Server 的所有版 ...
- 提高Django高并发性的部署方案(Python)
方案: nginx + uWSGI 提高 Django的并发性 1. uWSGI : uWSGI是一个web服务器,实现了WSGI协议.uwsgi协议.h ...
随机推荐
- aspose word导出表格
[HttpGet] [Route("GetPurchaseItemWord")] public IHttpActionResult Get_PurchaseItemWord(str ...
- demo----日常报错
yolov5:报错1:OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized ...
- C# 内存回收
开发完成之后发现自己写的程序内存占用太高,找到如下解决方案 使用了一个timer每2s调用一次ClearMemory() #region 内存回收 [DllImport("kernel32. ...
- Windows相关产品密钥
Win7/Win8/Win10系统下Visual Studio 2013各个版本的密钥:Visual Studio Ultimate 2013: BWG7X-J98B3-W34RT-33B3R-JVY ...
- [转]NET实现RSA AES DES 字符串 加密解密以及SHA1 MD5加密
表明来源 https://www.cnblogs.com/shanranlei/p/3630944.html#!comments 本文列举了 数据加密算法(Data Encryption Alg ...
- unity game Developemnt in 24 hours 第1章 untiy
屏幕有3个主要窗口 , Hierarchy.Project.Insepector ,个人理解Project是类定义,Hierarchy是创建类.实例化类,而Insepector是对类的属性进行管理
- 服务器性能测试工具ab
ab指令 ab -n 1000 -c 20 http://127.0.0.1/
- pycharm导入第三方包
- 关于pandas的一些用法
pandas用法之前我总是把他想的无比复杂.其实也是比较简单的,这个东西在做数据统计的时候还是挺好用的. 然后这里列举几个比较好用的几段代码.偏向数据透视类型pivot的,导出方式是直接在IDE 生成 ...
- SSM PUT请求导致的400,415,500问题
最近在尝试用PUT方法的请求时一直产生400,415,500错误,弄了半天(真的是半天),尝试了各种办法,现在终于解决了,为了防止忘记,在此记录下 下面是一步步解决的步骤.(还有许多我略过了)如果只想 ...