goroutine的使用
在编写 Socket 网络程序时,需要提前准备一个线程池为每一个 Socket 的收发包分配一个线程。开发人员需要在线程数量和 CPU 数量间建立一个对应关系,以保证每个任务能及时地被分配到 CPU 上进行处理,同时避免多个任务频繁地在线程间切换执行而损失效率。
虽然,线程池为逻辑编写者提供了线程分配的抽象机制。但是,如果面对随时随地可能发生的并发和线程处理需求,线程池就不是非常直观和方便了。能否有一种机制:使用者分配足够多的任务,系统能自动帮助使用者把任务分配到 CPU 上,让这些任务尽量并发运作。这种机制在 Go语言中被称为goroutine。
goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。
Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。
# 1. 创建协程 goroutine
# 使用普通函数创建 goroutine
Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。
# 格式
为一个普通函数创建 goroutine 的写法如下:
go 函数名( 参数列表 )
函数名:要调用的函数名。
参数列表:调用函数需要传入的参数。
使用 go 关键字创建 goroutine 时,被调用函数的返回值会被忽略。如果需要在 goroutine 中返回数据,请使用后面介绍的通道(channel)特性,通过通道把数据从 goroutine 中作为返回值传出。
# 例子
package main
import (
"fmt"
"strconv"
"time"
)
func test() {
for i := 1; i <= 5; i++ {
fmt.Println("hello golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() { // main主线程
go test() // go test() 开启协程
for i := 1; i <= 5; i++ {
fmt.Println("hello msb" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
/*
hello msb1
hello golang1
hello golang2
hello msb2
hello msb3
hello golang3
hello golang4
hello msb4
hello msb5
hello golang5
*/
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
# 使用匿名函数创建goroutine
go 关键字后也可以为匿名函数或闭包启动 goroutine。
# 格式
使用匿名函数或闭包创建 goroutine 时,除了将函数定义部分写在 go 的后面之外,还需要加上匿名函数的调用参数,格式如下:
go func( 参数列表 ){
函数体
}( 调用参数列表 )
2
3
其中:
参数列表:函数体内的参数变量列表。
函数体:匿名函数的代码。
调用参数列表:启动 goroutine 时,需要向匿名函数传递的调用参数。
# 例子
package main
import (
"fmt"
"strconv"
"time"
)
func main() { // main主线程
// 方式二: 使用匿名函数
go func() {
for i := 1; i <= 5; i++ {
fmt.Println("hello golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}()
for i := 1; i <= 5; i++ {
fmt.Println("hello msb" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
/*
hello msb1
hello golang1
hello golang2
hello msb2
hello msb3
hello golang3
hello golang4
hello msb4
hello msb5
hello golang5
*/
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
# 2. 启动多个协程
# 创建多个协程使用闭包问题
如果用了匿名函数,匿名函数会把使用的变量放到全局作用域中。所有导致输出的i有问题
package main
import (
"fmt"
"time"
)
func main() { // main主线程
// 创建多个协程(这里创建了5个)
// 问题点一: 如果用了匿名函数,匿名函数会把使用的变量放到全局作用域中。所有导致输出的i有问题
for i := 1; i <= 5; i++ {
go func() {
fmt.Printf("%d \t", i) // 4 6 6 6 6
}()
}
fmt.Println("--------")
for i := 1; i <= 5; i++ {
go func(n int) { // 解决方式常用值传递的方式可解决上面的问题
fmt.Printf("%d \t", n) // 3 4 6 1 6
}(i)
}
// 主死从随: 主线程退出了,则协程即使还没有执行完毕,也会退出
time.Sleep(time.Second * 3)
}
/*
--------
5 6 2 6 6 3 4 6 1 6
*/
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
# 3. 主死从随
如果主线程退出了,则协程即使还没有执行完毕,也会退出
当然协程也可以在主线程没有退出前,就自己结束了,比如完成了自己的任务
package main
import (
"fmt"
"strconv"
"time"
)
func main() { // main主线程
go func() {
for i := 1; i <= 100; i++ {
fmt.Println("hello golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}()
for i := 1; i <= 2; i++ {
fmt.Println("hello msb" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
/*
hello golang1
hello msb1
hello golang2
hello msb2 // 主线程结束 协程未执行完成也结束了
*/
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
# 4. 使用WaitGroup解决主死从随
# WaitGroup的作用
WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。---》解决主线程在子协程结束后自动结束
计数为0才会正常执行否则报错 fatal error: all goroutines are asleep - deadlock!
# 主要方法
# 案例
# Add\Done\Wait
一、定义WaitGroup
二、每次创建一个协程调用Add()计数加一
三、协程调用完毕计数减一(内部其实就是用的Add(-1))
四、使用Wait阻塞: 主线程一直在阻塞,什么时候wg减为0了,就停止
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup // 一、定义WaitGroup
func main() { // main主线程
for i := 1; i <= 5; i++ {
wg.Add(1) // 二、每次创建一个协程调用Add()计数加一
go func(n int) {
fmt.Println(n)
wg.Done() // 三、协程调用完毕计数减一(内部其实就是用的Add(-1))
}(i)
}
// 四、使用Wait阻塞: 主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
fmt.Println("主线程执行完毕")
}
/*
5
2
4
1
3
主线程执行完毕
*/
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
# 如果防止忘记计数器减1操作,结合defer关键字使用:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup // 一、定义WaitGroup
func main() { // main主线程
for i := 1; i <= 5; i++ {
wg.Add(1) // 二、每次创建一个协程调用Add()计数加一
go func(n int) {
defer wg.Done()
fmt.Println(n)
//wg.Done() // 三、协程调用完毕计数减一(内部其实就是用的Add(-1))
}(i)
}
// 四、使用Wait阻塞: 主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
fmt.Println("主线程执行完毕")
}
/*
5
2
4
1
3
主线程执行完毕
*/
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
# 可以最开始在知道协程次数的情况下先Add操作
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup // 一、定义WaitGroup
func main() { // main主线程
wg.Add(5)
for i := 1; i <= 5; i++ {
//wg.Add(1) // 二、每次创建一个协程调用Add()计数加一
go func(n int) {
defer wg.Done()
fmt.Println(n)
//wg.Done() // 三、协程调用完毕计数减一(内部其实就是用的Add(-1))
}(i)
}
// 四、使用Wait阻塞: 主线程一直在阻塞,什么时候wg减为0了,就停止
wg.Wait()
fmt.Println("主线程执行完毕")
}
/*
5
2
4
1
3
主线程执行完毕
*/
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
# 5. 锁(多个协程操作同一数据)
Go语言互斥锁(sync.Mutex)和读写互斥锁(sync.RWMutex) (opens new window)
# 当多个协程操纵同一数据会出现问题
# 案例
package main
import (
"fmt"
"sync"
)
// 定义一个变量:
var totalNum int
var wg1 sync.WaitGroup //只定义无需赋值
func add() {
defer wg1.Done()
for i := 0; i < 100000; i++ {
totalNum = totalNum + 1
}
}
func sub() {
defer wg1.Done()
for i := 0; i < 100000; i++ {
totalNum = totalNum - 1
}
}
func main() {
wg1.Add(2)
//启动协程
go add()
go sub()
wg1.Wait()
fmt.Println(totalNum) // 当多个协程出现数据共享是就会出错
}
/*
19751
*/
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
# 问题出现的原因:(图解为其中一种可能性)
# 解决问题
有一个机制:确保:一个协程在执行逻辑的时候另外的协程不执行 ----》锁的机制---》加入互斥锁
# Mutex 互斥锁
其中Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别 ----性能、效率相对来说比较低
使用步骤:
定义互斥锁 sync.Mutex
合适位置加锁 Lock
合适位置释放锁(释放锁的位置不正确会导致死锁)Unlock
package main
import (
"fmt"
"sync"
)
// 定义一个变量:
var num int
var wg2 sync.WaitGroup //只定义无需赋值
// 一、定义互斥锁
var lock sync.Mutex // 加入互斥锁
func add1() {
defer wg2.Done()
for i := 0; i < 100000; i++ {
lock.Lock() // 二、合适位置加锁
num = num + 1
lock.Unlock() // 三、合适位置释放锁(释放锁的位置不正确会导致死锁)
}
}
func sub1() {
defer wg2.Done()
for i := 0; i < 100000; i++ {
lock.Lock() // 加锁
num = num - 1
lock.Unlock() // 释放锁
}
}
func main() {
wg2.Add(2)
//启动协程
go add1()
go sub1()
wg2.Wait()
fmt.Println(num) // 当多个协程出现数据共享是就会出错
}
/*
0
*/
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
41
# RWMutex 读写锁
RWMutex是一个读写锁,其经常用于读次数远远多于写次数的场景. ---在读的时候,数据之间不产生影响, 写和读之间才会产生影响
package main
import (
"fmt"
"sync"
"time"
)
var wg3 sync.WaitGroup //只定义无需赋值
// 加入读写锁:
var lock3 sync.RWMutex
func read() {
defer wg3.Done()
lock3.RLock() //如果只是读数据,那么这个锁不产生影响,但是读写同时发生的时候,就会有影响
fmt.Println("开始读取数据")
time.Sleep(time.Second)
fmt.Println("读取数据成功")
lock3.RUnlock()
}
func write() {
defer wg3.Done()
lock3.Lock()
fmt.Println("开始修改数据")
//time.Sleep(time.Second * 10)
fmt.Println("修改数据成功")
lock3.Unlock()
}
func main() {
wg3.Add(6)
//启动协程 ---> 场合:读多写少
for i := 0; i < 2; i++ {
go read()
}
go write()
wg3.Wait()
}
/*
开始修改数据
修改数据成功
开始读取数据
开始读取数据
读取数据成功
读取数据成功
*/
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
41
42
43
44
45