golang是一门天生支持并发的编程语言,它提供了丰富而简单的并发编程原语。下面是Go语言中实现并发的一些特性
- Goroutine(协程):Goroutine是Go语言中轻量级的执行单元,由Go运行时环境调度和管理。通过关键字
go可以启动一个Goroutine,并发地执行函数或方法。 - Channel(通道):Channel是用于Goroutine之间进行安全的数据交换和同步的机制。通过Channel,可以在Goroutine之间发送和接收数据。Channel提供了阻塞式的操作,可以保证数据的同步和顺序性。
- Mutex(互斥锁):Mutex是Go语言中的互斥量,用于保护共享资源的访问。通过互斥锁,可以在不同的Goroutine之间实现对共享资源的互斥访问,防止竞态条件的发生。
- WaitGroup(等待组):WaitGroup用于等待一组Goroutine的完成。通过WaitGroup,可以等待所有的Goroutine执行完毕后再继续执行后续操作。
一、goroutine
Goroutine(协程)是Go语言并发编程的核心概念之一。它是一种轻量级的执行单元,由Go运行时环境(runtime)调度和管理。与传统的线程相比,Goroutine具有以下特点:
- 轻量级:Goroutine的创建和销毁开销非常小,可以创建成千上万个Goroutine而不会造成系统资源的浪费。
- 并发性:Goroutine可以同时运行在多个逻辑处理器(CPU核心)上,实现真正的并行计算。
- 通信机制:Goroutine之间通过通道(Channel)进行安全的数据交换和同步,避免了传统线程享内存带来的同步问题。
- 自动调度:Go运行时环境负责Goroutine的调度和切换,开发者无需手动管理线程池或锁,简化了并发编程的复杂性。
使用Goroutine非常简单,只需在函数或方法调用前加上关键字go即可启动一个Goroutine。例如:
func main() {
go other() // 启动一个新的协程并并发执行函数
// 执行其他操作
}
func other() {
// 函数内容
}
在上述例子中,other()函数将在一个独立的Goroutine中并发执行,不会阻塞main()函数的执行。可以同时启动多个Goroutine来实现并行计算或并发处理任务。
二、channel
Channel(通道)是Go语言中用于在Goroutine之间进行安全的数据交换和同步的机制。它可以用于在不同的Goroutine之间发送和接收数据,并提供了阻塞式的操作,以保证数据的同步和顺序性。
在Go语言中,通过内置的make函数来创建一个Channel,并指定所传递数据的类型。例如,创建一个传递整数类型数据的Channel:
ch := make(chan int)
创建Channel后,可以使用箭头操作符<-进行数据的发送和接收。发送数据使用<-操作符将数据发送到Channel中,接收数据则使用<-操作符从Channel中接收数据。
以下是使用Channel进行数据交换的示例:
func main() {
tongdao := make(chan string) // 创建一个字符串类型的通道
//开启并发
go func() {
tongdao <- "Hello, World!" // 发送数据到Channel
}()
data := <-tongdao // 从Channel接收数据
fmt.Println(data) // 输出:Hello, World!
}
在上述示例中,通过go关键字启动了一个新的Goroutine,在该Goroutine中将字符串"Hello, World!"发送到Channel中。然后,通过msg := <-ch语句从Channel中接收数据,并将其赋值给变量msg,最后打印出来。
Channel的特点:
- Channel是类型安全的,只能传递指定类型的数据。
- Channel是阻塞的,当向一个Channel发送数据时,如果没有接收方准备好接收数据,发送操作会被阻塞。同样,当从一个Channel接收数据时,如果没有发送方准备好发送数据,接收操作也会被阻塞。
- Channel的默认行为是先进先出(FIFO),保证发送和接收的顺序性。
- 可以通过关闭Channel来表示不再发送任何数据。接收方可以通过检查返回值的第二个参数来判断Channel是否已关闭。
使用Channel可以有效地在不同的Goroutine之间进行通信和同步,避免了竞态条件和数据冲突,使得并发编程更加安全和可靠。
需要注意的是,Goroutine之间的并发访问共享数据时,需要采取适当的同步机制,如使用通道进行数据交换、互斥锁等,以避免竞态条件和数据冲突。
三、Mutex互斥锁
互斥锁(Mutex)是一种常用的同步机制,用于在并发编程中保护共享资源的访问。它提供了一种独占的方式,确保同一时间只有一个线程可以访问被保护的代码块。
在Go语言中,可以使用sync包中的Mutex类型来创建和使用互斥锁。具体而言,可以通过调用Lock()方法来获取锁,并在需要时调用Unlock()方法来释放锁。
下面是一个例子
import (
"sync"
"time"
)
// 并发安全,对同一个数据同时操作导致操作丢失。加锁,lock
var (
x int
lock sync.Mutex
) //这里定义两个全局变量
func main() {
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
//休眠是为了等待协程完成它的任务
time.Sleep(time.Second)
println("withlock", x)
x = 0
//不加锁有时会出现错误结果
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("withoutlock", x)
}
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x = x + 1
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x = x + 1
}
}
四、WaitGroup等待组
sync.WaitGroup是Go语言中的另一个并发原语,它用于等待一组Goroutine完成执行。
WaitGroup提供了三个方法来管理计数器:Add()、Done()和Wait()。
Add(delta int):增加计数器的值,delta为正整数表示增加的数量。 Done():减少计数器的值,相当于Add(-1)。 Wait():阻塞当前Goroutine,直到计数器归零。
下面是一个简单的示例,展示了如何使用WaitGroup等待一组Goroutine完成执行:
import (
"fmt"
"sync"
)
//WaitGroup,替代sleep实现同步
func main() {
GoWaitGroup()
}
func GoWaitGroup() {
//引入
var wg sync.WaitGroup
//开启五个协程
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
//这个方法让计数器减一,表明这个协程已经完成
defer wg.Done()
hello(j)
}(i)
}
//进行阻塞主协程,直到计数器为0
wg.Wait()
}
func hello(i int) {
println("协程" + fmt.Sprint(i))
}