本文中提到的Promise/Future功能均已实现且开源,可以到github.com/jizhuozhi/g… 查看更多实现细节
Go中的并发编程模型
Go语言以其内置的并发支持而闻名,主要依赖于CSP模型(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。channel是可以让一个goroutine发送数据到另一个goroutine的通信机制:
channel注重的是进程之间的通信和同步,适合在任务需要频繁通信的场景下使用。但在处理单次计算结果的异步操作时存在一些限制:
- 无法传递错误:channel只能在goroutine之间传递单一类型的值,无法同时传递值和错误。
- 不支持多订阅者:Channel不支持多个订阅者消费同一个消息,这限制了广播模式的实现。
例如,当producer调用下游RPC服务,结果可能成功也可能出错,并且存在多个consumer需要处理结果,同时这些consumer可能会动态增加。在这种情况下,使用channel的实现会非常复杂且不直观。
Promise/Future模式
Promise/Future模式是另一种处理并发和异步编程的模式,由两个主要组件组成:
- Promise:表示一个操作的承诺(或契约),它将来会提供一个值或一个错误。
- Future:表示一个可能还未完成的异步操作结果,可以通过它获取最终的值或错误。
结构定义
type Promise[T any] struct
type Future[T any] struct
func NewPromise[T any]() *Promise[T]
func (p *Promise[T]) Set(val T, err error)
func (p *Promise[T]) Future() *Future[T]
func (p *Promise[T]) Get() (T, error)
在Promise/Future模式中,Promise与Future共享结果状态,Promise会分配给Producer,而Future会分配给多个Consumer。
当Producer正在执行异步操作时,Future对象会阻塞等待,直到Producer通过Promise将结果状态设置结果或错误。一旦结果状态设置了结果或错误,它会通知所有等待的Future对象,从而使它们能够获取结果或错误。在此之后,任何对该Future对象的访问都可以立即获取结果。
这种模式的主要思想是将异步操作的执行和结果处理分离,使得代码更加简洁和易于维护。Promise负责执行操作并设置结果状态,Future则提供了一个访问操作结果状态的接口。
代码示例
p := NewPromise[int]()
f := p.Future()
// producer goroutine
go func() {
fmt.Println("producer goroutine", val, err)
p.Set(1, nil)
}()
// A consumer goroutine
go func() {
val, err := f.Get()
fmt.Println("A consumer goroutine", val, err)
}()
// B consumer goroutine
go func() {
val, err := f.Get()
fmt.Println("B consumer goroutine", val, err)
}()
// C consumer goroutine
go func() {
val, err := f.Get()
fmt.Println("C consumer goroutine", val, err)
}()
预期的输出结果
C consumer goroutine 1 <nil>
B consumer goroutine 1 <nil>
A consumer goroutine 1 <nil>
惰性求值(Lazy)
在有些实现中,Future是支持惰性求值的,即计算只在需要时才开始执行,这是channel无法实现的。
函数定义
func Lazy[T any](func() (T, error))
这种情况下每个Consumer依旧会分配到Future来获取结果,但是没有Producer(Promise)的参与
只有在首次需要该结果时才会进行计算,计算的任务由首次获取结果的Consumer来执行,在计算结束后设置结果状态并通知其他Consumer
代码示例
var a int32
f := Lazy[int](func() (int, error) {
val := atomic.AddInt32(&a, 1)
return val, nil
})
// A consumer goroutine
go func() {
val, err := f.Get()
fmt.Println("A consumer goroutine", val, err)
}()
// B consumer goroutine
go func() {
val, err := f.Get()
fmt.Println("B consumer goroutine", val, err)
}()
// C consumer goroutine
go func() {
val, err := f.Get()
fmt.Println("C consumer goroutine", val, err)
}()
预期的输出结果,所有Consumer收到结果都是1,而不是随着调用顺序依次递增
C consumer goroutine 1 <nil>
B consumer goroutine 1 <nil>
A consumer goroutine 1 <nil>
结尾
通过Promise/Future实现,我们克服了Go channel在某些场景下的限制,实现了更灵活和强大的并发编程模型。这种实现不仅简化了代码,还提供了更多的功能,例如同时传递值和错误、多订阅者支持以及惰性求值。
Promise/Future的功能远不止于此,Then/AnyOf/AllOf也是Promise/Future中常见且重要的扩展,在github.com/jizhuozhi/g… 中都通过底层并发原语提供了高性能的实现