【Go语言学习】——并发编程

并发编程


参考博客

go语言中的并发是通过用户态的线程实现的,相比起java需要自己维护线程池并进行调度和上下文切换,go主需要利用goroutine去管理并发,并且相比内核态的线程更加轻量化,可以创建成千上万的goroutine线程工作,由go中的runtime调度完成的,利用channel可以在goroutine之间进行通信

在并发编程中常常用到闭包,但是注意如果希望调用的外部参数就是调用时的值,需要作为参数传入而不是直接利用闭包机制调用,因为可能等进程调用时外部参数已经发生改变了。

  • goroutine

    在调用函数前面加上go关键字,就能为该函数创建一个goroutine

    goroutine对应函数结束那么该任务也结束了,而main函数结束了,则所有由它创建的goroutine都结束了

    package main
    
    import "fmt"
    
    // func hello(i int) {
    // 	fmt.Println("hello", i)
    // }
    
    // 程序启动后会创建一个主goroutine执行main函数
    func main() {
    	// 开启一个单独的goroutine执行hello函数(任务)
    	for i := 0; i < 100; i++ {
    		// go hello(i)
    		// 如果不传入参数i的话,则形成闭包,每次内部匿名函数都需要向外寻找参数i,
    		// 而由于启动goroutine需要耗费时间,所以当匿名函数执行打印时外部函数的i已经改变了
    		// go func() {
    		// 	fmt.Println(i)
    		// }()
    
    		go func(i int) {
    			fmt.Println(i)
    		}(i)
    	}
    	fmt.Println("main")
    
    	// main函数结束,则由main函数启动的goroutine也都结束了
    }
    
    
  • waitGroup

    sync.Waitgroup用于实现同步,每次开启新的goroutine时需要在计数器处加一,而并发执行的函数完成后在计数器处减一,最后通过利用Wait()函数确保计数器为0也就是所有goroutine都执行完成后才退出。本质也是一个值类型的结构体,给函数传参数的时候要传指针

    package main
    
    import (
    	"fmt"
    	"math/rand"
    	"sync"
    	"time"
    )
    
    // 生产随机数
    // func f() {
    // 	// 生成随机种子让每次随机
    // 	rand.Seed(time.Now().UnixNano())
    // 	for i := 0; i < 5; i++ {
    // 		r1 := rand.Int()    //int
    // 		r2 := rand.Intn(10) //生成小于10的随机数
    // 		fmt.Println(r1, r2)
    // 	}
    // }
    
    func f1(i int) {
    	// 该函数结束后计数器减一
    	defer wg.Done()
    	time.Sleep(time.Duration(rand.Intn(300)) * time.Millisecond)
    	fmt.Println(i)
    }
    
    var wg sync.WaitGroup
    
    func main() {
    	// f()
    	for i := 0; i < 10; i++ {
    		// 函数执行前计数器加一
    		wg.Add(1)
    		go f1(i)
    	}
    	// 等待wg的计数器减为0
    	wg.Wait()
    }
    
  • goroutine与线程

    • 栈空间

      ==goroutine的栈空间是按需增大或减小的。==OS中线程具有固定的栈内存(通常为2MB),而goroutine的栈内存大小是不固定的(2KB~1GB),通常都很小(初始为2KB),所以能创建很多的goroutine

    • 调度模型

      参考博客

      GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

      G就是代表goroutine对象,存储了自身信息以及绑定的P的信息

      P管理着goroutine队列,存储goroutine的上下文环境(OS线程的上下文环境由OS保存),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。

      M是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的

    • 操作系统线程和goroutine的关系

      1. 一个操作系统线程对应用户态多个goroutine。

      2. go程序可以同时使用多个操作系统线程。

      3. goroutine和OS线程是多对多的关系,即m:n(m个goroutine分配给n个操作系统线程执行)

      Go运行时可以使用GOMAXPROCS确定使用多少个OS线程执行GO代码,默认是CPU核心数,可以通过runtime.GOMAXPROCS()自定义占用的CPU数

      package main
      
      import (
      	"fmt"
      	"runtime"
      	"sync"
      )
      
      var wg sync.WaitGroup
      
      func a() {
      	defer wg.Done()
      	for i := 0; i < 10; i++ {
      		fmt.Printf("A:%d\n", i)
      	}
      }
      func b() {
      	defer wg.Done()
      	for i := 0; i < 10; i++ {
      		fmt.Printf("B:%d\n", i)
      	}
      }
      
      func main() {
      	// 默认值是机器上的CPU核心数
      	// runtime.GOMAXPROCS(1) 串行输出
      	runtime.GOMAXPROCS(2) //并行输出
      	wg.Add(2)
      	go a()
      	go b()
      	wg.Wait()
      }
      
      
  • channel(通道)

    Go语言的并发模型是CSP(Communicating Sequential Process),提倡通过通信共享内存而不是通过共享内存实现通信。

    channel是一种特殊的引用类型,需要使用make分配内存初始化才能使用,并需要定义其中存储的类型,可以让一个goroutine发送特定值到另一个goroutine的通信机制,遵循先入先出的规则。

    channel的操作符号为->,具有三种操作,对于没有缓存的channel必须要接受和发送操作并发执行,否则会导致死锁。

    发送:ch<-10

    接受:x:=<-ch

    关闭:close(ch)

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    var a chan int
    var b chan int // 需要指定通道中的类型
    var wg sync.WaitGroup
    
    func noBufChannel() {
    	a = make(chan int) //不带缓冲区通道的初始化,需要有任务线程接受时才能够往里面放,不然直接放的话会引起死锁
    	wg.Add(1)
    	// 由于通道没有缓存所以需要接受值和发送值并发执行
    	go func() {
    		defer wg.Done()
    		x := <-a
    		fmt.Println("后台goroutine从通道a中取到了", x)
    	}()
    	a <- 10
    	fmt.Println("10发送到通道a中了... ")
    	wg.Wait()
    	// 关闭通道
    	close(a)
    }
    func BufChannel() {
    	// 通道不宜设置得太大,如果太大则存储指针
    	b = make(chan int, 16) //带缓冲区的通道的初始化,线程可以往缓冲区中预存通信的值,然后其他线程来接收这个值
    	b <- 10
    	fmt.Println("10发送到通道b中了... ")
    	x := <-b
    	fmt.Println("后台goroutine从通道b中取到了", x)
    	// 关闭通道
    	close(b)
    }
    
    func main() {
    	noBufChannel()
    	BufChannel()
    }
    
    
  • channel练习

    利用循环对通道实现批量存值与取值:先启动一个goroutine生成100个数放入通道1,再启动另外一个goroutine把这100个数平方放入通道二,最后取出来。

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    // channel练习
    // 1.启动一个goroutine,生成100个数发送到ch1
    // 2.启动一个goroutine,从ch1中取值,计算其平方放入ch2中
    // 3。在main中,从ch2中取值打印
    
    var wg sync.WaitGroup
    
    func f1(ch1 chan int) {
    	defer wg.Done()
    	for i := 0; i < 100; i++ {
    		ch1 <- i
    	}
    	// 这里关闭ch1是为了f2中读取完ch1的数据后不会阻塞而导致死锁,并且可以返回false
    	close(ch1)
    }
    
    func f2(ch1, ch2 chan int) {
    	defer wg.Done()
    	for {
    		// 关闭后的ch1中的所有数据读取完成后再去请求读取就会返回false,如果没有关闭就会一直阻塞导致死锁
    		x, ok := <-ch1
    		if !ok {
    			break
    		}
    		ch2 <- x * x
    	}
    	close(ch2)
    }
    
    func main() {
    	// a的缓存可以不用设置满,因为会一边存一边取
    	a := make(chan int, 100)
    	// b的缓存必须设置满,这样才能存储所有的数,并且后续从b中读取数据时才能够读完
    	b := make(chan int, 100)
    	wg.Add(2)
    	go f1(a)
    	go f2(a, b)
    	wg.Wait()
    	for ret := range b {
    		fmt.Println(ret)
    	}
    
    }
    
  • close

    close()并不是一个必需的操作,channel是一种类型,程序结束后会自动回收。关闭后的通道具有以下特点:

    1. 对一个关闭的通道再发送值就会导致panic。
    2. 对一个关闭的通道进行接收会一直获取值直到通道为空。
    3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
    4. 关闭一个已经关闭的通道会导致panic。
    package main
    
    import "fmt"
    
    // 关闭通道
    
    func main() {
    	ch1 := make(chan int, 2)
    	ch1 <- 10
    	ch1 <- 20
    	// 使用for range对没有关闭的通道循环取值会产生死锁
    	// for ret := range ch1 {
    	// 	println(ret)
    	// }
    	// close(ch1)
    	// 通道关闭了不能够写但是仍然可以取值
    	// 使用for range可以循环取出关闭后的通道里的所有值
    	for ret := range ch1 {
    		println(ret)
    	}
    	<-ch1
    	<-ch1
    	// 通道关闭后向空通道仍然能取值,取出的值为对应类型的零值,并且断言为false
    	x, ok := <-ch1
    	fmt.Println(x, ok)
    }
    
  • 单向通道

    指只能用于发送或者接受的通道,多用在函数的参数里,确保在该函数里只能执行对应操作,声明单向通道只需要在关键字旁标注上对应方向的符号<-

    chan<-表示通道只能接受值,不能发送值

    <-chan表示通道只能发送值,不能接受值

    func counter(out chan<- int) {
    	for i := 0; i < 100; i++ {
    		out <- i
    	}
    	close(out)
    }
    
    func squarer(out chan<- int, in <-chan int) {
    	for i := range in {
    		out <- i * i
    	}
    	close(out)
    }
    func printer(in <-chan int) {
    	for i := range in {
    		fmt.Println(i)
    	}
    }
    
    func main() {
    	ch1 := make(chan int)
    	ch2 := make(chan int)
    	go counter(ch1)
    	go squarer(ch2, ch1)
    	printer(ch2)
    }
    
  • 通道异常情况总结

    channel状态 nil 非空 没满
    从通道接收值 阻塞 接收值 阻塞 接收值 接收值
    向通道发送值 阻塞 发送值 发送值 阻塞 发送值
    关闭 panic 关闭成功,读完数据后返回零值 关闭成功,返回零值 关闭成功,读完数据后返回零值 关闭成功,读完数据后返回零值
  • work pool(goroutine池)

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    // 形成三个工人的工作池,然后并行处理五个任务
    
    func worker(id int, jobs <-chan int, results chan<- int) {
    	// 当jobs关闭后读取不到数据则会退出循环
    	for j := range jobs {
    		fmt.Printf("worker:%d start job:%d\n", id, j)
    		time.Sleep(time.Second)
    		fmt.Printf("worker:%d end job:%d\n", id, j)
    		results <- j * 2
    	}
    }
    
    func main() {
    	jobs := make(chan int, 100)
    	results := make(chan int, 100)
    	// 开启3个goroutine,形成工作池
    	for w := 1; w <= 3; w++ {
    		go worker(w, jobs, results)
    	}
    	// 5个任务
    	for j := 1; j <= 5; j++ {
    		jobs <- j
    	}
    	// 关闭jobs防止goroutine阻塞导致死锁
    	close(jobs)
    	// 输出结果
    	for a := 1; a <= 5; a++ {
    		<-results
    	}
    }
    
  • workpool改进版

    package main
    
    // 形成三个工人的工作池,然后并发处理五个任务,
    // 产生数字,数字翻倍转移,读取数字均通过goroutine并发执行,改善运行的效率
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    var wg sync.WaitGroup
    var notice sync.WaitGroup
    
    func worker(id int, jobs <-chan int, results chan<- int) {
    	// 当jobs关闭后读取不到数据则会退出循环
    	for j := range jobs {
    		fmt.Printf("worker:%d start job:%d\n", id, j)
    		time.Sleep(time.Second)
    		fmt.Printf("worker:%d end job:%d\n", id, j)
    		results <- j * 2
    	}
    	wg.Done()
    }
    
    func main() {
    	jobs := make(chan int, 100)
    	results := make(chan int, 100)
    	// 开启3个goroutine,形成工作池
    	wg.Add(3)
    	for w := 1; w <= 3; w++ {
    		go worker(w, jobs, results)
    	}
    	// 5个任务
    	go func() {
    		for j := 1; j <= 5; j++ {
    			jobs <- j
    		}
    		// 关闭jobs防止goroutine阻塞导致死锁
    		close(jobs)
    	}()
    	// 输出结果
    	notice.Add(1)
    	go func() {
    		for x := range results {
    			fmt.Println(x)
    		}
    		notice.Done()
    	}()
    	// 等待所有的worker完成工作,再关闭result通道
    	wg.Wait()
    close(results)
    	// 等待所有的result都输出
    	notice.Wait()
    }
    
    
  • workpool练习

    package main
    
    import (
    	"fmt"
    	"math/rand"
    	"time"
    )
    
    type result struct {
    	value int64
    	sum   int64
    }
    
    func random(jobChan chan<- int64) {
    	for {
    		rand.Seed(time.Now().UnixNano())
    		jobChan <- rand.Int63()
    		time.Sleep(time.Millisecond * 500)
    	}
    }
    
    func work(in int64) (out int64) {
    	for in != 0 {
    		out += in % 10
    		in = in / 10
    	}
    	return
    }
    
    func worker(jobChan <-chan int64, resultsChan chan<- *result) {
    	// 从jobChan中取值,如果jobChan空则阻塞等待,并且由于当所有随机数都放入后就关闭jobChan所以不会死锁
    	// 这里能用无限循环也能用for range,因为通道里会一直有值不会阻塞导致死锁
    	for j := range jobChan {
    		// j := <-jobChan
    		s := work(j)
    		newResult := &result{
    			value: j,
    			sum:   s,
    		}
    		resultsChan <- newResult
    	}
    }
    
    func main() {
    	jobChan := make(chan int64, 100)
    	// 结构体太大所以改为指针
    	resultsChan := make(chan *result, 100)
    	go random(jobChan)
    	// 开启工作池
    	for i := 1; i <= 24; i++ {
    		go worker(jobChan, resultsChan)
    	}
    	// 这里没有使用for range取值是因为resultChan没有关闭直接取值会导致死锁
    	for ret := range resultsChan {
    		// 等待从resultsChan中取值
    		fmt.Printf("value:%d,result:%d\n", ret.value, ret.sum)
    	}
    }
    
  • select多路复用

    select用于从多个通道接受数据的场景,可以响应多个通道的操作,哪个通道操作满足条件了就先执行哪一个,如果有多个同时相应则随机取一个,不会按照从前到后的顺序。select总结如下

    1. 可处理一个或多个channel的发送/接收操作。

    2. 如果多个case同时满足,select会随机选择一个。

    3. 对于没有caseselect{}会一直等待,可用于阻塞main函数。

    package main
    
    import "fmt"
    
    func main() {
    	// 只用一个缓冲时则只能按照装入,取出的顺序执行
    	ch := make(chan int, 1)
    	// 当缓冲够大则select的分支都能执行,则每次执行结果都会是随机的
    	// ch := make(chan int, 10)
    	for i := 0; i < 10; i++ {
    		// select的条件哪个能满足就执行哪个
    		select {
    		//通道有值则输出值
    		case x := <-ch:
    			// 结果为0,2,4,6,8,因为这几次都是放入数,而第1,3,5,7,9次则从中取出这些数进行打印
    			fmt.Println(x)
    		// 通道为空则输入值
    		case ch <- i:
    		}
    	}
    }
    
    
  • 异步日志库

    每一种的日志都能够并行执行,而不是同步按顺序执行任务

    1. 将需要写入的信息放入通道中
    2. 后台开启一个goroutine将通道里的信息输入到日志文件中(这里不能开启多个goroutine,多个进程写入会出现问题,而且切割文件会关闭文件就会导致其他运行的goroutine读取关闭的文件的信息,引发错误)
    package mylogger
    
    import (
    	"fmt"
    	"os"
    	"path"
    	"time"
    )
    
    // 往文件里面写日志
    
    //日志文件结构体
    type FileLogger struct {
    	Level       LogLevel
    	filePath    string //日志文件保存的路径
    	fileName    string //日志文件保存的文件名
    	fileObj     *os.File
    	errFileObj  *os.File
    	maxFileSize int64 //最大文件大小
    	timeFlag    int   //日志时间标志
    	timeErrFlag int   //错误日志时间标志
    	logChan     chan *logMsg
    }
    
    type logMsg struct {
    	level     LogLevel
    	msg       string
    	funcName  string
    	fileName  string
    	timeStamp string
    	line      int
    }
    
    //FileLogger构造函数
    func NewFileLogger(levelStr, fp, fn string, maxSize int64) *FileLogger {
    	LogLevel, err := parseLogLevel(levelStr)
    	if err != nil {
    		panic(err)
    	}
    	tf := time.Now().Minute()
    	tef := time.Now().Minute()
    	fl := &FileLogger{
    		Level:       LogLevel,
    		filePath:    fp,
    		fileName:    fn,
    		maxFileSize: maxSize,
    		timeFlag:    tf,
    		timeErrFlag: tef,
    		logChan:     make(chan *logMsg, 50000),
    	}
    	//按照文件路径和文件名将文件打开
    	err = fl.initFile()
    	if err != nil {
    		panic(err)
    	}
    	return fl
    }
    
    //根据指定的日志文件路径和文件名打开对应日志和错误日志
    func (f *FileLogger) initFile() error {
    	//将文件路径和文件名按照操作系统的格式进行拼接
    	fullFileName := path.Join(f.filePath, f.fileName)
    	//打开日志和日志错误文件
    	fileObj, err := os.OpenFile(fullFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    	if err != nil {
    		fmt.Printf("open log file failed,err:%v\n", err)
    		return err
    	}
    	errfileObj, err := os.OpenFile(fullFileName+".err", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    	if err != nil {
    		fmt.Printf("open errlog file failed,err:%v\n", err)
    		return err
    	}
    	f.fileObj = fileObj
    	f.errFileObj = errfileObj
    	// 这里不能使用多个goroutine执行,因为切割时会关闭文件,则其它进行的goroutine不能获取信息产生错误(应该可以考虑互斥锁)
    	// 开启后台goroutine写日志
    	go f.writeLogBackground()
    	return nil
    }
    
    func (f *FileLogger) Close() {
    	f.fileObj.Close()
    	f.errFileObj.Close()
    }
    
    //根据级别确定是否需要记录该日志
    func (f *FileLogger) enable(loglevel LogLevel) bool {
    	return f.Level <= loglevel
    }
    
    // 判断文件大小
    func (f *FileLogger) checkSize(file *os.File) bool {
    	fileInfo, err := file.Stat()
    	if err != nil {
    		fmt.Printf("get file info failed,err:%v\n", err)
    		return false
    	}
    	//返回当前文件大小和最大值的比较结果
    	return fileInfo.Size() >= f.maxFileSize
    }
    
    //切割文件,文件达到最大后就重新生产新的文件
    func (f *FileLogger) splitFile(file *os.File) (*os.File, error) {
    	//获取旧文件信息和生成新文件信息
    	nowStr := time.Now().Format("20060102150405000")
    	fileInfo, err := file.Stat()
    	if err != nil {
    		fmt.Printf("get fileInfo failed,err:%v\n", err)
    		return nil, err
    	}
    	//不能用fileLogger里的文件名,没有err
    	//使用file查看文件名区分普通日志和错误日志
    	logName := path.Join(f.filePath, fileInfo.Name())
    	newLogName := fmt.Sprintf("%s.bak%s", logName, nowStr)
    	// 1.关闭当前的日志文件,才能重命名
    	file.Close()
    	// 2.将已满的文件重新命名,添加上时间
    	os.Rename(logName, newLogName)
    	// 3.打开一个新的日志文件
    	fileObj, err := os.OpenFile(logName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    	if err != nil {
    		fmt.Printf("open new log file failed,err:%v\n", err)
    		return nil, err
    	}
    	// 4.将新打开的日志文件对象赋值给 f.fileObj
    	return fileObj, nil
    }
    
    func (f *FileLogger) writeLogBackground() {
    	for {
    		//根据文件大小判断结果确认是否切割文件
    		if f.checkSize(f.fileObj) {
    			newFile, err := f.splitFile(f.fileObj)
    			if err != nil {
    				return
    			}
    			f.fileObj = newFile
    		}
    		select {
    		// 能够取出日志
    		case logTmp := <-f.logChan:
    			// 拼接日志信息
    			fmt.Fprintf(f.fileObj, "[%s] [%s] [%s:%s:%d] %s\n", logTmp.timeStamp, getLogString(logTmp.level), logTmp.fileName, logTmp.funcName, logTmp.line, logTmp.msg)
    			//记录日志等级大于Error级别,需要再err日志中再记录一遍
    			if logTmp.level >= ERROR {
    				if f.checkSize(f.errFileObj) {
    					newFile, err := f.splitFile(f.errFileObj)
    					if err != nil {
    						return
    					}
    					f.errFileObj = newFile
    				}
    				fmt.Fprintf(f.errFileObj, "[%s] [%s] [%s:%s:%d] %s\n", logTmp.timeStamp, getLogString(logTmp.level), logTmp.fileName, logTmp.funcName, logTmp.line, logTmp.msg)
    			}
    		default:
    			// 取不出来就休息再退出,防止阻塞
    			time.Sleep(time.Millisecond * 500)
    		}
    
    	}
    }
    
    // 按照文件大写切割日志,每次记录日志之前都要判断下当前写的文件的大小
    func (f *FileLogger) log(lv LogLevel, format string, a ...interface{}) {
    	if f.enable(lv) {
    		msg := fmt.Sprintf(format, a...)
    		now := time.Now()
    		funcName, fileName, lineNo := getInfo(3)
    		// 将需要写的日志信息放入通道中
    		// 1.新建一个logMsg对象
    		logTmp := &logMsg{
    			level:     lv,
    			msg:       msg,
    			funcName:  funcName,
    			fileName:  fileName,
    			timeStamp: now.Format("2006-01-02 15:04:05"),
    			line:      lineNo,
    		}
    		// 尝试往通道放日志,如果通道满了就丢掉该日志继续执行,确保整体业务流畅不阻塞
    		select {
    		case f.logChan <- logTmp:
    		default:
    			// 不操作直接执行
    		}
    
    	}
    }
    
    func (f *FileLogger) Debug(format string, a ...interface{}) {
    	f.log(DEBUG, format, a...)
    }
    
    func (f *FileLogger) Trace(format string, a ...interface{}) {
    	f.log(TRACE, format, a...)
    }
    func (f *FileLogger) Info(format string, a ...interface{}) {
    	f.log(INFO, format, a...)
    }
    func (f *FileLogger) Warning(format string, a ...interface{}) {
    	f.log(WARNING, format, a...)
    }
    func (f *FileLogger) Error(format string, a ...interface{}) {
    	f.log(ERROR, format, a...)
    }
    func (f *FileLogger) Fatal(format string, a ...interface{}) {
    	f.log(FATAL, format, a...)
    }
    
    
  • 互斥锁

    锁的本质是一个结构体,给函数传参数的时候要传指针

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    // 互斥锁
    
    var x = 0
    var wg sync.WaitGroup
    var lock sync.Mutex
    
    func add() {
    	for i := 0; i < 50000; i++ {
    		lock.Lock()
    		x = x + 1
    		lock.Unlock()
    	}
    	wg.Done()
    }
    
    func main() {
    	wg.Add(2)
    	go add()
    	go add()
    	wg.Wait()
    	fmt.Println(x)
    }
    
  • 读写锁

    读写锁sync.RWMutex常常用于读多写少的情况 ,当一个goroutine获得读锁后,其他进程还能继续获得读锁来读取,而如果获得写锁就会阻塞,确保只有一个goroutine能执行操作。

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    // 读写锁
    
    var (
    	x  = 0
    	wg sync.WaitGroup
    	// lock   sync.Mutex
    	rwLock sync.RWMutex
    )
    
    func read() {
    	defer wg.Done()
    	rwLock.RLock()
    	fmt.Println(x)
    	time.Sleep(time.Millisecond)
    	rwLock.RUnlock()
    }
    
    func write() {
    	defer wg.Done()
    	rwLock.Lock()
    	x = x + 1
    	time.Sleep(time.Millisecond * 5)
    	rwLock.Unlock()
    }
    
    func main() {
    	start := time.Now()
    	for i := 0; i < 10; i++ {
    		wg.Add(1)
    		go write()
    
    	}
    	for i := 0; i < 1000; i++ {
    		wg.Add(1)
    		go read()
    	}
    	wg.Wait()
    	fmt.Println(time.Since(start))
    }
    
  • sync.Once

    sync.Once中的Do()方法针对高并发场景下某些操作只执行一次的情景(加载配置文件、关闭一次通道),它内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。每次执行先判断布尔值确认操作是否已经执行,如果没有,则先用互斥锁上锁再执行函数,完成后再释放锁。注意的是Do()的参数是一个没有参数和没有返回值的函数,所以如果要执行有参数的函数,需要使用匿名函数包装为闭包再作为参数。

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    var wg sync.WaitGroup
    var once sync.Once
    
    func f1(ch1 chan int) {
    	defer wg.Done()
    	for i := 0; i < 100; i++ {
    		ch1 <- i
    	}
    	// 这里关闭ch1是为了f2中读取完ch1的数据后不会阻塞而导致死锁,并且可以返回false
    	close(ch1)
    }
    
    func f2(ch1, ch2 chan int) {
    	defer wg.Done()
    	for x := range ch1 {
    		ch2 <- x * x
    	}
    	// 使用once确保只会关闭一次通道ch2,防止出现panic
    	once.Do(func() { close(ch2) })
    }
    
    func main() {
    	// a的缓存可以不用设置满,因为会一边存一边取
    	a := make(chan int, 100)
    	// b的缓存必须设置满,这样才能存储所有的数,并且后续从b中读取数据时才能够读完
    	b := make(chan int, 100)
    	wg.Add(3)
    	go f1(a)
    	go f2(a, b)
    	go f2(a, b)
    	wg.Wait()
    	for ret := range b {
    		fmt.Println(ret)
    	}
    
    }
    
  • sync.Map

    Go内置的map不是并发安全的,当多个goroutinemap中存值和取值时就会报错。而sync.Map是一个并发安全的map,不用像内置的map一样使用make函数初始化才能使用,可以声明后直接使用,而且key和value任何类型。同时sync.Map内置了诸如Store(存值)、Load(取值)、LoadOrStore(先取值如果没有就存值)、Delete(删除值)、Range(遍历值)等操作方法。

    package main
    
    import (
    	"fmt"
    	"strconv"
    	"sync"
    )
    
    // var m = make(map[string]int)
    
    // func get(key string) int {
    // 	return m[key]
    // }
    
    // func set(key string, value int) {
    // 	m[key] = value
    // }
    
    // func main() {
    // 	wg := sync.WaitGroup{}
    // 	// go本身内置的map是不支持安全并发运行的,超过20个并发执行就会出错
    // 	for i := 0; i < 19; i++ {
    // 		wg.Add(1)
    // 		go func(n int) {
    // 			key := strconv.Itoa(n)
    // 			set(key, n)
    // 			fmt.Printf("k=:%v,v:=%v\n", key, get(key))
    // 			wg.Done()
    // 		}(i)
    // 	}
    // 	wg.Wait()
    // }
    
    // sync中的map可以不用分配内存就能够使用
    var m = sync.Map{}
    
    func main() {
    	wg := sync.WaitGroup{}
    	// 使用sync内置的Map可以安全并发执行,go本身内置的map是不支持安全并发运行的,超过20个并发执行就会出错
    	for i := 0; i < 21; i++ {
    		wg.Add(1)
    		go func(n int) {
    			key := strconv.Itoa(n)
    			// 利用自带的store方法存值
    			m.Store(key, n)
    			// 利用自带的load方法取值
    			value, _ := m.Load(key)
    			fmt.Printf("k=:%v,v:=%v\n", key, value)
    			wg.Done()
    		}(i)
    	}
    	wg.Wait()
    	// 遍历
    	m.Range(func(key, value interface{}) bool {
    		fmt.Println(key, value)
    		return true
    	})
    }
    
  • 原子操作

    锁的机制的底层是基于原子操作的,其一般直接通过CPU指令实现,因此直接用原子操作比锁操作更加快。

    package main
    
    import (
    	"fmt"
    	"sync"
    	"sync/atomic"
    )
    
    // 原子操作
    
    var x int64
    var wg sync.WaitGroup
    
    func add() {
    	// x++
    	// 原子操作确保并发安全性
    	atomic.AddInt64(&x, 1)
    	wg.Done()
    }
    
    func main() {
    	wg.Add(100000)
    	for i := 0; i < 100000; i++ {
    		go add()
    	}
    	wg.Wait()
    	fmt.Println(x)
    	// 比较并交换数据
    	var y int64 = 100
    	// 第一个参数和第二个参数比较,如果相等就返回true,用第三个参数的值替换第一个参数值
    	ok := atomic.CompareAndSwapInt64(&y, 100, 200)
    	fmt.Println(ok, y)
    }
    

你可能感兴趣的:(学习,golang,java)