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()
}
}

浙公网安备 33010602011771号