073-取消并发请求

在服务器开发领域,有很多未知的原因可能导致用户的请求失败。比如:你的服务器并发请求了多个下游服务,并在等待它们返回所有结果。但是很不幸,其中一个服务返回了错误,此时其它所有其它请求我们也就没有必要再等待下去了,而是直接返回并告诉用户本次请求失败。

上面的场景在服务器开发领域非常常见,处理起来也比较困难。无论你是写 C++ 还是 Java,或是 Nodejs,都不那么容易处理。

多说不益,我们使用一个简单的服务器模拟请求下游,并人为取消请求。

1. 请求模拟

目标:假设我们的服务同时发起三次 HTTP GET 请求下游,得到结果,并打印到屏幕。

1.1 模拟的 Get 请求函数

下面是我们自己编写的一个模拟发送 HTTP GET 请求的函数。当然它并不会真的请求某个真实的下游。这个函数可以模拟网络拥塞的场景,比如长时间不返回结果。为了能让你看到效果,我特意将请求耗时随机设置在 [2,7] 秒之间。

关于 time.After 函数就不用我多说了,前面已经讲过了,当然你也可以自行查阅文档。

func Get() string {
    duration := rand.Intn(5) + 2
    tick := time.After(time.Duration(duration) * time.Second)
    select {
    case <-tick:
        return fmt.Sprintf("get page %d", duration)
    }
}

1.2 模拟请求下游服务

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func Get() string {
    duration := rand.Intn(5) + 2
    tick := time.After(time.Duration(duration) * time.Second)
    select {
    case <-tick:
        return fmt.Sprintf("get page %d", duration)
    }
}

func main() {
    rand.Seed(time.Now().Unix())
    var wg sync.WaitGroup
    wg.Add(3)
    go func() {
        fmt.Println(Get())
        wg.Done()
    }()
    go func() {
        fmt.Println(Get())
        wg.Done()
    }()
    go func() {
        fmt.Println(Get())
        wg.Done()
    }()
    wg.Wait()
}

下面是运行效果。


这里写图片描述
图1 并发 Get 请求

2. cancel 请求

在上一篇《火箭发射游戏》里,你已经学会了使用 channel 来取消火箭发射。其实在这里,你仍然可以借鉴这个思路。

具体的,我们可以通过创建一个 channel 来控制。修改的代码如下:

package main

import (
    "fmt"
    "math/rand"
    "os"
    "sync"
    "time"
)

func Get(done <-chan struct{}) string {
    duration := rand.Intn(5) + 2
    tick := time.After(time.Duration(duration) * time.Second)
    // 使用 select 来监听 channel
    select {
    case <-tick:
        return fmt.Sprintf("get page %d", duration)
    case <-done:
        return fmt.Sprintf("cancel %d", duration)
    }
}

func main() {
    rand.Seed(time.Now().Unix())
    // 创建一个 channel
    done := make(chan struct{})
    var wg sync.WaitGroup
    go func() {
        os.Stdin.Read(make([]byte, 1))
        // 因为我们发起了 3 次 Get 请求,所以要发送 3 次数据到 done channel
        for n := 0; n < 3; n++ {
            done <- struct{}{}
        }
    }()
    wg.Add(3)
    go func() {
        fmt.Println(Get(done))
        wg.Done()
    }()
    go func() {
        fmt.Println(Get(done))
        wg.Done()
    }()
    go func() {
        fmt.Println(Get(done))
        wg.Done()
    }()
    wg.Wait()
}

这一次,我们再次运行,并在请求结束前,按下你的 ENTER 键来结束请求。


这里写图片描述
图2 cancel 请求

在图 2 中,第一次我没有打断请求,程序自然结束。第二次,程序一启动我就按下 ENTER 打断了请求。第三次,程序第一个请求结束后我按下 ENTER 键,打断了后面的两个 GET 请求。

那么,我们的任务就到此为止了吗?显然不会。上面的实现太不优雅了,假设我们有 100 个并发请求出去了?难道就得把循环向 channel 发送 100 次数据吗?

有没有一种方向,可以广播的通知所有的 goroutine? 答案是有的。我们利用 channel 的另一个特性:如果从一个已经关闭的 channel 读取数据,程序会立即返回而不阻塞

这样的就简单了,我们只要稍微修改一下那个看起来不太舒服的 for 循环就行:

// 不要使用这种方式了
for n := 0; n < 3; n++ {
    done <- struct{}{}
}

// 改成下面这样
close(done)

稍作修改,你的程序就能达到和图 2 一样的效果。

3. 总结

  • 掌握 channel 的关闭后的特性
  • 掌握取消并发 goroutine 的方法

在 Golang 里,取消请求太常见了,以致于 Golang 为其实现了一些标准库来完成这些事情。下一讲,我们来看看它到底是什么。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值