在传统的编程语言中,如C++、Java、Python等,其并发逻辑多建立在操作系统线程之上。线程间的通信通常依赖于操作系统提供的基础原语,包括共享内存、信号、管道、消息队列及套接字等,其中共享内存是最为普遍的通信方式。但这种基于共享内存的并发模型在复杂或大规模业务场景下往往显得复杂且易于出错。
Go语言在设计时即以解决传统并发问题为目标,融入了CSP(Communicating Sequential Processes,通信顺序进程)模型的理念。CSP模型致力于简化并发编程,目标是让编写并发程序的难度与顺序程序相当。
在CSP模型中,通信和同步通过一种特定的流程实现:生产者产生数据,然后通过输出数据到输入/输出原语,最终到达消费者。Go语言为实现CSP模型,特别引入了Channel机制。Goroutine可以通过Channel进行数据的读写操作,Channel作为连接多个Goroutine的通信桥梁,简化了并发编程的复杂性。
虽然CSP模型在Go语言中占据主流地位,但Go同样支持基于共享内存的并发模型。在Go的sync
包中,提供了包括互斥锁、读写锁、条件变量和原子操作等在内的多种同步机制,以满足不同并发场景下的需求。
互斥锁(Mutex)是一种用于在并发环境中安全访问共享资源的机制。当一个协程获取到锁时,它将拥有临界区的访问权,而其他请求该锁的协程将会阻塞,直到该锁被释放。
并发访问共享资源的情形非常普遍,例如:
如果没有互斥锁的控制,将会导致商品超卖、变量数值不正确、用户信息更新错误等问题。这时候就需要使用互斥锁来控制并发访问。
Mutex实现了Locker接口,提供了两个方法:Lock
和Unlock
。
package mainimport ( "fmt" "sync" "time")func main() { var mu sync.Mutex var count int increment := func() { mu.Lock() defer mu.Unlock() count++ fmt.Println("Count:", count) } for i := 0; i < 5; i++ { go increment() } time.Sleep(time.Second)}
func example() { var mu sync.Mutex mu.Lock() defer mu.Unlock() // Do something... mu.Lock() // 死锁}
func example() { var mu sync.Mutex mu.Lock() // 未调用mu.Unlock() mu.Unlock() // 正确}
func example() { var mu sync.Mutex copyMu := mu copyMu.Lock() // 错误}
Mutex结构体有两个字段:state和sema。
type Mutex struct { state int32 sema uint32}
Mutex有以下四种状态。
以下代码展示了如何使用Mutex在并发环境中安全地访问共享资源:
package mainimport ( "fmt" "sync" "time")func main() { var mu sync.Mutex var count int increment := func() { mu.Lock() defer mu.Unlock() count++ fmt.Println("Count:", count) } for i := 0; i < 5; i++ { go increment() } time.Sleep(time.Second)}
在上述代码中,多个goroutine同时调用increment函数,通过Mutex来确保对共享变量count的访问是安全的。
在并发编程中,为了保证多个协程安全地访问共享资源,我们通常使用Mutex互斥锁。然而,在读多写少的场景下,Mutex会导致性能问题,因为所有操作(包括读操作)都必须串行进行。为了解决这一问题,可以区分读操作和写操作。RWMutex是一种读写锁,同一时间只能被一个写操作持有,或者被多个读操作持有。
RWMutex提供了五个方法:Lock、Unlock、RLock、RUnlock和RLocker。
package mainimport ( "fmt" "sync" "time")func main() { var rw sync.RWMutex var count int write := func() { rw.Lock() defer rw.Unlock() count++ fmt.Println("Write:", count) } read := func() { rw.RLock() defer rw.RUnlock() fmt.Println("Read:", count) } // Start multiple readers for i := 0; i < 5; i++ { go read() } // Start a single writer go write() time.Sleep(time.Second)}
RWMutex主要通过readerCount字段来维护读锁的数量。写操作时,会将readerCount减去2的30次方变成一个负数,从而阻塞新的读锁请求。当写锁被释放时,将readerCount加上2的30次方,恢复成一个整数并唤醒等待中的读锁操作。
RWMutex的易错场景和Mutex类似,包括以下几点。
func example() { var rw sync.RWMutex rw.Lock() defer rw.Unlock() // Do something... rw.Lock() // 死锁}
func example() { var rw sync.RWMutex rw.Lock() // 未调用rw.Unlock() rw.Unlock() // 正确}
func example() { var rw sync.RWMutex copyRw := rw copyRw.Lock() // 错误}
package mainimport ( "fmt" "sync" "time")func main() { var rw sync.RWMutex var count int write := func() { rw.Lock() defer rw.Unlock() count++ fmt.Println("Write:", count) } read := func() { rw.RLock() defer rw.RUnlock() fmt.Println("Read:", count) } // 启动多个读操作 for i := 0; i < 5; i++ { go read() } // 启动写操作 go write() time.Sleep(time.Second)}
在上述代码中,多个goroutine同时调用read函数,通过RWMutex来确保对共享变量count的读取是安全的。同时,write函数用于更新共享变量count,确保在写操作时独占访问权。
死锁指的是一组进程由于相互持有和等待资源,导致无法继续执行的状态。在这种情况下,所有相关的进程都会无限期阻塞,无法向前推进。具体来说,死锁发生在一个进程持有某些资源并等待其他进程释放其占有的资源,同时这些其他进程也在等待第一个进程释放资源,形成相互等待的状态。
死锁的发生需要满足以下四个必要条件。
为了解决死锁问题,可以采取以下两种策略。
以下是一个Go语言中的死锁示例,展示了两个goroutine由于相互等待对方持有的资源而导致的死锁:
package mainimport ( "fmt" "sync")func main() { var mutexA, mutexB sync.Mutex go func() { mutexA.Lock() fmt.Println("Goroutine 1: Locked mutexA") // Simulate some work mutexB.Lock() fmt.Println("Goroutine 1: Locked mutexB") mutexB.Unlock() mutexA.Unlock() }() go func() { mutexB.Lock() fmt.Println("Goroutine 2: Locked mutexB") // Simulate some work mutexA.Lock() fmt.Println("Goroutine 2: Locked mutexA") mutexA.Unlock() mutexB.Unlock() }() // Wait for goroutines to finish (they won't due to deadlock) select {}}
在上述代码中,两个goroutine分别持有mutexA
和mutexB
,并且尝试获取对方的锁,导致死锁发生。每个goroutine无限期等待对方释放资源,形成相互等待的循环。
通过了解死锁的概念、必要条件及解决策略,我们可以更好地设计并发程序,避免陷入死锁状态。
WaitGroup 是 Go 语言的 sync 包下提供的一种并发原语,用来解决并发编排的问题。它主要用于等待一组 goroutine 完成。假设一个大任务需要等待三个小任务完成才能继续执行,如果采用轮询的方法,可能会导致两个问题:一是小任务已经完成但大任务需要很久才能被轮询到,二是轮询会造成 CPU 资源的浪费。因此,WaitGroup 通过阻塞等待并唤醒大任务的 goroutine 来解决这个问题。
WaitGroup 提供了三个方法:Add、Done 和 Wait。
WaitGroup 维护了两个计数器,一个是 v 计数器,另一个是 w 计数器。
使用 WaitGroup 需要注意以下易错场景:
以下是一个使用 WaitGroup 的示例代码:
package mainimport ( "fmt" "sync" "time")func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Done() 方法用于减少计数器 fmt.Printf("Worker %d starting/n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done/n", id)}func main() { var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) // Add() 方法增加计数器 go worker(i, &wg) } wg.Wait() // Wait() 方法阻塞等待所有计数器为 0 fmt.Println("All workers done")}
在上述代码中,main 函数创建了一个 WaitGroup 并启动了三个 goroutine,每个 goroutine 执行 worker 函数。在 worker 函数中,调用 wg.Done() 方法表示当前工作已经完成。main 函数中的 wg.Wait() 方法阻塞等待,直到所有的 goroutine 都完成工作并调用了 Done 方法。
WaitGroup 是 Go 语言中非常有用的并发原语,用于等待一组 goroutine 完成。通过合理使用 Add、Done 和 Wait 方法,可以避免轮询等待带来的性能问题,并提高并发编排的效率。在使用 WaitGroup 时,需要注意计数器的增减操作,避免引发 panic 或长时间阻塞。
Go 语言提倡通过通信来实现共享内存,而不是通过共享内存来通信。Go 的 CSP(Communicating Sequential Processes)并发模型正是通过 Goroutine 和 Channel 来实现的。Channel 是 Go 语言中用于 goroutine 之间通信的主要工具。
Channel 有以下几类应用场景。
Channel 有三种类型:
Channel 通过 make 函数进行初始化,未初始化的 Channel 的零值是 nil,对 nil 的 Channel 进行接收或发送操作会导致阻塞。
Channel 可以分为有缓冲和无缓冲两种。无缓冲的 Channel 是同步的,有缓冲的 Channel 是异步的。发送操作只有在 Channel 满时才会阻塞,接收操作只有在 Channel 为空时才会阻塞。
发送操作是 chan<-,接收操作是 <-chan。接收数据时可以返回两个值,第一个是元素,第二个是一个布尔值,若为 false 则说明 Channel 已经被关闭并且 Channel 中没有缓存的数据。
Go 的内建函数 close、cap、len 都可以操作 Channel 类型,发送和接收都可以作为 select 语句的 case,Channel 也可以应用于 for range 语句。
在发送数据给 Channel 时,发送语句会转化为 chansend 函数:
在接收数据时,接收语句会转化为 chanrecv 函数:
以下是一个使用 Channel 的示例代码:
package mainimport ( "fmt" "time")// 生产者:生成数据并发送到 channelfunc producer(ch chan<- int, count int) { for i := 0; i < count; i++ { ch <- i fmt.Println("Produced:", i) time.Sleep(time.Millisecond * 500) } close(ch) // 关闭 channel,表示生产结束}// 消费者:从 channel 接收数据并处理func consumer(ch <-chan int) { for data := range ch { fmt.Println("Consumed:", data) time.Sleep(time.Millisecond * 1000) }}func main() { ch := make(chan int, 5) // 创建一个带缓冲的 channel go producer(ch, 10) // 启动生产者 consumer(ch) // 启动消费者}
在上述代码中,main 函数创建了一个带缓冲的 Channel,并启动了一个生产者 goroutine 和一个消费者 goroutine。生产者不断生成数据并发送到 Channel 中,消费者从 Channel 中接收数据并进行处理。生产者完成后关闭 Channel,消费者则在接收到所有数据后结束。
本文链接:http://www.28at.com/showinfo-26-112752-0.htmlGo并发编程详解锁、WaitGroup、Channel
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com