Go语言 一次写入多次读取用例的全局变量并发使用

axr492tv  于 2022-12-31  发布在  Go
关注(0)|答案(2)|浏览(240)

我想使用一个全局变量。在第一次访问读取它时,我想使用API写入这个变量。对于任何后续的访问读取它,它应该不需要锁。
这是我编写的实现,但是对于这样一个小任务来说,这似乎有些过头了。
1.围棋有没有传统的方法?
1.如果我有多个这样的全局变量,并且我不想把它们放在同一个结构体下,有没有一种方法可以做到这一点,而不需要重复代码?
1.阅读this答案。一种使单个变量的使用原子化的方法是使用“sync/atomic”库。但是,这些函数只适用于整型类型。社区如何处理“string”类型?
请自由建议任何其他无关的变化。
PS:使用同步。一次似乎不对。如果第一次获取失败,程序将永远不会得到'clusterName'。还有,想不出一种方法让其他读者等到同步。一次完成。有什么想法吗?

package main

import (
    "fmt"
    "sync"
    "time"
)

type ClusterName struct {
    sync.RWMutex
    clusterName string
}

// Global variable. Read-many, write once.
var clusterName ClusterName

// Method to avoid locking during reads if 'clusterName' has been filled once.
func GetClusterName() (string, error) {
    // Take read lock to see if 'clusterName' is filled.
    clusterName.RLock()
    if clusterName.clusterName == "" {
        // 'clusterName' is not filled. Release read-lock and call method to fill it with write-lock.
        clusterName.RUnlock()
        return getClusterName()
    }

    defer clusterName.RUnlock()
    return clusterName.clusterName, nil
}

// Method to fetch and fill cluster name. Takes a write-lock.
func getClusterName() (string, error) {
    // Take write-lock.
    clusterName.Lock()
    defer clusterName.Unlock()

    // See if previous writer has already filled this. Just return if already filled.
    if clusterName.clusterName != "" {
        return clusterName.clusterName, nil
    }

    // Only 1 writer will ever reach here.
    var err error
    clusterName.clusterName, err = fetchClusterName()
    if err != nil {
        return "", err
    }

    return clusterName.clusterName, nil
}

func fetchClusterName() (string, error) {
    // API call.
    time.Sleep(time.Second)
    return "test-cluster-name", nil
}

func main() {
    for i := 0; i < 50; i++ {
        fmt.Println(GetClusterName())
    }
}

Playground:https://go.dev/play/p/PV42PMXliRC

lx0bsm1f

lx0bsm1f1#

您需要修改sync.Once实现,如果您查看sync.Once,它将执行以下操作:

if (atomic.LoadUint32(&done) == 0) {
    m.Lock()
    if (done == 0) {
        f()
        atomic.StoreUint32(&done, 1)
    }
    m.Unlock()
}

本质上,它会在检查之前检查操作是否已经执行,然后输入一个锁来等待/执行操作。此外,要使用基本整数以外的数据类型,请将它们 Package 在指针中。因此,您可以执行以下操作:

var m sync.Mutex
var clusterName atomic.Pointer[string]

// Method to avoid locking during reads if 'clusterName' has been filled once.
func GetClusterName() (string, error) {
    // See if 'clusterName' is filled.
    cptr := clusterName.Load()
    if cptr == nil {
        return getClusterName()            
    }

    return *cptr, nil
}

// Method to fetch and fill cluster name. Takes a lock.
func getClusterName() (string, error) {
    // Take lock.
    m.Lock()
    defer m.Unlock()

    // See if previous writer has already filled this. Just return if already filled.
    cptr := clusterName.Load()
    if cptr != nil {
        return *cptr, nil
    }

    // Only 1 successful writer will ever reach here.
    c, err := fetchClusterName()
    if err != nil {
        // Fail, just abort, do not touch clusterName here
        return "", err
    }

    // Only release when succeed
    clusterName.Store(&c)
    return c, nil
}

func fetchClusterName() (string, error) {
    // API call.
    time.Sleep(time.Second)
    return "test-cluster-name", nil
}

函数GetClusterName只能在4个点返回,如果它在锁外检查变量后返回,那么肯定有另一个goroutine在之前写过clusterName,这就保证了这两个线程之间的释放-获取语义;如果它在锁内检查变量后返回,那么和前面一样;如果它在fetchClusterName后返回,那么它的返回是在fetchClusterName之后。那么这个操作就会遇到一个错误,结果是一个空字符串,或者操作成功了,goroutine是clusterName变量的唯一编写者。

txu3uszq

txu3uszq2#

读你的代码,似乎sync.RWMutex不适合这里。从它的工作原理来看,似乎 * 对于任何类型的读操作,都有可能导致写操作 *。

    • 使用sync. Mutex而不是RWMutex**

互斥锁修复了代码中的缺陷,即RLock只能用Lock替换。这是因为在您的代码中,您最初使用RLock读取变量,当您意识到它是空的时,释放读锁并改为写锁。在解锁和使用写锁重新锁定期间的这一非常短的时刻不受任何互斥锁的保护。因此,为什么不对每次读取执行完全锁定和解锁操作呢?这样还可以简化代码。正如您所说,提取有时会失败。这意味着 * 第一次读取操作并不总是导致写入clusterName *。因此,我们必须假设,每个读操作 * 可以导致写 *,这意味着这里不能使用只读锁定。
下面是代码的重新实现

func GetClusterName() (string, error) {
    // Take read lock to see if 'clusterName' is filled.
    clusterName.Lock()
    defer clusterName.Unlock()
    if clusterName.clusterName == "" {
        return getClusterName()
    }

    return clusterName.clusterName, nil
}

func getClusterName() (string, error) {
    // See if previous writer has already filled this. Just return if already filled.
    if clusterName.clusterName != "" {
        return clusterName.clusterName, nil
    }

    // Only 1 writer will ever reach here.
    var err error
    clusterName.clusterName, err = fetchClusterName()
    if err != nil {
        return "", err
    }

    return clusterName.clusterName, nil
}

所以,
1.应该使用sync.Mutex,正如您所说,这里不应该使用sync.Once,因为它只保证一个动作执行一次,而不管操作是否成功,这也是最常规、最简单的方式
1.这也取决于你想要实现什么以及你想要共享多少代码。我建议创建多个结构体和一个接口,其中包含获取、获取和设置值以及锁定和解锁的方法。所有结构体都应该有互斥锁和一个你想要设置的变量。然后你可以创建一个函数来执行锁定、获取、获取、设置和解锁。
1.字符串和整型不同。字符串是一个字符数组(整型),它的存储方式和整型不同。我们通常使用一个单独的互斥体来操作字符串,而不是使用原子互斥体。
这是我的例子,我假设你的每个变量都有不同的获取方式,所以Fetch也需要是抽象的。

type LockSetter interface {
    Lock()
    Unlock()
    Set(value string)
    Get() string
    Fetch() (string, error)
}

type LockSetVariable struct {
    lock       *sync.Mutex
    myVariable string
}

func (l *LockSetVariable) Lock() {
    l.lock.Lock()
}

func (l *LockSetVariable) Unlock() {
    l.lock.Unlock()
}

func (l *LockSetVariable) Set(value string) {
    l.myVariable = value
}

func (l *LockSetVariable) Get() string {
    return l.myVariable
}

func (l *LockSetVariable) Fetch() (string, error) {
    now := time.Now()
    l.myVariable = now.Format("2006")
    return "", nil
}

func GetValue(param LockSetter) (string, error) {
    param.Lock()
    defer param.Unlock()
    
    v := param.Get()
    if v == "" {
        newV, err := param.Fetch()
        if err != nil {
            return "", err
        }
        
        param.Set(newV)
    }
    
    return v, nil
}

相关问题