当前位置:网站首页>【Go语言学习】——并发编程
【Go语言学习】——并发编程
2022-06-10 22:48:00 【游戏编程】
并发编程
参考博客go 语言中的并发是通过用户态的线程实现的,相比起 java 需要自己维护线程池并进行调度和上下文切换, go 主需要利用 goroutine 去管理并发,并且相比内核态的线程更加轻量化,可以创建成千上万的 goroutine 线程工作,由 go 中的 runtime 调度完成的,利用 channel 可以在 goroutine 之间进行通信
在并发编程中常常用到闭包,但是注意如果希望调用的外部参数就是调用时的值,需要作为参数传入而不是直接利用闭包机制调用,因为可能等进程调用时外部参数已经发生改变了。
- goroutine
> 在调用函数前面加上go关键字,就能为该函数创建一个goroutine
> 当goroutine对应函数结束那么该任务也结束了,而main函数结束了,则所有由它创建的goroutine都结束了
package mainimport "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 mainimport ( "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.WaitGroupfunc 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的关系
一个操作系统线程对应用户态多个goroutine。
go程序可以同时使用多个操作系统线程。
goroutine和OS线程是多对多的关系,即m:n(m个goroutine分配给n个操作系统线程执行)Go运行时可以使用
GOMAXPROCS确定使用多少个OS线程执行GO代码,默认是CPU核心数,可以通过runtime.GOMAXPROCS()自定义占用的CPU数
package mainimport ( "fmt" "runtime" "sync")var wg sync.WaitGroupfunc 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 mainimport ( "fmt" "sync")var a chan intvar b chan int // 需要指定通道中的类型var wg sync.WaitGroupfunc 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 mainimport ( "fmt" "sync")// channel练习// 1.启动一个goroutine,生成100个数发送到ch1// 2.启动一个goroutine,从ch1中取值,计算其平方放入ch2中// 3。在main中,从ch2中取值打印var wg sync.WaitGroupfunc 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是一种类型,程序结束后会自动回收。关闭后的通道具有以下特点:
对一个关闭的通道再发送值就会导致panic。对一个关闭的通道进行接收会一直获取值直到通道为空。对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。关闭一个已经关闭的通道会导致panic。
package mainimport "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 mainimport ( "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.WaitGroupvar notice sync.WaitGroupfunc 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 mainimport ( "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总结如下
> 可处理一个或多个channel的发送/接收操作。
> 如果多个case同时满足,select会随机选择一个。
> 对于没有case的select{}会一直等待,可用于阻塞main函数。
package mainimport "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: } }}- 异步日志库 每一种的日志都能够并行执行,而不是同步按顺序执行任务
将需要写入的信息放入通道中后台开启一个goroutine将通道里的信息输入到日志文件中(这里不能开启多个goroutine,多个进程写入会出现问题,而且切割文件会关闭文件就会导致其他运行的goroutine读取关闭的文件的信息,引发错误)
package myloggerimport ( "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 mainimport ( "fmt" "sync")// 互斥锁var x = 0var wg sync.WaitGroupvar lock sync.Mutexfunc 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 mainimport ( "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 mainimport ( "fmt" "sync")var wg sync.WaitGroupvar once sync.Oncefunc 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不是并发安全的,当多个goroutine在map中存值和取值时就会报错。而sync.Map是一个并发安全的map,不用像内置的map一样使用make函数初始化才能使用,可以声明后直接使用,而且key和value任何类型。同时sync.Map内置了诸如Store(存值)、Load(取值)、LoadOrStore(先取值如果没有就存值)、Delete(删除值)、Range(遍历值)等操作方法。
package mainimport ( "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 mainimport ( "fmt" "sync" "sync/atomic")// 原子操作var x int64var wg sync.WaitGroupfunc 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)}作者:KayCh
游戏编程,一个游戏开发收藏夹~
如果图片长时间未显示,请使用Chrome内核浏览器。
边栏推荐
- IGBT与三代半导体SiC双脉冲测试方案
- 【Pygame小游戏】《坦克大战》,那些童年的游戏你还记得几个呢?
- LabVIEW用VISA Read函数来读取USB中断数据
- 【Pygame合集】滴~穿越童年游戏指南 请查收:这里面有你玩过的游戏嘛?(附五款源码自取)
- [turtle confessions collection] "the moon at the bottom of the sea is the moon in the sky, and the person in front of us is the sweetheart." Be happy for the rest of your life, and be safe for ever ~
- LabVIEW obtains the information of all points found by the clamp function
- [latex] latex vs Code Snippets
- LabVIEW使用MathScript Node或MATLAB脚本时出现错误1046
- How to remove the blank at the top of listview
- WinDriver compilation summary
猜你喜欢

黑马头条丨腾讯薪酬制度改革引争议;英特尔全国扩招女工程师;黑马100%就业真的吗......

【Opencv实战】寒冷的冬季,也会迎来漫天彩虹,这特效你爱了嘛?

LabVIEW用VISA Read函数来读取USB中断数据

LabVIEW get IMAQ get last event coordinates

Leetcode-15 sum of three numbers
![[turtle confessions collection]](/img/81/b4bacc23691e58e403f1330d0ca7cf.jpg)
[turtle confessions collection] "the moon at the bottom of the sea is the moon in the sky, and the person in front of us is the sweetheart." Be happy for the rest of your life, and be safe for ever ~

安全生产月,黄埔开展燃气安全进商铺宣传活动

【AI出牌器】第一次见这么“刺激”的斗地主,胜率高的关键因素竟是......

curl导入postman报错小记

A simple understanding of B tree
随机推荐
【Pygame合集】回忆杀-“童年游戏”,看看你中几枪?(附五款源码自取)
vtk.js中vtp下载
2022年高考量化卷|请各位量化考生答题
【Pygame小游戏】激荡大脑思维,一起来玩转奇思妙想“24点”叭~(超赞滴)
LabVIEW执行串行回送测试
When leaving the web page, the website displays 404 Not found- starze V Club
OpenResty安装
【AI出牌器】第一次见这么“刺激”的斗地主,胜率高的关键因素竟是......
【无标题】
【Pygame小游戏】不怕你走不过系列:极致AI走迷宫,学习完带你打开新世界大门~(附游戏源码)
csdn每日一练——有序表的折半查找
【Pygame小游戏】这款经典的炸弹人超能游戏上线,你爱了嘛?(附源码)
Flowable process deployment
30 | how to reset the consumer group displacement?
集合删除元素技巧 removeIf
[pyGame games] interesting puzzle game: how many hamsters can you play? (source code attached)
WinDriver compilation summary
[pyGame games] I'm not afraid you can't walk the maze series: the ultimate AI walks the maze. After learning, it will take you to open the door to a new world ~ (with game source code)
Excel essential toolbox 17.0 Free Edition
A simple understanding of B tree