Go的第15课——锁、channel遍历、select 语句

sync.Mutex

基本

这个协程可以理解为多线程里面的线程。线程在操作临界资源的时候会产生竞争,锁可以避免发生竞争。

使用以下语法定义一个锁变量

1
var lock sync.Mutex

lock.Lock() 可以为一个锁进行上锁操作,若锁已被其它协程上锁,则阻塞当前协程直到锁被释放。

lock.Unlock() 可以释放一个锁。

实例

看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup
var lock sync.Mutex
var k int
var locktest bool = false//change here

func add() {
defer wg.Done()
if locktest {
lock.Lock()
defer lock.Unlock()
}
k += 1

}
func sub() {
defer wg.Done()
if locktest {
lock.Lock()
defer lock.Unlock()
}
k -= 1

}
func main() {
for i := 0; i < 1000; i++ {
wg.Add(2)
go add()
go sub()
}
wg.Wait()
fmt.Printf("last value:%v\n", k)
}

这里使用了 WaitGroup 同步协程。很明显如果仅仅只是在 addsub 函数中对全局变量进行加减(locktest=false),那么结果不一定是 ==0 的。而如果加上了锁(locktest=true),那么结果必定为 0。

channel遍历

这个本来应该放 channel 那一节的,不过视频顺序如此,也在此补一下相当于。

channel 有个特性就是,如果 channel 没有缓冲区,那么写的时候会阻塞,直到读的协程到来才会继续运行。如果尝试读没有被写的协程,那么会造成错误,因此给遍历带来了一些困难。所以我们使用 channel 的时候,只要确定了后续不往 channel 中写数据就直接关闭,我们在读关闭的 channel 时会读到默认值并且第二个值 ok 会置为 false

for循环遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import "fmt"

var ch = make(chan int)

func test() {
for {//死循环读
if v, ok := <-ch; ok {//注意这里的写法,先初始化,再判断
fmt.Printf("value:%v\n", v)
} else {
break
}
}
}
func main() {
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
test()
}
/*
value:0
value:1
value:2
value:3
value:4
value:5
value:6
value:7
value:8
value:9
*/

for range 遍历

这种方式更加简单粗暴了,就把 channel 当成一个切片即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import "fmt"

var ch = make(chan int, 5)

func test() {
for i := range ch {
fmt.Println(i)
}
}
func main() {
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
test()
}
/*
0
1
2
3
4
5
6
7
8
9
*/

select语句

  1. select是Go中的一个控制结构,类似于switch语句,用于处理异步1o操作。select会监听case语句中channel的读写操作,当case中channel读写操作为非阻塞状态(即能读写)时,将会触发相应的动作。
  2. 如果有多个case都可以运行,select 会随机公平地选出一个执行,其他不会执行。
  3. 如果没有可运行的 case 语句,且有default 语句,那么就会执行 default 的动作。
  4. 如果没有可运行的 case 语句,且没有 default 语句,select 将阻塞,直到某个case 通信可以运行

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"fmt"
"runtime"
"time"
)

var intch = make(chan int)
var stringch = make(chan string)

func main() {
go func() {
intch <- 114514
stringch <- "xia0ji233"
//defer close(intch)
//defer clo se(stringch)
}()
runtime.Gosched()//让子协程先运行
for {
select {
case r := <-intch:
fmt.Printf("get int value: %v\n", r)
case r := <-stringch:
fmt.Printf("get string value: %v\n", r)
default:
fmt.Println("no value")
}
time.Sleep(time.Second)
}
}
/*
get int value: 114514
get string value: xia0ji233
no value
no value
no value
no value
...
*/

它还有一个特性就是,for循环select时,如果其中一个case通道已经关闭,则每次都会执行到这个case,有多个 case 被关闭的话则也是随机从被关闭的 case 读取。