I've done simple bench mark which one is more efficient among message-passing and locking for shared value.
Firstly, please check code bellow.
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)
}
As you already noticed, program is relatively simple. In message mode, it increases sharedValue by message passing. In mutex mode, it increases sharedValue by locking.
I've tried to use only one channel for message mode and gave up. I think it may not possible, isn't it?
My computer have 2 Xeon CPUs and each have 6 cores. Because of hyper threading, logically 24 cores are available. Its ram size is 12G.
If I run the program with any number for flags, mutex mode is always faster minimum 2 times(usually 3 times).
Okay, I can understand that managing channels need some amount of cost. Then, if I consider performance only, are there any reason to use channel instead of mutex? Plus, can message-passing cost be ignored with huge size of message?
if I consider performance only, are there any reason to use channel instead of mutex?
Not really. The wiki page "Use a sync.Mutex
or a channel?" say to use whichever is most expressive and/or most simple.
There is an example of channel used for Mutex, but as commented:
While channels provides a nice solution to protected data, it is a less efficient solution in the case of one writer and many readers.
This threads adds:
If you're sharing data, and never block in the locked section, just use a mutex.
Mutexes are really cheap in the non-blocking case.
If you have some shared service that does something complicated or long, and it has to be serialized, consider giving it its own goroutine that takes requests from a channel and sends replies back when done. Usually you send a struct
with the input parameters and
a channel object to be used for the reply.
This works a lot like RPC.
Channels are for communication, not locking.
If you're sending meaningless data over a channel for locking purposes only, you're
probably overcomplicating things.
VonC has described the specific reasons behind the results you have observed. In simple cases, mutexes are efficient because they are minimalist, channels less so because there is much more to be done, especially in your example code with the data being constructed as Response
instances.
Your test program could easily lead to the naive conclusion that mutexes are all you need, shared memory is sufficient, and channels are a wasteful and unnecessary nice idea. So why do the Go originators recommend sharing memory by communicating, instead of communicating by sharing memory?
Concurrency is about much more than just locking shared data. The whole premise behind Communicating Sequential Processes (CSP) is that systems fundamentally consist of processes (a.k.a. goroutines here) that do things, interacting with each other and with the outside world through the exchange of events, and these events may be messages carrying information. This model is recursive: processes themselves may contain smaller processes that do things, interacting with each other through the exchange of events.
So the channel communication model that Go supports as a key part of the language is scalable. It is possible to describe concurrent components at the small scale and use components to build up larger components, and so on. You can naturally describe very highly concurrent systems this way.
If you try to design concurrent systems using only mutexes, you will be frustrated and find you have to write mostly-sequential code. The resulting performance might be better in some cases, but there might be a significant counter cost in terms of expressiveness of the system and scope for parallel execution.
If you start a design thinking about how the shared data will be protected from race conditions, you will steer yourself into a design that suit mutexes, for which channels are too inefficient so have no relevance.
The simple case of multi-reader one-writer shared data comes up often enough to be worth reaching for the mutex solution. But sometimes this might mean overlooking a more general solution based on a service that has multiple clients.
Ultimately, all software design requires trade-offs to be assessed and decisions to be made one way or the other. In Go, you have the choice of using channels and composition of processes (i.e. goroutines) when appropriate. Very few other languages offer this. (Occam is the only one I know of that performs at least as well as Go).
It does matter which one you use. A channel is one-off, read/write on shared data. If you need some shared data to stick after initializing it one time, then you'll have to set some kind of variable and seize it with a lock. An example could be a stop channel.
// 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
}