go多线程

注意

go是默认一个核只在一个时间点只可以运行一个线程 如果要实现并发 需要告诉go我们允许同时使用多个核 这样才可以实现真正意义上的并发


goroutine

go语言可以使用go开始执行一个新的线程 完果没有go的语句就跟正常一样 当所有的代码执行完毕而还有goroutine还没执行完 那么那些线程都会被终止

1
2
3
4
func main()  {
go fmt.Println("lalala")
fmt.Println("hahaha")
}

上面的情况是 其实只用了一个核 所有这不是并行 这是并发 那么输出结果是取决于那个goroutine结束最快 第二行输出结束最快(这里跟异步的感觉有点像) 那么输出是第二行 然后第二行运行结束之后 就终止了所有的goroutine 这时候第一行就来不及执行了 第一行等不到结束就被终止了

如果需要两行都输出 那么需要sleep一下 当第二行执行完的时候 这时候sleep会释放出CPU资源 这时候第一行可以使用CPU资源进行执行输出结果

1
2
3
4
5
func main()  {
go fmt.Println("lalala")
fmt.Println("hahaha")
time.Sleep(4)
}

输出
image_1chi83vnr1r2417ginpa12s9118c9.png-14kB

goroutine应用在func上

1
2
3
4
5
6
7
8
9
10
11
12
func publicMews(text string, delay time.Duration)  {
go func() {
time.Sleep(delay)
fmt.Println("breaking news", text)
}()
fmt.Println("after func")
time.Sleep(3)
}

func main() {
publicMews("world cup", 12)
}

结果跟上面类似


管道的基本使用

声明管道

声明双向的无缓存管道

test := make(chan int)

声明双向的有缓存管道

test2 := make(chan int, 10)

创建只写管道

test3 := make(chan <- int)

创建只读管道

test4 := make(<- chan int)

也可以声明通道是指针类型

test5 := make(<- chan *int)

使用管道在线程之间进行通信

当一个管道被声明之后 它是开着的 如果没有信息传进去 那么等待管道的线程一直等不到输出 会陷入死锁

只要另外一个线程的管道没有被正确关闭 那么主线程会一直等待 这样会陷入死锁

像下面这样

1
2
ch := make(chan string)
fmt.Println(<- ch)

输出显示死锁
image_1chiabekv14kl1lat12lm1nqkmlf1m.png-31.7kB

但是即使我们没有在管道中输入信息 但是及时关闭了管道 这样是可以避免出现死锁的出现的

像下面这样

1
2
3
ch := make(chan string)
close(ch)
fmt.Println(<- ch)

这样就没有造成死锁问题了

下面的例子 第一次发送之后我们关闭这个管道 那么第一次接受是可以接受到发送过来的内容的 然后因为我们关闭了这个管道 那么接下来的输出都是输出空字符串 同时因为已经正确关闭了管道 所以不会出现上述的死锁问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
ch := make(chan string)
go func() {
ch <- "lalalalla"
close(ch)
}()
fmt.Println(<- ch)
fmt.Println(<- ch)
fmt.Println(<- ch)
a, b := <- ch
fmt.Println("a", a, "b", b)
}

输出
image_1chib44pt1faa1t1crb81v2a22j23.png-16.9kB

管道使用的时候一定要注意发送方和接受方一定要处于两个不同的线程 如果处于一个线程 会有一个莫名其妙的死锁报错

像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func sendMsgToCh(ch chan int) chan int {
go func() {
for i := 0; i < 4; i++ {
ch <- i
close(ch)
}
}()
return ch
}

func main() {
ch := make(chan int)
r := sendMsgToCh(ch)
for c := range r {
fmt.Println("receive value", c)

}
}

报错
image_1chk2d6gp1ffs16mg1o0hniitqe39.png-164.9kB


数据竞争

其实就是公有变量的互斥性访问问题 这个设置互斥锁问题

例如下面

1
2
3
4
5
6
7
8
9
10
11
func main()  {
ch := make(chan int)
n := 0
go func() {
n++
close(ch)
}()
n++
time.Sleep(3)
fmt.Println(n)
}

输出结果是 如果没有sleep三秒 那么输出一般都是1的 如果sleep之后输出是2 所以可见两个线程都是在做自增n的操作的

避免数据竞争的唯一方式是线程间同步访问所有的共享可变数据

在go语言中 这一般都是使用

  • 管道

有句话说的好

不要通过共享内存来通讯 而是通过通讯来共享内存(共享变量)

通过管道来实现两个线程同步修改公有变量

1
2
3
4
5
6
7
8
9
10
11
12
func main()  {
ch := make(chan int)
n := 0
go func() {
n++
ch <- n
close(ch)
}()
r := <- ch
r++
fmt.Println(r)
}


同步锁

向上面说的 出了使用管道来在进程间进行数据传输 还可以使用同步锁实现公有变量的互斥访问

下面是使用同步锁来让两个线程都自增一个公有变量

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
package main

import (
"sync"
"fmt"
)

type AutomicInt struct {
mu sync.Mutex
val int
}

func (a *AutomicInt) add() {
a.mu.Lock()
a.val++
a.mu.Unlock()
}

func (a AutomicInt) get() int {
a.mu.Lock()
r := a.val
a.mu.Unlock()
return r
}

func main() {
mu := sync.Mutex{}
a := AutomicInt{mu, 0}
wait := make(chan struct{})
go func() {
a.add()
close(wait)
}()
a.add()
<- wait
r := a.get()
fmt.Println(r)
}

输出
image_1chk4qjrm2d31ljs4ve1nq56cf9.png-15.8kB


WaitGroup
相当于Python asyncio的eventloop 都是讲线程添加到一个队列进行阻塞运行 直至运行完毕 有几个函数要注意一下

声明一个waitgroup 并制定可以放1五个线程进去

wg := sync.WaitGroup{}
wg.Add(5)

上面这样声明之后 这个waitgroup的计数器就是5了 每当我们执行一个线程 需要在后面加上

wg.Done()

done函数会使得计数器-1 计数器减少到0之后就执行完毕

使用waitgroup进行多个线程互斥访问公有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func race()  {
wg := sync.WaitGroup{}
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
fmt.Println("i", i)
wg.Done()
}()
}
wg.Wait()
}


func main() {
race()
}

输出是55555 这个结果很显然的了 就是for循环上的自增是先被执行的 后面的线程才被执行 那么所有线程输出的i都是5

可以在线程的匿名函数参数表chuan将公有变量作为参数穿进去 这样输出的结果才是对的 但是顺序肯定是乱的 因为线程的执行真的是顺序根本不确定的

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func race()  {
wg := sync.WaitGroup{}
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
fmt.Println("i", i)
wg.Done()
}(i)
}
wg.Wait()
}


func main() {
race()
}

输出结果
image_1chk5lond1u2k1vh68oj12glooq1m.png-15.8kB


select语句

golang的select跟select poll epoll相似 就是监听io操作 当思io操作发生时 触发相应的动作

注意select代码形式跟switch非常相似 但是select的case里操作语句只能是io操作

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
c1, c2 := make(chan int), make(chan int)
//var i1, i2 int
go func() {
c1 <- 12
close(c1)
}()
go func() {
c2 <- 23
close(c2)
}()
select {
case i1 := <- c1: fmt.Println("r 1", i1)
case i2 := <- c2: fmt.Println("r 2", i2)
}
}

上面的代码可以输出第一个 也可以额输出第二个 这样取决于哪个线程最先完成 select是一定要等到其中一个case有io操作 就是有数据传输过来的 再次之前一直阻塞

但是一直没有io语句执行成功 而且select含有default语句的时候 会执行default

select的作用感觉非常大 因为可以想象一个场景 一个服务器上有一个服务 这个服务有个处理入口 每个请求过来我们轮训select上每个io能否进行操作 如果有那么处理这个请求 如果没有那么去到default那里返回系统繁忙请稍后操作的提示 我们可以使用select轻易实现这个需求