【问题标题】:context without channels in the same thread of execution在同一执行线程中没有通道的上下文
【发布时间】:2020-06-06 11:44:55
【问题描述】:

如果通过上下文语义在同一个执行线程中计算需要花费大量时间,我无法弄清楚如何取消任务?

我用这个例子作为参考点 https://golang.org/src/context/context_test.go

这里的目标调用一个doWork,如果doWork计算时间太长,GetValueWithDeadline应该在超时后返回0,或者如果调用者调用cancel取消等待,(这里主要是调用者)或者返回的值在给一个时间窗口。

同样的场景可以用不同的方式完成。 (单独的 goroutine 睡眠、唤醒检查值等、互斥锁上的条件等)但我真的很想了解使用上下文的正确方法。

通道语义我理解但这里无法达到预期的效果,默认情况下 在默认情况下调用 doWork 故障并休眠。

package main

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

type Server struct {
    lock sync.Mutex
}

func NewServer() *Server {
    s := new(Server)
    return s
}

func (s *Server) doWork() int {
    s.lock.Lock()
    defer s.lock.Unlock()
    r := rand.Intn(100)
    log.Printf("Going to nap for %d", r)
    time.Sleep(time.Duration(r) * time.Millisecond)
    return r
}

// I take an example from here and it very unclear where is do work executed
// https://golang.org/src/context/context_test.go

func (s *Server)  GetValueWithDeadline(ctx context.Context) int {
    val := 0
    select {
        case <- time.After(150 * time.Millisecond):
                fmt.Println("overslept")
                return 0
        case <- ctx.Done():
                fmt.Println(ctx.Err())
                return 0
        default:
                val = s.doWork()
    }

    return all
}

func main() {

    rand.Seed(time.Now().UTC().UnixNano())
    s := NewServer()
    for i :=0; i < 10; i++ {
        d := time.Now().Add(50 * time.Millisecond)
        ctx, cancel := context.WithDeadline(context.Background(), d)
        log.Print(s.GetValueWithDeadline(ctx))
        cancel()
    }
}

谢谢

【问题讨论】:

    标签: go concurrency


    【解决方案1】:

    您的方法存在多个问题。

    上下文解决了什么问题

    首先,在 Go 中发明上下文的主要原因是它们允许统一一种方法来取消一组任务。

    为了用一个简单的例子来解释这个概念,考虑一个客户端对某个服务器的请求;为了进一步简化,让它成为一个 HTTP 请求。 客户端连接到服务器,发送一些数据告诉服务器如何完成请求,然后等待服务器响应。
    现在让我们假设该请求需要在服务器上进行精细且耗时的处理——例如,假设它需要对多个远程数据库引擎执行多个复杂查询,对外部服务执行多个 HTTP 请求,然后处理获取的结果以实际生成客户想要的数据。

    因此客户端开始其请求,而服务器继续处理所有这些请求。
    为了隐藏服务器为满足请求而必须执行的单个任务的延迟,它在单独的 goroutine 中运行它们。 一旦每个 goroutine 完成分配的任务,它就会将其结果(和/或错误)传达回处理客户端请求的 goroutine,依此类推。

    现在假设客户端由于任何原因未能等待对其请求的响应——网络中断、客户端软件中的显式超时、用户终止发起请求的应用程序等——有很多可能性.

    如您所见,服务器继续花费资源来完成在逻辑上绑定到现已失效的请求的任务几乎没有意义:反正没有人听到结果。
    因此,一旦我们知道请求不会完成,执行这些任务是有意义的,这就是上下文发挥作用的地方:您可以将每个传入请求与单个上下文相关联,然后将其本身传递给生成的任何 goroutine 进行携带出完成请求所需完成的单个任务,或从中派生另一个请求并传递它。 然后,一旦您取消“根”请求,该信号就会在从根请求派生的整个请求树中传播。
    现在,每个被赋予上下文的 goroutine 可能会“监听”它,以便在发送取消信号时得到通知,并且一旦 goroutine 注意到它可能会放弃它正在忙于做的任何事情并退出。

    就实际的context.Context 类型而言,该信号被称为“完成”——就像“我们已经完成了与上下文相关的任何事情”——这就是为什么想要知道它的 goroutine 应该停止执行它的原因work 侦听上下文方法返回的特殊通道,称为 Done

    回到你的例子

    要使其正常工作,您可以执行以下操作:

    func (s *Server) doWork(ctx context.Context) int {
        s.lock.Lock()
        defer s.lock.Unlock()
    
        r := rand.Intn(100)
        log.Printf("Going to nap for %d", r)
        select {
        case <- time.After(time.Duration(r) * time.Millisecond):
            return r
        case <- ctx.Done():
            return -1
        }
    }
    
    func (s *Server)  GetValueWithTimeout(ctx context.Context, maxTime time.Duration) int {
        d := time.Now().Add(maxTime)
        ctx, cancel := context.WithDeadline(ctx, d)
        defer cancel()
    
        return s.doWork(ctx)
    }
    
    func main() {
        const maxTime = 50 * time.Millisecond
    
        rand.Seed(time.Now().UTC().UnixNano())
    
        s := NewServer()
        for i :=0; i < 10; i++ {
            v := s.GetValueWithTimeout(context.Background(), maxTime)
            log.Print(v)
        }
    }
    

    (Playground).

    那么这里发生了什么?

    GetValueWithTimeout 方法接受 doWork 方法产生值所花费的最长时间,计算截止日期,派生一个上下文,一旦截止日期从传递给该方法的上下文过去并调用 @987654327 @ 与新的上下文对象。

    doWork 方法将其自己的计时器设置为在随机时间间隔后关闭,然后 同时侦听上下文和计时器。
    这一点是关键点:执行某些本应可取消的工作单元的代码必须检查上下文以使其主动自动“完成”。
    因此,在我们的玩具示例中,要么doWork 自己的计时器首先触发,要么首先达到生成上下文的截止日期;无论先发生什么,都会使select 语句解除阻塞并继续。

    请注意,如果您的“工作”代码涉及更多——它实际上会做某事而不是睡觉——你很可能需要定期检查上下文的状态,通常是在执行该工作的个别部分之后。

    【讨论】:

    • 非常感谢您以非常尖锐的解释回答。对于我,这说得通。我还评估了 doWork 返回频道的第二个选项。即缓冲通道,当它完成时关闭通道。在我的情况下,缩小到小例子,但如果 doWork 等待互斥体 API 不返回任何结果,那么它会尝试获取价值,如果你不能不阻塞。 Go 具有非常丰富的并发语义;)
    猜你喜欢
    • 1970-01-01
    • 2013-05-12
    • 1970-01-01
    • 2021-05-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-28
    • 1970-01-01
    相关资源
    最近更新 更多