Loading

【go语言学习】标准库之sync

一、两个问题

1、同步执行问题
package main

import (
	"fmt"
	"time"
)

func main() {
	go fun1()
	go fun2()
	fmt.Println("main函数等待")
	time.Sleep(time.Second * 1)
	fmt.Println("main函数结束")
}

func fun1() {
	fmt.Println("fun1函数执行")
}

func fun2() {
	fmt.Println("fun2函数执行")
}

主线程为了等待所有的子goroutine都运行完毕,不得不在程序中使用time.Sleep() 来睡眠一段时间,等待其他线程充分运行。这种方式耗费时间,显然是不够优雅的。

2、临界资源问题

临界资源: 指并发环境中多个进程/线程/协程共享的资源。并发编程中对临界资源的处理不当, 往往会导致数据不一致的问题。

如果多个goroutine在访问同一个数据资源(临界资源)的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的。

举个例子,我们通过并发来实现火车站售票这个程序。一共有10张票,3个售票口同时出售。

package main

import (
	"fmt"
	"math/rand"
	"time"
)

//全局变量票数
var tickets = 10

func main() {

	//三个goroutine  模拟售票窗口
	go saleTickets("售票口1")
	go saleTickets("售票口2")
	go saleTickets("售票口3")

	//为了保证3个goroutine协程正常工作,先将主线程睡眠5秒
	time.Sleep(5 * time.Second)
}

func saleTickets(name string) {
	//随机数种子
	rand.Seed(time.Now().UnixNano())
	for {
		if tickets > 0 {
			//随机睡眠1~1000ms
			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
			fmt.Println(name, "余票:", tickets)
			tickets--
		} else {
			fmt.Println(name, "售罄,已无票。。")
			break
		}
	}
}

运行结果

售票口3 余票: 10
售票口2 余票: 10
售票口1 余票: 10
售票口3 余票: 7
售票口1 余票: 7
售票口3 余票: 5
售票口2 余票: 4
售票口3 余票: 3
售票口2 余票: 3
售票口1 余票: 3
售票口1 售罄,已无票。。
售票口2 余票: 0
售票口2 售罄,已无票。。
售票口3 余票: -1
售票口3 售罄,已无票。。

在以上的代码中,使用三个并发运行的go协程模拟了三个售票窗口同时售票,而由于全局变量tickets会被三个协程在一段时间内同时访问,因此tickets就是我们所说的“临界资源”。
我们可以发现:

在开始时,三个窗口同时读到信息:tickets=10,从而随机都输出了余票=10
而在结尾时,竟然出现了余票为负数的情况,其产生的原因在于,票数快要卖完时,当售票口1余票1,并且售完这一张票后,在这个时间段内,售票口2已经进入了if tickets > 0满足条件的代码块内,然而售票口1此时将最后一张票售出,tickets 由1变为0售票口2打印出来了不应该出现的结果:余票0,同理售票口3打印了不该出现的结果:余票-1。

多goroutine【多任务】,有共享资源,且多goroutine修改共享资源,出现数据不安全问题【数据错误】,保证数据安全一致,需要goroutine同步

goroutine同步方式

  • channel 【csp模型】
  • sync包提供的方法

二、sync同步等待组WaitGroup

使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务。
等待组的方法:

方法名 功能
(wg *WaitGroup)Add(delta int) 等待组的计数器+1
(wg *WaitGroup)Done() 等待组的计数器-1
(wg *WaitGroup)Wait() 当等待组计数器不等于0时阻塞,直到为0

代码示例:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	go fun1()
	wg.Add(1)
	go fun2()
	fmt.Println("main函数等待")
	wg.Wait()
	fmt.Println("main函数结束")
}

func fun1() {
	fmt.Println("fun1函数执行")
	wg.Done()
}

func fun2() {
	fmt.Println("fun2函数执行")
	wg.Done()
}

运行结果

main函数等待
fun1函数执行
fun2函数执行
main函数结束

三、sync互斥锁Mutex

加锁成功则操作资源,加锁失败则等待直至锁加锁成功——所有的goroutine互斥,一个得到锁其他全部等待。

互斥锁被称为Mutex,它有2个函数,Lock()和Unlock()分别是获取锁和释放锁,如下:

type Mutex
func (m *Mutex) Lock(){}
func (m *Mutex) Unlock(){}

修改上面售票代码,解决临界资源安全问题
示例代码:

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

//全局变量票数
var tickets = 10
var mutex sync.Mutex
var wg sync.WaitGroup

func main() {

	//三个goroutine  模拟售票窗口
	wg.Add(1)
	go saleTickets("售票口1")
	wg.Add(1)
	go saleTickets("售票口2")
	wg.Add(1)
	go saleTickets("售票口3")
	wg.Wait()
}

func saleTickets(name string) {
	//随机数种子
	rand.Seed(time.Now().UnixNano())
	for {
		//上锁
		mutex.Lock()
		if tickets > 0 {
			//随机睡眠1~1000ms
			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
			fmt.Println(name, "余票:", tickets)
			tickets--
		} else {
			mutex.Unlock()
			fmt.Println(name, "售罄,已无票。。")
			break

		}
		//解锁
		mutex.Unlock()
	}
	wg.Done()
}

运行结果

售票口3 余票: 10
售票口3 余票: 9
售票口1 余票: 8
售票口2 余票: 7
售票口3 余票: 6
售票口1 余票: 5
售票口2 余票: 4
售票口3 余票: 3
售票口1 余票: 2
售票口2 余票: 1
售票口1 售罄,已无票。。
售票口2 售罄,已无票。。
售票口3 售罄,已无票。。

四、sync读写锁RWMutex

读写锁要达到的效果是同一时间可以允许多个协程读数据,但只能有且只有1个协程写数据。也就是说,读和写是互斥的,写和写也是互斥的,但读和读并不互斥。
简单来说:

  • (1)可以随便读,多个goroutine同时读。读的时候不能写。
  • (2)写的时候,啥也不能干。不能读也不能写。

读写锁是RWMutex,它有5个函数:

  • Lock()和Unlock()是给写操作用的。
  • RLock()和RUnlock()是给读操作用的。
  • RLocker()能获取读锁,然后传递给其他协程使用。使用较少。
type RWMutex
func (rw *RWMutex) Lock(){}
func (rw *RWMutex) RLock(){}
func (rw *RWMutex) RLocker() Locker{}
func (rw *RWMutex) RUnlock(){}
func (rw *RWMutex) Unlock(){}

举个例子,学生信息录入系统,录入学生信息是写操作,读取学生信息是读操作。可以使用读写锁:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

// Student 学生信息系统
type Student struct {
	// 读写锁
	sync.RWMutex
	// 存储信息 姓名-年龄
	data map[string]int
}

// Add 增加学生信息
func (s *Student) Add(name string, age int) {
	defer wg.Done()
	s.Lock()
	defer s.Unlock()
	if _, ok := s.data[name]; !ok {
		s.data[name] = age
	}
}

// Query 读取学生信息
func (s *Student) Query(name string) {
	defer wg.Done()
	s.RLock()
	defer s.RUnlock()
	if v, ok := s.data[name]; ok {
		fmt.Printf("姓名:%s\t年龄:%d\n", name, v)
	} else {
		fmt.Println("学生信息不存在!")
	}
}

func main() {
	s := &Student{
		data: make(map[string]int),
	}
	wg.Add(4)
	s.Add("jack", 20)
	s.Add("tom", 23)
	s.Add("lili", 18)
	s.Add("lili", 20)

	nameList := []string{"jack", "tom", "lili", "xiaohua"}
	for _, v := range nameList {
		wg.Add(1)
		go s.Query(v)
	}
	wg.Wait()
}

运行结果

学生信息不存在!
姓名:jack      年龄:20
姓名:lili      年龄:18
姓名:tom       年龄:23

五、sync单次执行Once

sync.Once 是 Golang package 中使方法只执行一次的对象实现,作用与 init 函数类似。但也有所不同:

  • init 函数是在文件包首次被加载的时候执行,且只执行一次
  • sync.Once 是在代码运行中需要的时候执行,且只执行一次

当一个函数不希望程序在一开始的时候就被执行的时候,我们可以使用 sync.Once 。
sync.Once是让函数方法只被调用执行一次的实现,其最常应用于单例模式之下,例如初始化系统配置、保持数据库唯一连接等。

代码示例

package main

import (
	"sync"
)

var configs map[string]string

func loadConfig() {
	configs = map[string]string{
		"url":   "https://www.jianshu.com",
		"id":    "cd41c8c3645c",
		"email": "everydawn@jianshu.com",
	}
}

// Config1 被多个goroutine调用时不是并发安全的
// 比如有两个线程都在调用Config1函数,线程A在执行到if configs==nil后
// cpu切换到线程B执行,直到线程B运行完,这时configs已经被实例化,
// 当cpu在切回到线程A继续执行的时候,对configs又执行实例化操作,
// 这时内存中已有configs的两个实例,违背了单例定义。
func Config1(name string) string {
	if configs == nil {
		loadConfig()
	}
	return configs[name]
}

var loadConfigOnce sync.Once

// Config2 是并发安全的
func Config2(name string) string {
	loadConfigOnce.Do(loadConfig)
	return configs[name]
}

func main() {

}
posted @ 2020-11-07 20:09  Every_dawn  阅读(121)  评论(0)    收藏  举报