Promise/Future: Go异步计算更好的选择

783 阅读4分钟

本文中提到的Promise/Future功能均已实现且开源,可以到github.com/jizhuozhi/g… 查看更多实现细节

Go中的并发编程模型

Go语言以其内置的并发支持而闻名,主要依赖于CSP模型(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。channel是可以让一个goroutine发送数据到另一个goroutine的通信机制:

channel_single.drawio.png

channel注重的是进程之间的通信和同步,适合在任务需要频繁通信的场景下使用。但在处理单次计算结果的异步操作时存在一些限制:

  • 无法传递错误:channel只能在goroutine之间传递单一类型的值,无法同时传递值和错误。

channel_error.drawio.png

  • 不支持多订阅者:Channel不支持多个订阅者消费同一个消息,这限制了广播模式的实现。

channel.drawio.png

例如,当producer调用下游RPC服务,结果可能成功也可能出错,并且存在多个consumer需要处理结果,同时这些consumer可能会动态增加。在这种情况下,使用channel的实现会非常复杂且不直观。

Promise/Future模式

en.wikipedia.org/wiki/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。

promise_future.drawio.png

当Producer正在执行异步操作时,Future对象会阻塞等待,直到Producer通过Promise将结果状态设置结果或错误。一旦结果状态设置了结果或错误,它会通知所有等待的Future对象,从而使它们能够获取结果或错误。在此之后,任何对该Future对象的访问都可以立即获取结果。

promise_future_set.drawio.png

这种模式的主要思想是将异步操作的执行和结果处理分离,使得代码更加简洁和易于维护。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)的参与

promise_future_lazy.drawio.png

只有在首次需要该结果时才会进行计算,计算的任务由首次获取结果的Consumer来执行,在计算结束后设置结果状态并通知其他Consumer

promise_future_lazy_set.drawio.png

代码示例

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… 中都通过底层并发原语提供了高性能的实现