Go_并发编程
gorouting
goroutine是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。
package main import ( "fmt" ) func main() { go Add(1, 1) } func Add(x, y int) { z := x + y fmt.Println(z) }
在一个函数调用前加上 go 关键字,这次调用就会在一个新的goroutine中并发执行。当被调用的函数返回时,这个goroutine也自动结束了。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃
通信
传统方式: 共享数据加锁
package main import "fmt" import "sync" import "runtime" var counter int = 0 func Count(lock *sync.Mutex) { lock.Lock() counter++ fmt.Println(counter) lock.Unlock() } func main() { lock := &sync.Mutex{} for i := 0; i < 10; i++ { go Count(lock) } for { lock.Lock() c := counter lock.Unlock() runtime.Gosched() if c >= 10 { break } } }
Go语言提供的是另一种通信模型,即以消息机制而非共享内存作为通信方式.
消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。这有点类似于进程的概念,每个进程不会被其他进程打扰,它只做好自己的工作就可以了。不同进程间靠消息来通信,它们不会共享内存。
channel是Go语言在语言级别提供的goroutine间的通信方式。我们可以使用channel在两个或多个goroutine之间传递消息。channel是进程内的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。
channel
特点
- 类似unix中的管道先进先出
- 线程安全, 多个goroutine同时访问,不需要加锁
- channel是有类型的, 一个整数的channel只能存放整数
声明
// var 变量名 chan 类型 // 比如下面的数字类型的示例 func main() { var intChan chan int intChan = make(chan int, 10) intChan <- 10 }
操作
// 初始化 intChan = make(chan int, 10) // 存值 intChan <- 10 // 取值 a := <- intChan
示例
package main import "fmt" type student struct { name string } func main() { var stuChan chan interface{} stuChan = make(chan interface{}, 10) stu := student{name:"stu1"} stuChan <- &stu var stu1 interface{} stu1 = <- stuChan var stu2 *student stu2, ok := stu1.(*student) // 接口转类型 if !ok{ fmt.Println("can not convert") return } fmt.Println("stu2",stu2) }
阻塞: 取不到元素会等待, 放不进去也会等待
package main import( "time" "fmt" ) func main(){ var ch chan int ch = make(chan int, 10) func (ch chan int){ time.Sleep(2*time.Second) ch <- 10 }(ch) a := <- ch fmt.Println(a) }
关闭一个channel, 使其只能进行读操作,且读完后不会阻塞
// 关闭一个channel(只能读,不能写), 以及判断是否关闭 func main(){ var ch chan int ch = make(chan int, 10) for i:=0; i<10; i++{ ch <- i } close(ch) // 关闭 for{ var b int b,ok := <-ch if ok ==false{ fmt.Println("chan is close") break } fmt.Println(b) } }
循环一个channel
func main(){ var ch chan int ch = make(chan int, 10) for i:=0; i<10; i++{ ch <- i } close(ch) for v:= range ch{ fmt.Println(v) } }
通过channel阻塞的特性, 我们可以通过channel来等待gorouting结束
package main import ( "fmt" ) func calc(taskChan chan int, resChan chan int,exitChan chan bool){ for v := range taskChan{ flag := true for i:=2; i<v; i++{ if v%i == 0{ flag = false break } } if flag{ resChan <- v } } fmt.Println("exit") exitChan <- true // 计算结束后将一个结果放入已完成的channel中 } func main() { intChan := make(chan int, 1000) resChan := make(chan int, 1000) exitChan := make(chan bool, 8) go func(){ for i:= 0;i<10000 ;i++{ intChan <- i } close(intChan) }() // 将数字全部放入一个channel for i:= 0; i<8; i++{ go calc(intChan, resChan, exitChan) } // 开启8个gorouting去计算 // 等待所有计算的gorouting全部退出 go func(){ for i:= 0; i<8; i++{ <- exitChan // 阻塞等待完成 } close(resChan) }() for v:= range resChan{ fmt.Println(v) } }
select语法
select 的用法与 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由case 语句来描述。与 switch 语句可以选择任何可使用相等比较的条件相比, select 有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个IO操作,大致的结构如下:
select { case <-chan1: // 如果chan1成功读到数据,则进行该case处理语句 case chan2 <- 1: // 如果成功向chan2写入数据,则进行该case处理语句 default: // 如果上面都没有成功,则进入default处理流程 }
可以看出, select 不像 switch ,后面并不带判断条件,而是直接去查看 case 语句。每个case 语句都必须是一个面向channel的操作。
基于此功能可以实现跳过阻塞
func main(){ car ch chan int ch = make(chan int, 10) for i:=0; i<10; i++{ ch <-i } for{ select{ case v:= <-ch fmt.Println(v) default: fmt.Println("get data timeout") time.Sleep(time.Second) } } }
如果channel去不到值就会执行默认操作, 也可以接多个IO操作
超时机制
Go语言没有提供直接的超时处理机制,但我们可以利用 select 机制。虽然 select 机制不是专为超时而设计的,却能很方便地解决超时问题。因为 select 的特点是只要其中一个 case 已经完成,程序就会继续往下执行,而不会考虑其他 case 的情况。
func main(){ car ch chan int ch = make(chan int, 10) for i:=0; i<10; i++{ ch <-i } for{ // 首先,我们实现并执行一个匿名的超时等待函数 timeout := make(chan bool, 1) go func() { time.Sleep(1e9) // 等待1秒钟 timeout <- true }() // 然后我们把timeout这个channel利用起来 select{ case v:= <-ch fmt.Println(v) case <-timeout // 一直没有从ch中读取到数据,但从timeout中读取到了数据 default: fmt.Println("get data timeout") time.Sleep(time.Second) } } }
单向channel
顾名思义,单向channel只能用于发送或者接收数据。channel本身必然是同时支持读写的,否则根本没法用。假如一个channel真的只能读,那么肯定只会是空的,因为你没机会往里面写数据。同理,如果一个channel只允许写,即使写进去了,也没有丝毫意义,因为没有机会读取里面的数据。所谓的单向channel概念,其实只是对channel的一种使用限制。
我们在将一个channel变量传递到一个函数时,可以通过将其指定为单向channel变量,从而限制该函数中可以对此channel的操作,比如只能往这个channel写,或者只能从这个channel读。
单向channel变量的声明非常简单,如下:
var ch1 chan int // ch1是一个正常的channel,不是单向的 var ch2 chan<- float64// ch2是单向channel,只用于写float64数据 var ch3 <-chan int // ch3是单向channel,只用于读取int数据
限制操作
func Parse(ch <-chan int) { for value := range ch { fmt.Println("Parsing value", value) } }
同步锁
互斥锁 : 只有一个线程能够执行加锁代码
var lock sync.Mutex func main(){ for i := 0; i < 2; i++ { go func(b map[int]int) { lock.Lock() b[8] = rand.Intn(100) lock.Unlock() }(a) } }
读写锁 : 分读锁与写锁, 一个线程获得读锁时其他线程依旧可以进行读操作.而一个线程获得写锁时, 其他线程不能进行操作
func main(){ var rwLock sync.RWMutex rwLock.Lock() // 写suo b[8] = rand.Intn(100) time.Sleep(10 * time.Millisecond) rwLock.Unlock() rwLock.RLock() // 读锁 time.Sleep(time.Millisecond) fmt.Println(a) rwLock.RUnlock() }
单次锁 : 只有此一次会被执行
var a string var once sync.Once func setup() { a = "hello, world" } func doprint() { once.Do(setup) print(a) } func twoprint() { go doprint() go doprint() }
只能执行没有参数没有返回值的函数
package sync import ( "sync/atomic" ) type Once struct { m Mutex done uint32 } func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 1 { return } // Slow-path. o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }