我做了简单的基准测试,其中一个在消息传递和锁定共享值方面更有效。
首先,请检查以下代码。
package main
import (
"flag"
"fmt"
"math/rand"
"runtime"
"sync"
"time"
)
type Request struct {
Id int
ResChan chan Response
}
type Response struct {
Id int
Value int
}
func main() {
procNum := flag.Int("proc", 1, "Number of processes to use")
clientNum := flag.Int("client", 1, "Number of clients")
mode := flag.String("mode", "message", "message or mutex")
flag.Parse()
if *procNum > runtime.NumCPU() {
*procNum = runtime.NumCPU()
}
fmt.Println("proc:", *procNum)
fmt.Println("client:", *clientNum)
fmt.Println("mode:", *mode)
runtime.GOMAXPROCS(*procNum)
rand.Seed(time.Now().UnixNano())
var wg sync.WaitGroup
sharedValue := 0
start := time.Now()
if *mode == "message" {
reqChan := make(chan Request) // increasing channel size does not change the result
go func() {
for {
req := <-reqChan
sharedValue++
req.ResChan <- Response{Id: req.Id, Value: sharedValue}
}
}()
for i := 0; i < *clientNum; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
c := make(chan Response)
defer close(c)
id := rand.Int()
reqChan <- Request{Id: id, ResChan: c}
<-c
}(i)
}
} else if *mode == "mutex" {
mutex := &sync.Mutex{}
for i := 0; i < *clientNum; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
mutex.Lock()
sharedValue++
mutex.Unlock()
}(i)
}
}
wg.Wait()
elapsed := time.Since(start)
fmt.Println("Elapsed:", elapsed, "value:", sharedValue)
}
正如您已经注意到的,程序相对简单。在消息模式中,它会通过消息传递增加sharedValue。在互斥模式中,它会通过锁定增加 sharedValue 。
我尝试只使用一个频道用于消息模式并放弃了。我想这可能不可能,不是吗?
我的电脑有2个Xeon CPU,每个都有6个核心。由于超线程,逻辑上可以使用24个内核。它的公羊尺寸是12G。
如果我使用任何数字运行程序的标志,互斥模式总是最快2倍(通常是3次)。
好的,我可以理解管理渠道需要一定的成本。那么,如果我只考虑性能,有没有理由使用channel而不是mutex?另外,消息传递成本可以被大量消息忽略吗?
答案 0 :(得分:8)
如果我只考虑性能,有没有理由使用频道代替互斥?
不是真的。维基页面&#34; Use a sync.Mutex
or a channel?&#34;说使用最具表现力和/或最简单的
有一个example of channel used for Mutex,但as commented:
虽然渠道为受保护的数据提供了一个很好的解决方案,但对于一个作家和许多读者而言,它是一种效率较低的解决方案。
This threads补充道:
如果您正在共享数据,并且从不在锁定的部分中阻止,请使用互斥锁 非阻塞情况下互斥锁非常便宜。
如果您有一些复杂或冗长的共享服务,并且必须将其序列化,请考虑为其提供自己的goroutine,它接收来自通道的请求并在完成后发送回复。通常你发送一个带有输入参数的
struct
和 用于回复的频道对象 这很像RPC。频道用于沟通,而不是锁定 如果您通过频道发送无意义的数据仅用于锁定目的,那么您就是其中之一 可能过于复杂的事情。
答案 1 :(得分:3)
VonC已经描述了您观察到的结果背后的具体原因。在简单的情况下,互斥体是有效的,因为它们是极简主义的,频道较少,因为还有很多工作要做,尤其是在示例代码中,数据被构造为Response
个实例。
您的测试程序很容易导致天真的结论,即互斥体是您所需要的,共享内存就足够了,并且通道是一个浪费且不必要的好主意。那么为什么Go发起人建议通过通信来共享内存,而不是通过共享内存进行通信?
并发不仅仅是锁定共享数据。传递顺序进程(CSP)背后的整个前提是系统基本上由进行事务的进程(例如,这里的goroutines)组成,通过事件交换彼此交互以及与外部世界交互,并且这些事件可以是携带信息的消息。这个模型是递归的:进程本身可能包含较小的进程,这些进程通过事件交换相互交互。
因此,Go支持的通道通信模型是可扩展的关键部分。可以以小规模描述并发组件,并使用组件来构建更大的组件,等等。您可以通过这种方式自然地描述非常高度并发的系统。
如果您尝试仅使用互斥锁设计并发系统,您会感到沮丧,并且发现您必须编写大部分顺序代码。在某些情况下,最终的性能可能会更好,但在系统的表现力和并行执行的范围方面可能会有相当大的反成本。
如果您开始考虑如何保护共享数据免受竞争条件的影响,您将引导自己进入适合互斥体的设计,因为这些设备的效率太低,因此没有相关性。
多读取器一个写入器共享数据的简单情况经常出现,值得为互斥解决方案提供帮助。但有时这可能意味着忽略了基于具有多个客户端的服务的更通用的解决方案。
最终,所有软件设计都需要进行权衡评估,并以某种方式做出决策。在Go中,您可以选择在适当时使用通道和进程组合(即goroutines)。很少有其他语言能提供此功能。 (Occam是我所知道的唯一一个至少和Go一样好的人。)
答案 2 :(得分:0)
使用哪一个确实很重要。通道是一次性的,对共享数据进行读/写。如果在初始化一次之后需要一些共享数据,那么你必须设置某种变量并用锁来抓住它。一个例子可能是停止通道。
// thread two spawned right before thread one reached the stop case.
select {
case <-stop // thread one reaches stop case and does stuff.
// do stuff
default: // a few nanoseconds later, thread two breaches the stop case.
// do other stuff
}