Go多线程(协程)与延迟执行、线程安全

前言

本文所说的线程指的是Go语言的goroutine,也就是协程。

多线程

Go语言天生支持多线程编程,Go语言的goroutine是一种轻量级线程实现,可以在同一个进程中并发执行多个任务,同时又能保证数据安全。

启动一个goroutine很简单,只需要在函数前加上关键字go,就可以让这个函数在一个新的goroutine中运行。下面是一个简单的例子:

1
2
3
4
5
6
func main() {
go func() {
fmt.Println("Hello from a goroutine")
}()
fmt.Println("Hello from main")
}

上面的代码会输出:

1
2
Hello from main
Hello from a goroutine

注意到输出的顺序可能不是按照代码的顺序来的,因为两个goroutine是并发执行的。

Go语言还提供了一些同步机制,如channel、锁等,来保证多个goroutine之间的协调和同步。这些机制在多线程编程中非常重要,可以避免数据竞争等问题,保证程序的正确性和可靠性。

延迟执行

在Go语言中,可以使用时间.After和定时器来延迟执行函数。

时间.After会在指定时间后发送当前时间到返回的channel中,我们可以使用它来延迟执行函数:

1
2
3
4
go func() {
time.After(5 * time.Second)
sayHello()
}()

5秒后,会在新的goroutine中调用sayHello函数。

使用定时器,我们可以重复延迟执行函数:

1
2
3
4
5
6
7
8
timer := time.NewTimer(5 * time.Second)
go func() {
for {
<-timer.C
sayHello()
timer.Reset(5 * time.Second)
}
}()

定时器的C channel会在定时时间后发送当前时间,我们通过channel接收时间,执行函数,然后重置定时器,这样就实现了重复延迟执行。

另外,我们也可以使用time.Tick实现重复延迟:

1
2
3
4
5
6
ticker := time.NewTicker(5 * time.Second)
go func() {
for t := range ticker.C {
sayHello()
}
}()

time.Tick会每隔一定时间就发送当前时间到返回的channel中。

综上,Go语言提供了三种延迟执行函数的方法:

  1. time.After: 单次延迟,在指定时间后执行函数
  2. 定时器:可以重复延迟执行函数
  3. time.Tick: 可以按固定时间间隔重复执行函数使用这些方法,我们可以在Go语言中实现各种延迟调度和定时任务。

线程安全

多线程操作切片

在 Go 语言中,可以通过 goroutine 来实现多线程操作同一个切片。

但需要注意的是,在多个 goroutine 中同时操作同一个切片时,可能会出现竞争条件(race condition),因此需要使用互斥锁(Mutex)来保护共享资源,以避免数据竞争问题。

主要使用Mutex实现

1
2
3
4
5
var mutex sync.Mutex

//多线程操作的时候
mutex.Lock()
defer mutex.Unlock()

以下是一个示例代码,演示如何在多个 goroutine 中操作同一个切片并使用互斥锁保护:

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

import (
"fmt"
"sync"
)

func main() {
var slice []int
var mutex sync.Mutex

var wg sync.WaitGroup
wg.Add(numGoroutines)

for i := 0; i < 5; i++ {
go func() {
mutex.Lock()
defer mutex.Unlock()

// 操作共享切片
for j := 0; j < 3; j++ {
slice = append(slice, j)
}

fmt.Println("Goroutine done")
wg.Done()
}()
}

wg.Wait()

fmt.Println("Final slice:", slice)
}

在上面的示例中,我们定义了一个切片 slice 和一个互斥锁 mutex,然后启动了 5 个 goroutine 并发地向切片中追加数据。

在每个 goroutine 中对切片的修改都需要先获取互斥锁,然后在操作完成后释放互斥锁。

最后等待所有 goroutine 执行完成后输出最终的切片内容。

线程池

在 Go 语言中,可以通过使用 goroutine 和 channel 来实现线程池的功能。

线程池可以帮助有效地管理 goroutine 的数量,避免无限制地启动大量的 goroutine,从而提高系统的性能和稳定性。

自定义线程池

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
41
package utils

import (
"sync"
)

type WorkerFunc func()

type GoroutinePool struct {
maxWorkers int
tasks chan WorkerFunc
wg sync.WaitGroup
}

func NewGoroutinePool(maxWorkers int) *GoroutinePool {
return &GoroutinePool{
maxWorkers: maxWorkers,
tasks: make(chan WorkerFunc),
}
}

func (p *GoroutinePool) Run(worker WorkerFunc) {
p.tasks <- worker
}

func (p *GoroutinePool) Start() {
for i := 0; i < p.maxWorkers; i++ {
p.wg.Add(1)
go func() {
defer p.wg.Done()
for task := range p.tasks {
task()
}
}()
}
}

func (p *GoroutinePool) Wait() {
close(p.tasks)
p.wg.Wait()
}

使用

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

import (
"fmt"
"testing"
"xh-control-ws/utils"
)

func TestPool01(t *testing.T) {
pool := utils.NewGoroutinePool(3)
pool.Start()
// 执行一些任务
for i := 1; i <= 5; i++ {
taskID := i
pool.Run(func() {
fmt.Printf("Task %d is running\n", taskID)
})
}

pool.Wait()
fmt.Println("所有的任务执行结束")
}

chan

在 Go 语言中,chan 是用于在不同 goroutine 之间进行通信的一种数据结构。

chan(通道)并不完全是一个队列,但是它可以用于实现队列的功能,但它更广泛地用于在 goroutine 之间安全地传递数据。

chan 可以用来在 goroutine 之间传递数据,实现协程之间的同步和通信。

在 Go 中,使用 make 函数可以创建一个 chan,语法如下:

1
ch := make(chan int)

上面的代码创建了一个 chan,这个 chan 只能传递整数类型的数据。

你也可以创建传递其他类型的数据,比如字符串、结构体等。

chan 可以用于发送和接收数据,发送数据可以使用 <- 操作符,接收数据也可以使用 <- 操作符。

下面是一个简单的示例:

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 int)

// 发送数据
go func() {
ch <- 10
}()

// 接收数据
data := <-ch
fmt.Println(data) // 输出: 10
}

在这个示例中,我们创建了一个整数类型的 chan,并在一个 goroutine 中向 chan 发送了数字 10,然后在主 goroutine 中接收这个数据并打印出来。

通过 chan 实现的通信机制可以帮助不同的 goroutine 之间进行数据交换和同步,是 Go 语言并发编程中的重要组成部分。