Multithreading 允许一个线程更新值(刷新身份验证令牌),其他线程等待并跳过关键部分

Multithreading 允许一个线程更新值(刷新身份验证令牌),其他线程等待并跳过关键部分,multithreading,go,mutex,condition-variable,Multithreading,Go,Mutex,Condition Variable,我正在从多个线程向多个目标发送HTTP请求。这些请求需要授权。我有一个单一的授权服务器,从那里我可以获得授权令牌 因此,当授权令牌到期时(即HTTP响应状态401),我希望其中一个线程在其他线程等待时去刷新令牌。刷新令牌后,所有线程应继续发送请求,而无需再次尝试刷新令牌 以下是我的实现: type httpSenderAgent struct { // irrelevant members omited ... client *http.Client tokenS

我正在从多个线程向多个目标发送HTTP请求。这些请求需要授权。我有一个单一的授权服务器,从那里我可以获得授权令牌

因此,当授权令牌到期时(即HTTP响应状态401),我希望其中一个线程在其他线程等待时去刷新令牌。刷新令牌后,所有线程应继续发送请求,而无需再次尝试刷新令牌

以下是我的实现:

type httpSenderAgent struct {
    // irrelevant members omited
    ...
    client *http.Client
    tokenState int // (valid = 1, expired = 0)
    token string
    cond sync.Cond
}
但是,问题出现在以下情况:

假设第一个线程得到401响应,它使
tokenState
过期,启动goroutine获取令牌并等待令牌有效,同时,另一个线程得到401并调用
refreshAuthToken()
。此时,令牌获取线程进入
锁定节2
。因此,第二个线程无法进入
lock section 1
,它等待互斥锁解锁。当令牌获取线程将
tokenState
更新为valid并解锁互斥锁时,第二个线程将锁定互斥锁并发现令牌有效。因此,它将重复整个令牌获取过程

我已经用相同的标题检查了SO问题。但是,答案并不包括我的用例


那么,我如何更改它以仅获取一次身份验证令牌,直到再次过期?我应该使用什么样的同步原语来实现这一点?

我不确定您如何设想在这里使用
sync.Cond
,但听起来您正在寻找一些可以处理的方法,您是否有办法知道它何时过期,或者便宜地检查它是否过期?这将大大提高解决方案的复杂性。@jimbsingleflight似乎很有趣,我将尝试一下。但是,同步
forget()
函数调用可能会导致相同的情况。除了
sync.Cond
在这种情况下,您会建议使用哪种同步原语?@Adrian可以利用到期时间。但是,情况可能并非总是如此(当令牌被加密时)。我在处理MS的Graph API时使用了类似的技术。身份验证令牌为一小时(在身份验证响应中指示)。客户端api获取受互斥体保护的令牌来执行其工作。服务go例程在到期前5分钟执行重新身份验证-完成后在新令牌中交换互斥。客户端api在这些重新授权窗口期间不会出现服务中断。我不确定您如何设想一个
sync.Cond
在这里工作,但听起来好像您正在寻找一些可以处理的方法。您是否有办法知道它何时过期,或者便宜地检查它是否过期?这将大大提高解决方案的复杂性。@jimbsingleflight似乎很有趣,我将尝试一下。但是,同步
forget()
函数调用可能会导致相同的情况。除了
sync.Cond
在这种情况下,您会建议使用哪种同步原语?@Adrian可以利用到期时间。但是,情况可能并非总是如此(当令牌被加密时)。我在处理MS的Graph API时使用了类似的技术。身份验证令牌为一小时(在身份验证响应中指示)。客户端api获取受互斥体保护的令牌来执行其工作。服务go例程在到期前5分钟执行重新身份验证-完成后在新令牌中交换互斥。在这些重新授权窗口期间,客户端api未发现服务中断。
func (a *httpSenderAgent) send(url string, body interface{}, retrycount int) error {

    if retrycount >= MAX_AUTH_RETRY {
        return errors.New("Max retry exceeded")
    }

    buf := &bytes.Buffer{}
    json.NewEncoder(buf).Encode(body)
    req, err := http.NewRequest("POST", url, buf)
    if err != nil {
        return err
    }

    req.Header.Add("Content-Type", "application/json; charset=utf-8")
    req.Header.Set("Authorization", "Bearer "+a.token)

    res, err := a.client.Do(req)
    if err != nil {
        return err
    }

    defer res.Body.Close()

    if res.StatusCode == http.StatusUnauthorized {
        a.refreshAuthToken()
        return a.send(url , body, retrycount + 1)
    }

    if res.StatusCode < 200 || res.StatusCode > 299 {
        return errors.New(http.StatusText(res.StatusCode))
    }

    return nil
}
func (a *httpSenderAgent) refreshAuthToken() {
    // lock section 1
    a.cond.L.Lock()
    if a.tokenState == valid {
        a.tokenState = expired
        go func() {
            // getAuthToken() is a http call to auth server
            tkn := a.getAuthToken()


            // lock section 2
            a.cond.L.Lock()
            a.token = tkn
            a.tokenState = valid
            a.cond.L.Unlock()

            a.cond.Broadcast()
        }()
    }

    for a.tokenState != valid {
        a.cond.Wait()
    }
    a.cond.L.Unlock()
}