28

Golang - sync包

 3 years ago
source link: https://studygolang.com/articles/29009
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
QJnUJnV.jpg!web

​ Golang的 sync包提供了大量的线程同步操作的类. 所以满足我们日常使用. 包含锁,和安全的集合. 其中,关于golang 各种锁的实现,请看这个文章,我个人感觉写的不错 juejin.im/entry/5ed32…

sync.Mutex

第一种就是 Mutex 为互斥锁 , 实现跟Java的reentrantlock很像. 基本都是自旋锁,

我们下面有一个场景 , 将变量x累加50000次 我们开启5个goroutine去执行

func main() {
	var x = 0
	for num := 0; num < 4; num++ {
		go func() {
			for i := 0; i < 10000; i++ {
				increase(&x)
			}
		}()
	}
	// 等待子线程退出
	time.Sleep(time.Millisecond * 2000)
	fmt.Println(x)
}
// 这个操作是Java做不了的.只有C,C++和Golang可以
func increase(x *int) {
	*x = *x + 1
}
复制代码

如果输出结果是 50000那就对了, 我们看看结果

23283
复制代码

为什么呢, 因为多线程同时操作一个变量时就会出现这种问题 , 出现读写不一致的问题 , 如何处理呢

我们申明一个锁,让他同步执行 , 这么申明就可以了

var ILock = sync.Mutex{}

func main() {
	var x = 0
	for num := 0; num < 5; num++ {
		go func() {
			for i := 0; i < 10000; i++ {
				increase(&x)
			}
		}()
	}

	// 等待子线程退出
	time.Sleep(time.Millisecond * 2000)
	fmt.Println(x)
}

func increase(x *int) 
	// 1. 先加锁
	ILock.Lock()
	// 执行完方法释放锁. 跟Java的很像,但是不是reentrant
    defer ILock.Unlock()
	*x = *x + 1
}
复制代码

此时执行 发现结果

50000
复制代码

输出正确.

其实还可以这么写. 效率更高

go func() {
    ILock.Lock()
    defer ILock.Unlock()
    for i := 0; i < 10000; i++ {
        increase(&x)
    }
}()
复制代码

利用Chan阻塞实现锁

var (
	lock = make(chan int, 1)
)

func Lock() {
	lock <- 1
}
func Unlock() {
	<-lock
}
复制代码

此时这就是一个最简单的锁.

sync.RWMutex 读写锁

​ 跟Java的一模一样 , 我就不多说了, 读写互斥, 可多读(读锁可以同时存在多个), 但读写不能同时存在, 写互斥

func main() {
	// 读写锁
	mutex := sync.RWMutex{}

	go func() {
		// 读锁
		mutex.RLocker().Lock()
		fmt.Println("读")
		mutex.RLocker().Unlock()
	}()
	go func() {
		mutex.RLocker().Lock()
		fmt.Println("读")
		time.Sleep(time.Millisecond * 5000)
		mutex.RLocker().Unlock()
	}()

	time.Sleep(time.Millisecond * 1000)

	go func() {
		// 写锁 , 就是普通的锁了
		fmt.Println("拿到了")
		mutex.Lock()
		fmt.Println("写")
		mutex.Unlock()
	}()
	time.Sleep(time.Millisecond * 6000)
}
复制代码

输出

读
读
拿到了 // 耗时1ms
写 // 对比前一步耗时 ms
复制代码

sync.WaitGroup

​ 这个类类似于Java的 CountDownLatch 类, 但是比Java的好点. 第一Java的需要初始化告诉他计数器是多少, 所以他只有一个countdown和wait操作.

​ 但是 sync.WaitGroup 却是三个操作, 第一个 down , add(int) , wait 三个操作. 所以他实现更加好.

简单以第一个例子为例子吧, 比如说 , 我们不知道goroutine啥时候退出是吧, 但是有了这玩意就知道了.

func main() {
	var WG = sync.WaitGroup{}

	start := time.Now().UnixNano() / 1e6
	for num := 0; num < 5; num++ {
		WG.Add(1)
		go func(num int) {
			defer WG.Done()
			ran := rand.Int63n(1000)
			fmt.Printf("goroutine-%d sleep : %dms\n", num, ran)
			time.Sleep(time.Millisecond * time.Duration(ran))
		}(num)
	}
	// 等待子线程退出
	WG.Wait()
	fmt.Printf("main waitting : %dms\n", time.Now().UnixNano()/1e6-start)
}
复制代码

输出 : 根据木桶原理, 耗时最长的是937ms, 所以主线程等待了939ms.

goroutine-1 sleep : 410ms
goroutine-0 sleep : 821ms
goroutine-2 sleep : 51ms
goroutine-3 sleep : 937ms
goroutine-4 sleep : 551ms
main waitting : 939ms
复制代码

注意点 :

​ 由于 sync.WaitGroup 也是一个对象Structs, 所以需要指针传递, 不能使用值传递, 注意一下.因为状态值复制了就无效了.

根据封装, 我们需要传递 sync.waitgroup .

func fun(num int, wg *sync.WaitGroup) {
	defer wg.Done()
	ran := rand.Int63n(1000)
	fmt.Printf("goroutine-%d sleep : %dms\n", num, ran)
	time.Sleep(time.Millisecond * time.Duration(ran))
}
复制代码

然后main方法

func main() {
	var WG = &sync.WaitGroup{}
	start := time.Now().UnixNano() / 1e6

	for num := 0; num < 5; num++ {
		WG.Add(1)
		go fun(num, WG)
	}

	// 等待子线程退出
	WG.Wait()
	fmt.Printf("main waitting : %dms\n", time.Now().UnixNano()/1e6-start)
}
复制代码

输出 :

goroutine-4 sleep : 410ms
goroutine-1 sleep : 551ms
goroutine-0 sleep : 821ms
goroutine-2 sleep : 51ms
goroutine-3 sleep : 937ms
main waitting : 939ms
复制代码

sync.Once 一次操作

这个玩意可以让 once.Do() 方法只执行一次. 其实类似于一个flag,一开始为true. 每次执行判断是否为true , 当执行了一次以后改成false. 其实他就是这个原理 , 不过他使用了cas , 保证了线程安全性,

func main() {
	once := sync.Once{}
	one(&once)
	one(&once)
	one(&once)
}

func one(once *sync.Once)  {
	fmt.Println("执行函数")
	once.Do(func() {
		fmt.Println("只会执行了一次")
	})
}
复制代码

输出

执行函数
只会执行了一次
执行函数
执行函数
复制代码

所以他可以只执行一次 , 适合做初始化操作, 或者其他一次性的操作, 不需要多次

sync.Map

提供的线程安全的map , 多线程访问时, 对于crud 操作, 会加锁.

maps := sync.Map{}
// 存
maps.Store("","")
// 删
maps.Delete("")
// 取
maps.Load("")

// 有就不存,返回已经存了的对象和true, 如果没有就返回存的value和false.
maps.LoadOrStore("","")
复制代码

sync.Pool

​ 顾名思义一个池子, 那么我们看看这个池子主要做啥了.

Pool用于存储那些被分配了但是没有被使用,而未来可能会使用的值,以减小垃圾回收的压力。(适合大对象)

​ 同时他提供了一个存储的地方. 减少大量实例化过程 . 但是效率未必要比实例化快奥 . 因为维护一个对象要考虑各种问题, 这就是效率 , 但是实例化啥也不用考虑.

操作很简单.

func main() {

	pool := &sync.Pool{
		New: func() interface{} {
			return "new"
		},
	}
	
	// 首先的放一个 , 由于put操作.
	pool.Put("init")

	go func() {
		// 拿一个
		s := pool.Get().(string)
		// 使用
		fmt.Println(s)
		// 使用完放进去, 所以特别适合大对象. 
		pool.Put(s)
	}()
}
复制代码

sync.Cond

类似与Java的wait和notify 或者说 Condition ,更像后者,但是没有超时的机制

func main() {
	mutex := sync.Mutex{}
	start := sync.NewCond(&mutex)

	for x := 0; x < 10; x++ {
		go func() {
			start.L.Lock()
			defer start.L.Unlock()
			start.Wait()
			fmt.Println("do work")
		}()
	}

	go func() {
		time.Sleep(time.Second * 1)
		start.Broadcast()
	}()

	time.Sleep(time.Second * 2)
}
复制代码

欢迎关注我们的微信公众号,每天学习Go知识

FveQFjN.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK