共计 7230 个字符,预计需要花费 19 分钟才能阅读完成。
1. 简介
本文探讨了并发编程中的同步操作,讲述了为何须要同步以及两种常见的实现形式:sync.Cond
和通道。通过比拟它们的实用场景,读者能够更好地理解何时抉择应用不同的同步形式。本文旨在帮忙读者了解同步操作的重要性以及抉择适合的同步机制来确保多个协程之间的正确协调和数据共享的一致性。
2. 为什么须要同步操作
2.1 为什么须要同步操作
这里举一个简略的图像处理场景来阐明。工作 A 负责加载图像,工作 B 负责对已加载的图像进行解决。这两个工作将在两个并发协程中同时启动,实现并行执行。然而,这两个工作之间存在一种依赖关系:只有当图像加载实现后,工作 B 能力平安地执行图像处理操作。
在这种状况下,咱们须要对这两个工作进行协调和同步。工作 B 须要确保在解决已加载的图像之前,工作 A 曾经实现了图像加载操作。通过应用适当的同步机制来确保工作 B 在图像准备就绪后再进行解决,从而防止数据不一致性和并发拜访谬误的问题。
事实上,在咱们的开发过程中,常常会遇到这种须要同步的场景,所以理解同步操作的实现形式是必不可少的,上面咱们来认真介绍。
2.2 如何实现同步操作呢
通过下面的例子,咱们晓得当多协程工作 存在依赖关系 时,同步操作是必不可免的,那如何实现同步操作呢? 这里的一个简略想法,便是采纳一个简略的条件变量,一直采纳轮询的形式来查看事件是否曾经产生或条件是否满足,此时便可实现简略的同步操作。代码示例如下:
package main
import (
"fmt"
"time"
)
var condition bool
func waitForCondition() {
for !condition {
// 轮询条件是否满足
time.Sleep(time.Millisecond * 100)
}
fmt.Println("Condition is satisfied")
}
func main() {go waitForCondition()
time.Sleep(time.Second)
condition = true // 批改条件
time.Sleep(time.Second)
}
在上述代码中,waitForCondition
函数通过轮询形式查看条件是否满足。当条件满足时,才继续执行上来。
然而这种轮训的形式其实存在一些毛病,首先是资源节约,轮询会耗费大量的 CPU 资源,因为协程须要一直地执行循环来查看条件。这会导致 CPU 使用率升高,节约系统资源,其次是提早,轮询形式无奈及时响应条件的变动。如果条件在循环的某个工夫点满足,但轮询查看的时机未到,则会提早对条件的响应。最初轮询形式可能导致协程的执行效率升高。因为协程须要在循环中一直查看条件,无奈进行其余有意义的工作。
既然通过轮训一个条件变量来实现同步操作存在这些问题。那 go 语言中,是否存在更好的实现形式,能够防止轮询形式带来的问题,提供更高效、及时响应的同步机制。其实是有的,sync.Cond
和 channel
便是两个能够实现同步操作的原语。
3. 实现形式
3.1 sync.Cond 实现同步操作
应用 sync.Cond
实现同步操作的办法,能够参考 sync.Cond 这篇文章,也能够依照能够依照以下步骤进行:
- 创立一个条件变量:应用
sync.NewCond
函数创立一个sync.Cond
类型的条件变量,并传入一个互斥锁作为参数。 - 在期待条件满足的代码块中应用
Wait
办法:在须要期待条件满足的代码块中,调用条件变量的Wait
办法,这会使以后协程进入期待状态,并开释之前获取的互斥锁。 - 在满足条件的代码块中应用
Signal
或Broadcast
办法:在满足条件的代码块中,能够应用Signal
办法来唤醒一个期待的协程,或者应用Broadcast
办法来唤醒所有期待的协程。
上面是一个简略的例子,演示如何应用 sync.Cond
实现同步操作:
package main
import (
"fmt"
"sync"
"time"
)
func main() {var cond = sync.NewCond(&sync.Mutex{})
var ready bool
// 期待条件满足的协程
go func() {fmt.Println("期待条件满足...")
cond.L.Lock()
for !ready {cond.Wait()
}
fmt.Println("条件已满足")
cond.L.Unlock()}()
// 模仿一段耗时的操作
time.Sleep(time.Second)
// 扭转条件并告诉期待的协程
cond.L.Lock()
ready = true
cond.Signal()
cond.L.Unlock()
// 期待一段时间,以便察看后果
time.Sleep(time.Second)
}
在下面的例子中,咱们创立了一个条件变量 cond
,并定义了一个布尔型变量ready
作为条件。在期待条件满足的协程中,通过调用 Wait
办法期待条件的满足。在主协程中,通过扭转条件并调用 Signal
办法来告诉期待的协程条件已满足。在期待协程被唤醒后,输入 ” 条件已满足 ” 的音讯。
通过应用sync.Cond
,咱们实现了一个简略的同步操作,确保期待的协程在条件满足时才会继续执行。这样能够防止了不必要的轮询和资源节约,进步了程序的效率。
3.2 channel 实现同步操作
当应用通道(channel)实现同步操作时,能够利用通道的阻塞个性来实现协程之间的同步。上面是一个简略的例子,演示如何应用通道实现同步操作:
package main
import (
"fmt"
"time"
)
func main() {
// 创立一个用于同步的通道
done := make(chan bool)
// 在协程中执行须要同步的操作
go func() {fmt.Println("执行一些操作...")
time.Sleep(time.Second)
fmt.Println("操作实现")
// 向通道发送信号,示意操作已实现
done <- true
}()
fmt.Println("期待操作实现...")
// 阻塞期待通道接管到信号
<-done
fmt.Println("操作已实现")
}
在下面的例子中,咱们创立了一个通道 done
,用于同步操作。在执行须要同步的操作的协程中,首先执行一些操作,而后通过向通道发送数据done <- true
来示意操作已实现。在主协程中,咱们应用 <-done
来阻塞期待通道接管到信号,示意操作已实现。
通过应用通道实现同步操作,咱们利用了通道的阻塞个性,确保在操作实现之前,主协程会始终期待。一旦操作实现并向通道发送了信号,主协程才会继续执行后续的代码。基于此实现了同步操作。
3.3 实现形式回顾
从下面的介绍来看,sync.Cond
或者 channel
都能够用来实现同步操作。
但因为它们是不同的并发原语,因而在代码编写和了解上可能会有一些差别。条件变量是一种在并发编程中罕用的同步机制,而通道则是一种更通用的并发原语,可用于实现更宽泛的通信和同步模式。
在抉择并发原语时,咱们应该思考到代码的可读性、可维护性和性能等因素。有时,应用条件变量可能是更适合和直观的抉择,而在其余状况下,通道可能更实用。理解不同并发原语的劣势和限度,并依据具体需要做出适当的抉择,是编写高质量并发代码的要害。
4. channel 实用场景阐明
事实上,channel
并不是被专门用来实现同步操作,而是基于 channel
中阻塞期待的个性,从而来实现一些简略的同步操作。尽管 sync.Cond
是专门设计来实现同步操作的,然而在某些场景下,应用通道比应用 sync.Cond
更为适合。
其中一个最典型的例子,便是工作的有序执行,应用 channel,可能使得工作的同步和程序执行变得更加直观和可治理。上面通过一个示例代码,展现如何应用通道实现工作的有序执行:
package main
import "fmt"
func taskA(waitCh chan<- string, resultCh chan<- string) {
// 期待开始执行
<- waitCh
// 执行工作 A 的逻辑
// ...
// 将工作 A 的后果发送到通道
resultCh <- "工作 A 实现"
}
func taskB(waitCh <-chan string, resultCh chan<- string) {
// 期待开始执行
resultA := <-waitCh
// 依据工作 A 的后果执行工作 B 的逻辑
// ...
// 将工作 B 的后果发送到通道
resultCh <- "工作 B 实现"
}
func taskC(waitCh <-chan string, resultCh chan<- string) {
// 期待工作 B 的后果
resultB := <-waitCh
// 依据工作 B 的后果执行工作 C 的逻辑
// ...
resultCh <- "工作 C 实现"
}
func main() {
// 创立用于工作之间通信的通道
beginChannel := make(chan string)
channelA := make(chan string)
channelB := make(chan string)
channelC := make(chan string)
beginChannel <- "begin"
// 启动工作 A
go taskA(beginChannel, channelA)
// 启动工作 B
go taskB(channelA, channelB)
// 启动工作 C
go taskC(channelB,channelC)
// 阻塞主线程,期待工作 C 实现
select {}
// 留神:上述代码只是示例,理论状况中可能须要适当地增加同步操作或敞开通道的逻辑
}
在这个例子中,咱们启动了三个工作,并通过通道进行它们之间的通信来保障执行程序。工作 A 期待 beginChannel
通道的信号,一旦接管到信号,工作 A 开始执行并将后果发送到 channelA
通道。其余工作,比方工作 B,期待工作 A 实现的信号,一旦接管到 channelA
通道的数据,工作 B 开始执行。同样地,工作 C 期待工作 B 实现的信号,一旦接管到 channelB
通道的数据,工作 C 开始执行。通过这种形式,咱们实现了工作之间的有序执行。
绝对于应用 sync.Cond
的实现形式来看,通过应用通道,在工作之间进行有序执行时,代码通常更加简洁和易于了解。比方下面的例子,咱们能够很分明得辨认进去,工作的执行程序为 工作 A —> 工作 B –> 工作 C。
其次通道能够轻松地增加或删除工作,并调整它们之间的程序,而无需批改大量的同步代码。这种灵活性使得代码更易于保护和演进。也是以下面的代码例子为例,如果当初须要批改工作的执行程序,将其执行程序批改为 工作 A —> 工作 C —> 工作 B,只须要简略调整下程序即可,具体如下:
func main() {
// 创立用于工作之间通信的通道
beginChannel := make(chan string)
channelA := make(chan string)
channelB := make(chan string)
channelC := make(chan string)
beginChannel <- "begin"
// 启动工作 A
go taskA(beginChannel, channelA)
// 启动工作 B
go taskB(channelC, channelB)
// 启动工作 C
go taskC(channelA,channelC)
// 阻塞主线程,期待工作 C 实现
select {}
// 留神:上述代码只是示例,理论状况中可能须要适当地增加同步操作或敞开通道的逻辑
}
和之前的惟一区别,只在于工作 B 传入的 waitCh 参数为 channelC,工作 C 传入的 waitCh 参数为 channelA,做了这么一个小小的变动,便实现了工作执行程序的调整,非常灵活。
最初,绝对于 sync.Cond,通道提供了一种平安的机制来实现工作的有序执行。因为通道在发送和接收数据时会进行隐式的同步,因而不会呈现数据竞争和并发拜访的问题。这能够防止潜在的谬误和 bug,并提供更牢靠的同步操作。
总的来说,如果是工作之间的简略协调,比方工作执行程序的协调同步,通过通道来实现是十分适合的。通道提供了简洁、牢靠的机制,使得工作的有序执行变得灵便和易于保护。
5. sync.Cond 实用场景阐明
在工作之间的简略协调场景下,应用 channel 的同步实现,绝对于 sync.Cond 的实现是更为简洁和易于保护的,然而并非意味着 sync.Cond 就无用武之地了。在一些绝对简单的同步场景下,sync.Cond 绝对于 channel 来说,表达能力是更强的,而且是更为容易了解的。因而,在这些场景下,尽管应用 channel 也可能起到同样的成果,应用 sync.Cond 可能相对来说也是更为适合的,即便 sync.Cond 应用起来更为简单。上面咱们来简略讲述下这些场景。
5.1 精细化条件管制
对于具备简单的期待条件和须要精细化同步的场景,应用 sync.Cond
是一个适合的抉择。它提供了更高级别的同步原语,可能满足这种特定需要,并且能够确保线程平安和正确的同步行为。
上面举一个简略的例子,有一个主协程负责累加计数器的值,而存在多个期待协程,每个协程都有本人独特的期待条件。期待协程须要期待计数器达到特定的值能力继续执行。
对于这种场景,应用 sync.Cond
来实现是更为适合的抉择。sync.Cond
提供了一种基于条件的同步机制,能够不便地实现协程之间的期待和告诉。应用 sync.Cond
,主协程能够通过调用Wait
办法期待条件满足,并通过调用 Broadcast
或Signal
办法来告诉期待的协程。期待的协程能够在条件满足时继续执行工作。
相比之下,应用通道来实现可能会更加简单和繁琐。通道次要用于协程之间的通信,并不间接提供条件期待的机制。尽管能够通过在通道中传递特定的值来模仿条件期待,但这通常会引入额定的复杂性和可能的竞争条件。因而,在这种状况下,应用 sync.Cond
更为适合,能够更间接地表白协程之间的条件期待和告诉,代码也更易于了解和保护。上面来简略看下应用 sync.Cond
实现:
package main
import (
"fmt"
"sync"
)
var (
counter int
cond *sync.Cond
)
func main() {cond = sync.NewCond(&sync.Mutex{})
// 启动期待协程
for i := 0; i < 5; i++ {go waitForCondition(i)
}
// 模仿累加计数器
for i := 1; i <= 10; i++ {
// 加锁,批改计数器
cond.L.Lock()
counter += i
fmt.Println("Counter:", counter)
cond.L.Unlock()
cond.Broadcast()}
}
func waitForCondition(id int) {
// 加锁,期待条件满足
cond.L.Lock()
defer cond.L.Unlock()
// 期待条件满足
for counter < id*10 {cond.Wait()
}
// 执行工作
fmt.Printf("Goroutine %d: Counter reached %d\n", id, id*10)
}
在上述代码中,主协程应用 sync.Cond
的Wait
办法期待条件满足时进行告诉,而期待的协程通过查看条件是否满足来决定是否继续执行工作。每个协程执行的计数器值条件都不同,它们会期待主协程累加的计数器值达到预期的条件。一旦条件满足,期待的协程将执行本人的工作。
通过应用sync.Cond
,咱们能够实现多个协程之间的同步和条件期待,以满足不同的执行条件。
因而,对于具备简单的期待条件和须要精细化同步的场景,应用 sync.Cond
是一个适合的抉择。它提供了更高级别的同步原语,可能满足这种特定需要,并且能够确保线程平安和正确的同步行为。
5.2 须要重复唤醒所有期待协程
这里还是以下面的例子来简略阐明,主协程负责累加计数器的值,并且有多个期待协程,每个协程都有本人独特的期待条件。这些期待协程须要期待计数器达到特定的值能力继续执行。在这种状况下,每当主协程对计数器进行累加时,因为无奈确定哪些协程满足执行条件,须要唤醒所有期待的协程。这样,所有的协程能力判断是否满足执行条件。如果只唤醒一个期待协程,那么可能会导致另一个满足执行条件的协程永远不会被唤醒。
因而,在这种场景下,每当计数器累加一个值时,都须要唤醒所有期待的协程,以防止某个协程永远不会被唤醒。这种须要反复调用 Broadcast
的场景并不适宜应用通道来实现,而是最适宜应用 sync.Cond
来实现同步操作。
通过应用 sync.Cond
,咱们能够创立一个条件变量,协程能够应用Wait
办法期待特定的条件呈现。当主协程累加计数器并满足期待条件时,它能够调用 Broadcast
办法唤醒所有期待的协程。这样,所有满足条件的协程都有机会继续执行。
因而,在这种须要反复调用 Broadcast
的同步场景中,应用 sync.Cond
是最为适合的抉择。它提供了灵便的条件期待和唤醒机制,确保所有满足条件的协程都能失去执行的机会,从而实现正确的同步操作。
6. 总结
同步操作在并发编程中起着要害的作用,用于确保协程之间的正确协调和共享数据的一致性。在抉择同步操作的实现形式时,咱们有两个常见选项:应用 sync.Cond
和通道。
应用 sync.Cond
和通道的形式提供了更高级、更灵便的同步机制。sync.Cond
容许协程期待特定条件的呈现,通过 Wait
、Signal
和Broadcast
办法的组合,能够实现简单的同步需要。通道则提供了间接的通信机制,通过发送和接管操作进行隐式的同步,防止了数据竞争和并发拜访谬误。
抉择适当的同步操作实现形式须要思考具体的利用场景。对于简略的同步需要,能够应用通道形式。对于简单的同步需要,波及共享数据的操作,应用 sync.Cond
和能够提供更好的灵活性和安全性。
通过理解不同实现形式的特点和实用场景,能够依据具体需要抉择最合适的同步机制,确保并发程序的正确性和性能。