在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
并发与并行并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。 并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。 Go语言的并发通过 Go语言还提供 Go语言中的 在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能– 使用goroutineGo语言中使用 一个 启动单个goroutine启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个 func Hello() { fmt.Println("hello word") } func main() { Hello() fmt.Println("This main func") } 这个示例中hello函数和下面的语句是串行的 接下来我们在调用hello函数前面加上关键字 func main() { // 启动 goroutine go Hello() // 1.创建一个goroutine 2.在新的goroutine中执行Hello函数 fmt.Println("This main func") time.Sleep(time.Second) } 当main()函数返回的时候该 所以我们要想办法让main函数等一等hello函数,最简单粗暴的方式就是 WaitGroup用来等待goroutine运行结束,用法类似python中Event var wg sync.WaitGroup func Hello() { fmt.Println("hello word") wg.Done() // 计数器-1 } func main() { wg.Add(1) // 计数 +1 go Hello() fmt.Println("This main func") wg.Wait() // 阻塞,一直等待所有的goroutine结束 } 启动多个goroutinevar wg sync.WaitGroup func hello(i int) { defer wg.Done() // goroutine结束就登记-1 fmt.Println("Hello Goroutine!", i) } func main() { for i := 0; i < 10; i++ { wg.Add(1) // 启动一个goroutine就登记+1 go hello(i) } wg.Wait() // 等待所有登记的goroutine都结束 } 多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个 channel单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。 虽然可以使用共享内存进行数据交换,但是共享内存在不同的 Go语言的并发模型是 如果说 Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。 channel类型
var 变量 chan 元素类型
eg: var ch1 chan int // 声明一个传递整型的通道 var ch2 chan bool // 声明一个传递布尔型的通道 var ch3 chan []int // 声明一个传递int切片的通道 创建channel通道是引用类型,通道类型的空值是 var ch chan int fmt.Println(ch) // <nil>
声明的通道后需要使用 创建channel的格式如下: make(chan 元素类型, [缓冲大小]) channel的缓冲大小是可选的。 channel操作通道有发送(send)、接收(receive)和关闭(close)三种操作。 发送和接收都使用 现在我们先使用以下语句定义一个通道: ch := make(chan int) 发送将一个值发送到通道中。 ch <- 10 // 把10发送到ch中 接收从一个通道中接收值。 x := <- ch // 从ch中接收值并赋值给变量x <-ch // 从ch中接收值,忽略结果 关闭我们通过调用内置的 close(ch) 关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。 关闭后的通道有以下特点:
eg; package main import "fmt" // channel func main() { // 定义一个ch1变量 // 是一个channel类型 // 这个channel内部传递的数据是int类型 var ch1 chan int var ch2 chan string // channel是引用类型 fmt.Println("ch1:", ch1) // nil fmt.Println("ch2:", ch2) // nil // make函数初始化(分配内存):slice map channel ch3 := make(chan int, 10) // 通道的操作:发送、 接收、关闭 // 发送和接收都用一个符号: <- ch3 <- 10 // 把10发送到ch3中 // ch3 <- 20 // <-ch3 // 从ch3中接收值,直接丢弃 ret := <-ch3 // 从ch3中接收值,保存到变量ret中 fmt.Println(ret) ch3 <- 9 ch3 <- 8 ch3 <- 7 // 关闭 close(ch3) // 1. 关闭的通道再接收,能取到对应类型的零值 ret2 := <-ch3 fmt.Println(ret2) // 2. 往关闭的通道中发送值 会引发panic // ch3 <- 20 // 3. 关闭一个已经关闭的通道会引发panic // close(ch3) } 无缓冲的通道无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码: func main() { ch := make(chan int) ch <- 10 fmt.Println("发送成功") } 上面这段代码能够通过编译,但是执行的时候会出现以下错误: fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() .../src/github.com/Q1mi/studygo/day06/channel02/main.go:8 +0x54 为什么会出现 因为我们使用 上面的代码会阻塞在 一种方法是启用一个 func recv(c chan int) { ret := <-c fmt.Println("接收成功", ret) } func main() { ch := make(chan int) go recv(ch) // 启用goroutine从通道接收值 ch <- 10 fmt.Println("发送成功") } 无缓冲通道上的发送操作会阻塞,直到另一个 使用无缓冲通道进行通信将导致发送和接收的 有缓冲的通道解决上面问题的方法还有一种就是使用有缓冲区的通道。我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如: func main() { ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道 ch <- 10 fmt.Println("发送成功") } 只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。 我们可以使用内置的 for range从通道循环取值当向通道中发送完数据时,我们可以通过 当通道被关闭时,再往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢? 我们来看下面这个例子: // channel 练习 func main() { ch1 := make(chan int) ch2 := make(chan int) // 开启goroutine将0~100的数发送到ch1中 go func() { for i := 0; i < 100; i++ { ch1 <- i } close(ch1) }() // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中 go func() { for { i, ok := <-ch1 // 通道关闭后再取值ok=false if !ok { break } ch2 <- i * i } close(ch2) }() // 在主goroutine中从ch2中接收值打印 for i := range ch2 { // 通道关闭后会退出for range循环 fmt.Println(i) } } 从上面的例子中我们看到有两种方式在接收值的时候判断该通道是否被关闭,不过我们通常使用的是 单向通道有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。 Go语言中提供了单向通道来处理这种情况。例如,我们把上面的例子改造如下: 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) }
在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。 通道总结
关闭已经关闭的 select多路复用在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现: for{ // 尝试从ch1接收值 data, ok := <-ch1 // 尝试从ch2接收值 data, ok := <-ch2 … } 这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了
select{ case <-ch1: ... case data := <-ch2: ... case ch3<-data: ... default: 默认操作 } 举个小例子来演示下 func main() { ch := make(chan int, 1) for i := 0; i < 10; i++ { select { case x := <-ch: fmt.Println(x) case ch <- i: } } } 使用
并发安全和锁有时候在Go代码中可能会存在多个 举个例子: var x int64 var wg sync.WaitGroup func add() { for i := 0; i < 5000; i++ { x = x + 1 } wg.Done() } func main() { wg.Add(2) go add() go add() wg.Wait() fmt.Println(x) }
上面的代码中我们开启了两个 互斥锁互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 var x int64 var wg sync.WaitGroup var lock sync.Mutex func add() { for i := 0; i < 5000; i++ { lock.Lock() // 加锁 x = x + 1 lock.Unlock() // 解锁 } wg.Done() } func main() { wg.Add(2) go add() go add() wg.Wait() fmt.Println(x) } 使用互斥锁能够保证同一时间有且只有一个 读写互斥锁互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用 读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的 读写锁示例: var ( x int64 wg sync.WaitGroup lock sync.Mutex rwlock sync.RWMutex ) func write() { // lock.Lock() // 加互斥锁 rwlock.Lock() // 加写锁 x = x + 1 time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒 rwlock.Unlock() // 解写锁 // lock.Unlock() // 解互斥锁 wg.Done() } func read() { // lock.Lock() // 加互斥锁 rwlock.RLock() // 加读锁 time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒 rwlock.RUnlock() // 解读锁 // lock.Unlock() // 解互斥锁 wg.Done() } 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() end := time.Now() fmt.Println(end.Sub(start)) } 需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。 |
请发表评论