select
select 语句用于在多个发送/接收信道操作中进行选择。
select 语句会一直阻塞,直到发送/接收操作准备就绪。
如果有多个信道操作准备完毕,select 会随机地选取其中之一执行。
该语法与 switch 类似,所不同的是,这里的每个 case 语句都是信道操作。
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
time.Sleep(6 * time.Second)
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
select应用
默认情况
在没有 case 准备就绪时,可以执行 select 语句中的默认情况(Default Case)。通常用于防止 select 语句一直阻塞。
package main
import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(10500 * time.Millisecond)
ch <- "process successful"
}
func main() {
ch := make(chan string)
go process(ch)
for {
time.Sleep(1000 * time.Millisecond)
select {
case v := <-ch:
fmt.Println("received value: ", v)
return
default:
fmt.Println("no value received")
}
}
}
死锁与默认情况
package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}
由于没有 Go 协程向该信道写入数据,因此 select 语句会一直阻塞,导致死锁。该程序会触发运行时 panic
如果存在默认情况,就不会发生死锁,因为在没有其他 case 准备就绪时,会执行默认情况,例:
package main
import "fmt"
func main() {
ch := make(chan string)
select {
case <-ch:
default:
fmt.Println("default case executed")
}
}
如果 select 只含有值为 nil 的信道,也同样会执行默认情况。
package main
import "fmt"
func main() {
var ch chan string
select {
case v := <-ch:
fmt.Println("received value", v)
default:
fmt.Println("default case executed")
}
}
select 随机选取
当 select 由多个 case 准备就绪时,将会随机地选取其中之一去执行。
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
ch <- "from server1"
}
func server2(ch chan string) {
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
time.Sleep(1 * time.Second)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
空 select
package main
func main() {
select {}
}
Mutex
通过 Mutex 和信道来处理竞态条件(Race Condition)。
临界区:
当程序并发地运行时,多个 Go 协程不应该同时访问那些修改共享资源的代码。这些修改共享资源的代码称为临界区。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x + 1
m.Unlock()
wg.Done()
}
func main() {
var w sync.WaitGroup
var m sync.Mutex
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, &m)
}
w.Wait()
fmt.Println("final value of x", x)
}
Mutex 是一个结构体类型,我们在第 15 行创建了 Mutex
类型的变量 m
,其值为零值。在上述程序里,我们修改了 increment
函数,将增加 x
的代码(x = x + 1
)放置在 m.Lock()
和 m.Unlock()
之间。现在这段代码不存在竞态条件了,因为任何时刻都只允许一个协程执行这段代码。
传递 Mutex 的地址很重要。如果传递的是 Mutex 的值,而非地址,那么每个协程都会得到 Mutex 的一份拷贝,竞态条件还是会发生。
使用信道处理竞态条件
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
ch <- true
x = x + 1
<- ch
wg.Done()
}
func main() {
var w sync.WaitGroup
ch := make(chan bool, 1)
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, ch)
}
w.Wait()
fmt.Println("final value of x", x)
}
创建了容量为 1 的缓冲信道,将它传入 increment
协程。该缓冲信道用于保证只有一个协程访问增加 x
的临界区。具体的实现方法是在 x
增加之前,传入 true
给缓冲信道。由于缓冲信道的容量为 1,所以任何其他协程试图写入该信道时,都会发生阻塞,直到 x
增加后,信道的值才会被读取。实际上这就保证了只允许一个协程访问临界区。
Mutex 和 信道 该如何选择
当 Go 协程需要与其他协程通信时,可以使用信道。而当只允许一个协程访问临界区时,可以使用 Mutex。
就我们上面解决的问题而言,我更倾向于使用 Mutex,因为该问题并不需要协程间的通信。所以 Mutex 是很自然的选择。
网友评论