• 【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	年龄:%d
    ", 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() {
    
    }
    
  • 相关阅读:
    组合模式
    HashMap,ArrayList扩容
    Maven入门使用(一)
    OutputStreamWriter API 以及源码解读
    java.io.BufferedWriter API 以及源码解读
    java.io.writer API 以及 源码解读
    自定义redis序列化工具
    策略模式
    Spring下redis的配置
    简单工厂模式
  • 原文地址:https://www.cnblogs.com/everydawn/p/13942168.html
Copyright © 2020-2023  润新知