使用go的并发性来解决Hilbert酒店问题

使用,go,并发,解决,hilbert,酒店,问题 · 浏览次数 : 264

小编点评

**代码分析** * **RoomKeysClerk** 函数负责从**welcomeKitCh**中获取**welcomeKits**并将它们保存在**welcomeKits**集合中。 * **BusClerk** 函数负责处理每个大巴的**welcomeKits**,并将其传递给下一个大巴的**nextClerk**。 * ****Relative Independence**** 的核心思想是通过在**RoomKeysClerk**和**BusClerk**之间创建可并行的通道来实现并发性。 **主要概念** * **通道**:用于传递数据 между不同的goroutine。 * **可并行**:指可以并行执行的程序。 * **并行**:指多个程序在相同时间内执行的程序。 * ****Relative Independence**:指通过在通道中传递数据来实现并发性。 **性能分析** * ****Relative Independence**** 的性能优于**并发**的性能。 * ****Relative Independence**** 中,创建通道、传递数据和处理数据之间的延迟较低。 * ****Relative Independence**** 中,每个大巴的**welcomeKits**的生成速度较低。 **代码示例** ```python # RoomKeysClerk def RoomKeysClerk(upTo, keysCh, welcomeKitsCh): count = 0 nextClerkCh = None welcomeKits = [] for roomKey in roomKeysCh: if nextClerkCh == None: nextClerkCh = make(chan int, buffer) go BusClerk(busNumber+1, nextClerkCh, welcomeKitsCh, buffer, delay) if count == keyPosition: kit = hilberthotel.NewWelcomeKit(busNumber, keyPosition, roomKey, delay) welcomeKits.append(kit) keyPosition++ count = 0 continue nextClerkCh <- roomKey count += 1 if nextClerkCh != None: close(nextClerkCh) close(welcomeKitsCh) # Bus Clerk def BusClerk(busNumber int, roomKeysCh, welcomeKitsCh): # ... return welcomeKits # ... ``` **结论** **Relative Independence** 的设计能有效地实现并发性,并提升了性能。但是,在实际应用中,需要考虑各种性能因素,例如创建通道的延迟、传递数据的效率等。

正文

译自:Designing for Concurrency: the Hilbert’s Hotel Problem in Go,本文使用go的并发性来解决Hilbert酒店问题。本文比较有意思的是它对问题的描述很吸引人,在看完文字描述之后,代码实现逻辑也基本顺理成章,当然代码本身的实现也相当优雅。

文章一开始叙述了并发和并行的区别和联系,此处略去该部分。

Hilbert酒店

Hilbert酒店是一个与无限有关的问题。

假设Hilbert是一个酒店的所有者,且该酒店有无限个房间。

一天一个大巴达到了Hilbert的酒店,假设大巴上有无限个旅客想要住在Hilbert的酒店,假设Hilbert的酒店有无限个房间,因此能够容纳无限个旅客。

但有一天有无限辆大巴达到了Hilbert的酒店,每辆大巴上载有无限个旅客,且所有大巴的旅客都要在酒店过夜。

image

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的行为和第一个雇员相同,他知道接收到的第一个钥匙需要交给他负责的大巴的第一个乘客,而第二把钥匙则需要交给下一个雇员,以此类推。

image

在某种程度上,由于我们的程序不能像原来的Hilbert问题那样永远继续下去,因此需要能够停止移交钥匙,并通知所有雇员停止工作。此时需要将雇员准备的欢迎礼包还给Hilbert。最终,所有的雇员都会停止工作,并在Hilbert接收到准备好的欢迎礼包之后就可以通知Hilbert也停止工作。

image

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表示房间钥匙的上限:

  1. 首先它会为钥匙创建channel keysch,并使用keysch作为参数来启动Room Key Clerk的goroutine。
  2. 然后它会创建另一个channel welcomeKitCh(雇员会从该channel中接收钥匙,并在雇员结束工作后发送欢迎礼包),用于接收Welcome Kits(欢迎礼包),并使用keyschwelcomeKitCh作为参数来启动第一个BusClerk(大巴雇员)
  3. 最后,它会通过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
   }
}
  1. 每个Bus Clert对应一个goroutine。BusClerk的第一个参数是其所属的大巴号,welcomeKitCh 用于在处理结束之后发送欢迎礼包(welcomeKits),roomKeysCh用于读取钥匙号
  2. 在初始化内部计数器count之后,使用一个变量keyPosition来保存下一个乘客的钥匙位置,使用channel nextClerkCh通知下一个BusClerk。通过循环读取roomKeysCh来启动整个处理逻辑。
  3. 在接收到第一个钥匙之后,此时nextClerkCh为nil,之后会初始化nextClerkCh并启动下一个BusClerk
  4. 对比countkeyPosition,用于确定此时的钥匙是给本大巴的还是给其他大巴的。当钥匙是给本大巴的,它会创建一个WelcomeKit并将其保存在它的集合中,反之,将钥匙传给下一个BusClerk
  5. 当钥匙接收完之后(即roomKeysCh被关闭),它会关闭用于通知下一个BusClerk的nextClerkCh channel
  6. 最后一个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的任务越重,并行的提升幅度越大

image

每个大巴雇员都有一些核心的工作需要完成,此外还有一些并发编排的工作(即配置goroutine和通过channel发送/接收钥匙)。

并行可以提升核心工作的性能。在Hilbert的例子中,核心工作就是为每个客户准备欢迎礼包(即函数NewWelcomeKit执行的内容)。能够并行执行的核心工作越多,就越能在相同的时间内服务更多的顾客。

为了实现并行处理,我们需要执行一些并发编排工作。与并发编排工作相比,核心工作占主导地位越高,从并行性中获得的好处就越多。在Hilbert的例子中,每个大巴雇员的核心工作是准备欢迎礼包。因此,准备欢迎礼包的工作占比越高,多核硬件上运行并发设计的效率就越高。另一方面,处理的顾客越多,并发编排工作也就越重(由于需要在更多的goroutines之间进行切换和发送/接收钥匙),因此并发的成本也会越高。

可以使用样例代码提供的benchmarks,通过变更顾客数目来对性能进行验证。

附录

也可以使用上面所使用的并发方案的基本设计导出非并发递归方案:

  • 将RoomKeysClerk 转变为一个生成钥匙并将其传递给第一个BusClerk的循环
  • 使用闭包(即一个封装了上下文状态的函数(当前计数器,keyPosition和 nextClerk))来实现BusClerk,
  • 使用Hilbert函数用来触发整个执行逻辑,并收集各个BusClerk构造的welcomeKits

与使用go的并发性来解决Hilbert酒店问题相似的内容:

使用go的并发性来解决Hilbert酒店问题

译自:Designing for Concurrency: the Hilbert’s Hotel Problem in Go,本文使用go的并发性来解决Hilbert酒店问题。本文比较有意思的是它对问题的描述很吸引人,在看完文字描述之后,代码实现逻辑也基本顺理成章,当然代码本身的实现也相当优雅。

在Go中如何实现并发

Go语言的并发机制是其强大和流行的一个关键特性之一。Go使用协程(goroutines)和通道(channels)来实现并发编程,这使得编写高效且可维护的并发代码变得相对容易。下面是Go的并发机制的详细介绍: 协程(Goroutines): 协程是Go中的轻量级线程,由Go运行时管理。与传统线程相比

Go通道机制与应用详解

本文深入探讨了Go语言中通道(Channel)的各个方面,从基础概念到高级应用。文章详细解析了通道的类型、操作方法以及垃圾回收机制,更进一步通过具体代码示例展示了通道在数据流处理、任务调度和状态监控等多个实际应用场景中的作用。本文旨在为读者提供一个全面而深入的理解,以更有效地使用Go中的通道进行并发

go select 使用总结

转载请注明出处: 在Go语言中,select语句用于处理多个通道的并发操作。它类似于switch语句,但是select语句用于通信操作,而不是条件判断。select语句会同时监听多个通道的操作,并选择其中一个可用的通道进行操作。 select语句的语法如下: select { case <-chan

go高并发之路——go语言如何解决并发问题

一、选择GO的原因 作为一个后端开发,日常工作中接触最多的两门语言就是PHP和GO了。无可否认,PHP确实是最好的语言(手动狗头哈哈),写起来真的很舒爽,没有任何心智负担,字符串和整型压根就不用区分,开发速度真的是比GO快很多。现在工作中也还是有一些老项目在使用PHP,但21年之后的新项目基本上就都

golang sync.Map 与使用普通的 map 的区别

使用sync.Map与普通的Go map主要有以下几点区别: 1. 并发安全性 普通map: 在没有外部同步的情况下,不是并发安全的。在多goroutine访问时,如果没有适当的锁或其他同步机制保护,可能会导致数据竞争和未定义行为。 sync.Map: 是并发安全的。它内部实现了必要的同步机制,允许

go项目实现通过配置文件进行配置项统一管理

转载请注明出处: go项目中实现配置项统一管理,实现逻辑:将 配置项整理为一个json的数据结构,并保存到go.conf文件中,然后在go项目启动main方法中加载 go.conf 文件,读取go.conf 文件的json 数据结构,并进行反序列化为go的数据结构,从而在go项目中可以全局使用 go

go 实现ringbuffer以及ringbuffer使用场景介绍

> ringbuffer因为它能复用缓冲空间,通常用于网络通信连接的读写,虽然市面上已经有了go写的诸多版本的ringbuffer组件,虽然诸多版本,实现ringbuffer的核心逻辑却是不变的。但发现其内部提供的方法并不能满足我当下的需求,所以还是自己造一个吧。 源码已经上传到github ```

微信公众号消息加解密

在微信公众号的使用过程中,为了提高信息传输的安全性,可以在服务器配置中将消息加解密模式指定为安全模式。 启用安全模式后,公众号主动调用API的情况并不会受影响,只有被动回复用户的消息时才需要对消息进行加解密。 官方提供了5种开发语言的示例代码,参照官方给的C++示例代码,本文给出go语言的解密实现:

2023-04-26-微信安全模式下消息解析

在微信公众号的使用过程中,为了提高信息传输的安全性,可以在服务器配置中将消息加解密模式指定为安全模式。 启用安全模式后,公众号主动调用API的情况并不会受影响,只有被动回复用户的消息时才需要对消息进行加解密。 官方提供了5种开发语言的示例代码,参照官方给的C++示例代码,本文给出go语言的解密实现: