go并发编程常用的一些方法及解释

go并发编程常用的一些函数,本文中主要涉及了以下几个锁:

sync.Mutex 普通的读锁

sync.RWMutex 读写锁

sync.WaitGroup 记数锁

sync.Map 线程安全的map

sync.Pool 临时存储池


package main

import (
	"fmt"
	"sync"
	"testing"
	"time"

	"golang.org/x/sync/singleflight"
)

type Lock struct {
	count int
}

// 普通读锁
// 允许在共享资源上互斥访问(不能同时访问)
func TestSyncMutex(t *testing.T) {
	l := &Lock{}
	mu := &sync.Mutex{}
	mu.Lock()
	time.Sleep(1 * time.Second)
	_ = l.count
	defer mu.Unlock()
	fmt.Println(time.Now().Format("2006-01-02 15:04:05"))
}

// 读写互斥锁,
// 提供了sync.Mutex的Lock和UnLock方法实现普通的读锁
// 它还允许使用RLock和RUnlock方法进行并发读取
func TestRWMutex(*testing.T) {
	l := &Lock{}
	rwMu := &sync.RWMutex{}
	rwMu.RLock()
	fmt.Println(time.Now().Format("开始执行 2006-01-02 15:04:05"))
	time.Sleep(3 * time.Second)
	fmt.Println(time.Now().Format("执行完 2006-01-02 15:04:05"))
	l.count++
	defer rwMu.RUnlock()
	fmt.Println(time.Now().Format("锁执行完 2006-01-02 15:04:05"))
}

// sync.WaitGroup也是一个经常会用到的同步原语,它的使用场景是在一个goroutine等待一组goroutine执行完成。
// 使用Add(int) 增加计数器
// 使用Done() 或者add(-int) 减少计数器。
// 当计数器等于0时,则Wait()方法会立即返回。否则它将阻塞执行Wait()方法的goroutine直到计数器等于0时为止。
func TestWaitGroup(*testing.T) {
	fmt.Println(time.Now().Format("开始  2006-01-02 15:04:05"))
	wg := &sync.WaitGroup{}
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go func() {
			time.Sleep(5 * time.Second)
			fmt.Println(time.Now().Format("2006-01-02 15:04:05"))
			defer wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println(time.Now().Format("结束  2006-01-02 15:04:05"))

}

// 支持并发的map
// 解决在并发情况下,map写操作不安全的问题,
// 虽然可以加一个大锁来控制整个map的写入,但是效率存在问题
func TestMap(t *testing.T) {
	m := sync.Map{}
	m.Store(1, 1)
	go func(m sync.Map) {
		i := 0
		for i < 10000 {
			m.Store(1, 1)
			i++
		}
	}(m)
	go func(m sync.Map) {
		i := 0
		for i < 10000 {
			m.Store(1, 1)
			i++
		}
	}(m)

	time.Sleep(1 * time.Second)
	fmt.Println(m.Load(1))

}

// Pool 可以作为一个临时存储池,把对象当作一个临时对象存储在池中,然后进行存或取操作,这样对象就可以重用,不用进行内存分配,减轻 GC 压力
// 但是需要注意Pool是一个线程安全但是数据不安全的玩意,因为会被GC且没有提示,那么姑且就认为他是不安全的吧
func TestPool(t *testing.T) {
	// 创建一个 Pool
	pool := sync.Pool{
		// New 函数用处:当我们从 Pool 中用 Get() 获取对象时,如果 Pool 为空,则通过 New 先创建一个
		// 对象放入 Pool 中,相当于给一个 default 值
		New: func() interface{} {
			return "111"
		},
	}
	name := "lilei"
	num := 1
	pool.Put(&name)
	pool.Put(&num)

	fmt.Println(pool.Get())
	fmt.Println(pool.Get())
	fmt.Println(pool.Get())
}

// 这个是用的比较多的了,我们在自己的框架里面,经常用来初始化数据库,日志这种,保证只会执行一次
// 这个例子里面,只会执行一次打印
func TestOnce(t *testing.T) {
	once := &sync.Once{}
	for i := 0; i < 4; i++ {
		i := i
		go func() {
			once.Do(func() {
				fmt.Printf("first %d\n", i)
			})
		}()
	}
}

// 并发的访问同一组资源的时候,只允许一个请求进行,这个请求把结果告诉其它等待者,避免雪崩的现象。
// 比如cache 失效的时候,只允许一个goroutine从数据库中捞数据回种,避免雪崩对数据库的影响。
// 扩展库中提供。
// 但是我不怎么想用这个函数,因为我认为,如果一个缓存,很重要的量很大的,肯定用预热来处理,就完全不存在击穿的问题
// 如果说,你要在读取的时候做判断来直接更新缓存,就认为你可以接受比如在读取缓存时候的编发问题和击穿问题。
// 如果说既不做预热也不担心缓存失效时候的并发读取问题,我认为这是一个很极端的案例
func TestSingleflight(t *testing.T) {
	g := new(singleflight.Group)

	// 第1次调用
	go func() {
		v1, _, shared := g.Do("getData", func() (interface{}, error) {
			ret := getData(1)
			return ret, nil
		})
		fmt.Printf("1st call: v1:%v, shared:%v\n", v1, shared)
	}()

	time.Sleep(2 * time.Second)

	// 第2次调用(第1次调用已开始但未结束)
	v2, _, shared := g.Do("getData", func() (interface{}, error) {
		ret := getData(1)
		return ret, nil
	})
	fmt.Printf("2nd call: v2:%v, shared:%v\n", v2, shared)
}

func getData(id int64) string {
	fmt.Println(time.Now().Format("本次有查询 2006-01-02 15:04:05"))
	time.Sleep(10 * time.Second) // 模拟一个比较耗时的操作
	return "liwenzhou.com"
}
发布于
标记为