当前位置: 代码网 > it编程>前端脚本>Golang > Go多线程中数据不一致问题的解决方案(sync锁机制)

Go多线程中数据不一致问题的解决方案(sync锁机制)

2024年11月03日 Golang 我要评论
go语言中的sync锁在go语言的并发编程中,如何确保多个goroutine安全地访问共享资源是一个关键问题。go语言提供了sync包,其中包含了多种同步原语,用于解决并发编程中的同步问题。本文将详细

go语言中的sync锁

在go语言的并发编程中,如何确保多个goroutine安全地访问共享资源是一个关键问题。go语言提供了sync包,其中包含了多种同步原语,用于解决并发编程中的同步问题。本文将详细介绍sync包中的锁机制,并结合实际案例,帮助读者理解和使用这些锁。

要想解决临界资源安全的问题,很多编程语言的解决方案都是同步。
通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个共享数据,当前goroutine访问完毕, 解锁后,其他的goroutine才 能来访问

我们可以借助于sync包下的锁操作。 synchronization

但是实际上,在go的并发编程中有一句很经典的话:不要以共享内存的方式去通信:锁,而要以通信的方式去共享内存。

共享内存的方式

锁:多个线程拿的是同一个钥匙,go语言不建议使用锁机制来解决。不要以共享内存的方式去通信

而要以通信的方式去共享内存 go语言更建议我们使用 chan(通道) 来解决安全问题。(后面会学)

在go语言中并不鼓励用锁保护共享状态的方式,在不同的goroutine中分享信息(以共享内存的方式去通信)。
而是鼓励通过channei将共享状态或共享状态的变化在各个goroutine之间传递(以通信的方式去共享内存),这样同样能像用锁一样保证在同一的时间只有一个goroutine访问共享状态。

当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。

go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多

一、互斥锁(mutex)

互斥锁(sync.mutex)是最基本的同步机制之一,用于确保同一时间只有一个goroutine能够访问特定的资源。
当一个goroutine持有互斥锁时,其他试图获取该锁的goroutine将会被阻塞,直到锁被释放。

1.1 基本用法

package main

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

// 定义全局变量 票库存为10张
var tickets int = 10

// 定义一个锁  mutex 锁头
var mutex sync.mutex

func main() {
    go saleticket("张三")
    go saleticket("李四")
    go saleticket("王五")
    go saleticket("赵六")

    time.sleep(time.second * 5)
}

// 售票函数
func saleticket(name string) {
    for {
        // 在拿到共享资源之前先上锁
        mutex.lock()
        if tickets > 0 {
            time.sleep(time.millisecond * 1)
            fmt.println(name, "剩余票的数量为:", tickets)
            tickets--
        } else {
            // 票卖完,解锁
            mutex.unlock()
            fmt.println("票已售完")
            break
        }
        // 操作完毕后,解锁
        mutex.unlock()
    }
}

上锁之后,就不会出现问题了

在这里插入图片描述

1.2 使用sync.waitgroup等待一组goroutine完成

sync.waitgroup类型可以用来等待一组goroutine完成。例如:

package main

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

// waitgroup、

var wg sync.waitgroup

func main() {
    // 公司最后关门的人   0
    // wg.add(2) wg.add(2)来告诉waitgroup我们要等待两个goroutine完成  开启几个协程,就add几个
    // wg.done() 我告知我已经结束了  defer wg.done()来在goroutine完成时通知waitgroup
    // 开启几个协程,就add几个
    wg.add(2)

    go test1()
    go test2()

    fmt.println("main等待ing")
    wg.wait() // 等待 wg 归零,wg.wait()来等待所有goroutine完成 代码才会继续向下执行
    fmt.println("end")

    // 理想状态:所有协程执行完毕之后,自动停止。
    //如果每次都强制设置个等待时间。那么协程代码也可能在这个时间内还没跑完,也可能提前就跑完了,所以设置死的等待时间不合理。此时就需要用到了等待组waitgroup
    //time.sleep(1 * time.second)

}
func test1() {
    for i := 0; i < 10; i++ {
        time.sleep(1 * time.second)
        fmt.println("test1--", i)
    }
    wg.done() //这里就将该代码块放在了其他逻辑之后
}
func test2() {
    defer wg.done() // defer wg.done()来在goroutine完成时通知waitgroup  如果不用defer就得把该方法放在其他代码之后
    for i := 0; i < 10; i++ {
        fmt.println("test2--", i)
    }
}

主线程会等待所有协程执行完毕,才继续往下执行代码

在这里插入图片描述

1.3 注意事项

避免死锁:确保在获取锁之后,无论发生什么情况(包括panic),都能够释放锁。可以使用defer语句来确保锁的释放。

减少锁的持有时间:锁的持有时间越长,其他goroutine被阻塞的时间就越长,系统的并发性能就越差。因此,应该尽量减少锁的持有时间,只在必要的代码段中持有锁。

避免嵌套锁:尽量避免在一个锁已经持有的情况下再尝试获取另一个锁,这可能会导致死锁。

避免忘记调用done:如果忘记调用done方法,waitgroup将会永远等待下去,导致程序无法正常结束。

避免负数计数器:调用add方法时,如果传入的参数为负数,或者导致计数器变为负数,将会导致panic。

二、读写锁(rwmutex)

读写锁(sync.rwmutex)允许多个goroutine同时读取资源,但在写入时会阻塞所有其他读和写的goroutine。读写锁可以提高读多写少的场景下的并发性能。

2.1 基本用法

package main

import (
    "fmt"
    "sync"
)

var (
    data map[string]int
    rwmu sync.rwmutex
)

func readdata(key string) int {
    rwmu.rlock()
    defer rwmu.runlock()
    return data[key]
}

func writedata(key string, value int) {
    rwmu.lock()
    defer rwmu.unlock()
    data[key] = value
}

func main() {
    data = make(map[string]int)

    var wg sync.waitgroup

    // 写操作
    for i := 0; i < 10; i++ {
        wg.add(1)
        go func(i int) {
            defer wg.done()
            writedata(fmt.sprintf("key%d", i), i*10)
        }(i)
    }

    // 读操作
    for i := 0; i < 100; i++ {
        wg.add(1)
        go func(i int) {
            defer wg.done()
            value := readdata(fmt.sprintf("key%d", i%10))
            fmt.printf("read: key%d = %d\n", i%10, value)
        }(i)
    }

    wg.wait()
}

在这里插入图片描述

在上述代码中,我们定义了一个全局变量data和一个读写锁rwmu。readdata函数用于读取data中的值,在读取之前先获取读锁,读取完成后释放读锁。

writedata函数用于写入data中的值,在写入之前先获取写锁,写入完成后释放写锁。

在main函数中,我们启动了10个写goroutine和100个读goroutine,分别调用writedata和readdata函数。通过sync.waitgroup等待所有goroutine完成。

2.2 注意事项

避免写锁长时间持有:写锁会阻塞所有其他读和写的goroutine,因此应该尽量减少写锁的持有时间。
读多写少场景:读写锁适用于读多写少的场景,如果写操作非常频繁,读写锁的性能优势可能会消失。
避免嵌套锁:与互斥锁类似,读写锁也应该避免嵌套使用。

三、once(一次执行)

sync.once用于确保某个操作只执行一次,无论有多少个goroutine调用它。这对于单例模式或初始化只执行一次的场景非常有用。

3.1 基本用法

package main

import (
    "fmt"
    "sync"
)

var (
    once    sync.once
    message string
)

func initmessage() {
    message = "hello, world!"
}

func printmessage() {
    once.do(initmessage)
    fmt.println(message)
}

func main() {
    var wg sync.waitgroup

    for i := 0; i < 10; i++ {
        wg.add(1)
        go func() {
            defer wg.done()
            printmessage()
        }()
    }

    wg.wait()
}

在这里插入图片描述

在上述代码中,我们定义了一个全局变量message和一个sync.once类型的变量once。

initmessage函数用于初始化message的值。printmessage函数通过once.do方法确保initmessage只被调用一次,然后打印出message的值。

在main函数中,我们启动了10个goroutine,每个goroutine都调用printmessage函数。通过sync.waitgroup等待所有goroutine完成。

3.2 注意事项

避免重复初始化:sync.once确保某个操作只执行一次,因此它通常用于初始化全局变量或执行其他只需要执行一次的操作。

性能开销:虽然sync.once的性能开销很小,但在高性能要求的场景下,仍然需要注意其使用。

四、总结

本文详细介绍了go语言中sync包中的锁机制,包括互斥锁(sync.mutex)、读写锁(sync.rwmutex)、once(一次执行)和waitgroup(等待组)。

通过实际案例,帮助读者理解和使用这些锁。在并发编程中,正确地使用这些同步原语,可以确保多个goroutine安全地访问共享资源,避免数据竞争和其他并发问题。希望本文能够对大家有所帮助。

以上就是go多线程中数据不一致问题的解决方案的详细内容,更多关于go多线程数据不一致的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com