1. Golang的并发模型概述
并发是现代软件开发中的一个重要概念,它可以让应用程序充分利用多核处理器提高性能。在Golang中并发模型的核心是goroutine,它是一种轻量级线程,一个goroutine在一个线程中运行,多个goroutine可以在同一个线程中并发执行。Golang中还提供了一些有用的工具来控制goroutine的并发执行,如channel和锁等。
2. Goroutine的使用
2.1 Goroutine的创建
Goroutine的创建非常简单,只需要在函数或方法前面加上go关键字即可。例如:
func main() {
go func() {
// do something
}()
}
上面的代码创建了一个goroutine,该goroutine会异步执行匿名函数的内容。使用goroutine创建的函数或方法,它们的调用者会立即返回,而不需要等待函数或方法执行完毕。下面的代码示例展示了如何在main函数中异步执行另外一个函数:
func main() {
go doSomething()
fmt.Println("main")
}
func doSomething() {
fmt.Println("something")
}
运行上面的代码,输出结果可能是:
main
something
也可能是:
something
main
这是因为两个goroutine是并行执行的,它们的输出可能是交错的。
2.2 Goroutine的调度
Goroutine的调度是由Golang的运行时系统控制的,开发者无法控制或预测哪个goroutine会被先执行或后执行。在Golang中每个goroutine都有自己的栈,栈的大小可以在运行时动态调整。当一个goroutine的栈空间不足时,运行时系统会自动扩展该栈空间。
func main() {
go func() {
fmt.Println("goroutine")
}()
fmt.Println("main")
}
运行上面的代码,输出结果可能是:
main
goroutine
也可能是:
goroutine
main
3. Channel的使用
3.1 Channel的概念
在Golang中,channel是一种基于消息传递的通信方式。它可以用于在多个goroutine之间进行同步和通信。一个channel有发送和接收两个端点,通过它们可以安全地传输数据。
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 1 // 发送1到channel中
ch <- 2 // 发送2到channel中
ch <- 3 // 发送3到channel中
close(ch)
}()
for i := range ch {
fmt.Println(i)
}
}
运行上面的代码,输出结果是:
1
2
3
3.2 Channel的类型
Golang中channel的类型是chan,可以是任意类型的数据。例如:
ch := make(chan int) // 整数类型的channel
ch := make(chan string) // 字符串类型的channel
ch := make(chan struct {x int; y int}) // 自定义类型的channel
3.3 Channel的操作
channel支持的操作有:发送(send)、接收(receive)、关闭(close)。其中发送和接收是阻塞操作,直到对方准备好之前它们都会阻塞。close可以关闭通道,当channel被关闭后,再次向它写入数据会导致panic异常,从它读取数据会返回对应类型的零值和一个标志位说明channel已被关闭。
ch := make(chan int)
ch <- 1 // 发送1到channel中
x := <-ch // 从channel中接收数据
close(ch)
当channel被关闭后,它会保留它里面的所有数据,直到通道中的所有数据被读取完毕。使用range循环可以便捷地读取channel中的数据:
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch)
}()
for i := range ch {
fmt.Println(i)
}
运行上面的代码可以看到输出结果:
1
2
3
4. 原子操作
在多个goroutine并发访问共享资源时,为了保证数据的正确性和一致性,需要采用同步机制来保障。在Golang中,原子操作是一种极为轻量级的同步机制,它可以保证数据的原子性的读取和存储。
4.1 使用atomic包
Golang中提供了atomic包来支持原子操作。它包括了一些函数,可以原子地读取和存储数据,如AddInt32、AddUint32、AddInt64、AddUint64等。
4.2 使用mutex锁进行同步
除了原子操作,Golang中还支持互斥锁来保证对共享资源的互斥访问。在访问共享资源之前先获取锁,访问完后释放锁。这样可以保证同一时刻只有一次goroutine访问共享资源,防止并发访问时发生冲突。
在Golang中可以使用sync包中的Mutex来进行互斥锁的操作,Mutex提供了两个方法:Lock和Unlock。我们在对共享资源进行访问前使用Lock方法获取锁,在访问完毕后使用Unlock方法释放锁。
package main
import (
"fmt"
"sync"
)
var mu sync.Mutex // 创建一个互斥锁
func main() {
var count int
for i := 0; i < 1000; i++ {
go func() {
mu.Lock() // 访问共享资源前先获取锁
defer mu.Unlock() // 访问完成后释放锁
count++
}()
}
fmt.Println(count)
}
上面的代码会启动1000个goroutine来进行count的加1操作。由于count变量是共享资源,会存在并发访问的情况。使用Mutex锁保证对count的访问是互斥的。
5. 总结
Golang的并发模型提供了非常简单和有效的方法来实现并行编程。通过使用goroutine和channel,开发者可以轻松地实现并发控制和数据通信。通过使用原子操作和互斥锁,可以在多个goroutine并发访问共享资源时保证数据的正确性和一致性。在实际开发中,我们可以根据实际情况选择合适的并发控制方式来满足应用的需求。