goroutinegoroutinechannel
1. 协程(goroutine)
goroutinegogoroutinegoroutine
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go语言中一次创建十万左右的goroutine也是可以的。
参考自https://www.liwenzhou.com/posts/Go/14_concurrence/
1.1. 创建一个goroutine
goroutinehellogoroutine
package main
import (
"fmt"
)
func hello() {
fmt.Println("hello")
}
func main() {
go hello()
//time.Sleep(time.Second)
fmt.Println("main")
}
time.Sleep()goroutinegoroutinesync.WaitGroup
1.2. sync.WaitGroup
wg.Done()
goroutinehello()goroutinegoroutine
package main
import (
"fmt"
"time"
)
func hello(i int) {
fmt.Println("hello goroutine", i)
}
func main() {
for i := 0; i < 100; i++ {
go hello(i)
time.Sleep(time.Millisecond)
}
}
goroutinegoroutinegoroutinegoroutinegoroutinesleep
sync.WaitGroup
sync.WaitGroupgoroutine
package main
import (
"fmt"
"sync"
)
func hello(i int) {
defer wg.Done() // goroutine将hello中任务执行完,计数器减一
fmt.Println("hello goroutine", i)
}
var wg sync.WaitGroup
func main() {
for i := 0; i < 100; i++ {
wg.Add(1) // 每开启一个goroutine,计数器加一
go hello(i)
//time.Sleep(time.Millisecond)
wg.Wait() // 等待计数器降为0,再继续执行
}
// 更多的业务场景是,多个goroutine和main之间的同步,上述代码只是为了模拟sleep同样的效果。
}
2. channel
goroutinechannelgoroutinegoroutine
2.1. channel类型
// 1. 声明
var ch1 chan int
var ch2 chan bool
// 2. 创建channel
make(chan int, 16)
// 3. 将一个值发送到通道
ch <- 10
// 4. 从一个通道中接收值
x := <- ch // 赋值给x
<- ch // 忽略结果
// 5. 关闭通道
close(ch)
2.2. 优雅的操作channel
- 循环读取数据
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启一个goroutine,把0-24之间的数发送到ch1
go func() {
for i := 0; i < 25; i++ {
ch1 <- i
}
close(ch1)
}()
// 从ch1中取出数据,计算平方,放到ch2
go func() {
for {
x, ok := <- ch1
if ok {
ch2 <- x * x
}
}
close(ch2)
}()
for i := range ch2 {
fmt.Println(i)
}
}
2.3. 单向通道
有些通道可能只用于发送,或者只用于接收。单向通道多用于函数的参数中。
func f1(ch1 chan<- int) {
}
func f2(ch1 <-chan int){}
2.4. 通道注意事项
关闭: 只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被GC回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要作的,但关闭通道不是必须的。
2.5. 生产者消费者模型案例
开启两个协程,一个协程生产数据,另一个协程对数据进行处理,处理完再把数据发回去。
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func producer(in chan<- int) {
defer wg.Done()
for i := 0; i < 10; i++ {
in <- i * i
}
close(in)
}
func consumer(out <-chan int) {
defer wg.Done()
for {
x, ok := <-out
if !ok {
break
}
fmt.Println("num: ", x)
}
}
func main() {
ch := make(chan int, 16)
wg.Add(2)
go producer(ch)
go consumer(ch)
wg.Wait()
fmt.Println("主协程结束...")
}
3. 锁
3.1. 竞态
竞态是指在多个goroutine按某些交错顺序执行时,程序无法给出正确的结果。它对于程序是致命的,因为它们潜伏在程序中,出现的频率也很低,有可能仅在高负载环境或者在特定的编译器、平台和架构时才会出现。发生竞态现象的必要条件是:两个goroutine并发读写同一个变量,并且至少其中一个goroutine是写入操作。
在存款操作和取款操作并发执行的时候,就会发生静态现象。有三种方法可以避免数据静态。
- 不要修改变量。
- 避免从多个goroutine访问同一变量。
- 允许多个goroutine访问同一变量,但同一时间只有一个goroutine可以访问。这种方法称为互斥机制。
Golang中代码中加锁,一般体现在是对临界区加锁。
临界区:程序片段访问临界资源的代码,临界区同一时刻只能有一个线程运行,其他线程必须等待访问,所以需要用到锁;
3.2. 互斥锁:sync.Mutex
goroutinesyncMutexgoroutinegoroutinegoroutinegoroutine
3.3. 读写互斥锁:sync.RWMutex
syncRWMutexgoroutinegoroutinegoroutine
4. sync包其它应用
4.1. sync.Once
sync.Once
// 如果要执行函数f,就需要搭配闭包来使用
func (o *Once) Do(f func()) {}
4.2. sync.Map
sync.Map
5. atomic原子性操作
- 我开10万个goroutine,每个goroutine对全局变量x加一,每次执行结果都不一样。
package main
import (
"fmt"
"sync"
)
var x int
var wg sync.WaitGroup
func add() {
x++
wg.Done()
}
func main() {
for i := 0; i < 100000; i++ {
wg.Add(1)
go add()
}
wg.Wait()
fmt.Println(x)
}
2. 加锁(sync.Mutex)
package main
import (
"fmt"
"sync"
)
var x int
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
defer wg.Done()
// 对临界区加锁
lock.Lock()
x++
lock.Unlock()
}
func main() {
for i := 0; i < 100000; i++ {
wg.Add(1)
go add()
}
wg.Wait()
fmt.Println(x)
}
3. 使用原子操作
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
defer wg.Done()
//lock.Lock()
//x++
//lock.Unlock()
atomic.AddInt64(&x, 1)
}
func main() {
for i := 0; i < 100000; i++ {
wg.Add(1)
go add()
}
wg.Wait()
fmt.Println(x)
}