Pylinux's solution using a Mutex is, like he says, probably the simplest in your case. I'll add another one here as an alternative, though. It may or may not apply in your case.
Instead of using a Mutex, you could have a single goroutine perform all the operations on the serial interface, and use a channel to serialise the work it needs to perform. Example:
package main
import (
"fmt"
"sync"
)
// handleCommands will handle commands in a serialized fashion
func handleCommands(opChan <-chan string) {
for op := range opChan {
fmt.Printf("command: %s\n", op)
}
}
// produceCommands will generate multiple commands concurrently
func produceCommands(opChan chan<- string) {
var wg sync.WaitGroup
wg.Add(2)
go func() { opChan <- "cmd1"; wg.Done() }()
go func() { opChan <- "cmd2"; wg.Done() }()
wg.Wait()
close(opChan)
}
func main() {
var opChan = make(chan string)
go produceCommands(opChan)
handleCommands(opChan)
}
The advantage of this relative to a Mutex is that you have more control over the wait queue. With the Mutex, the queue exists implicitly at Lock()
, and is unbounded. Using a channel, on the other hand, you can limit the maximum number of callers waiting and react appropriately if the synchronised call site is overloaded. You can also do things like checking how many goroutines are in the queue with len(opChan)
.
Edit to add:
A limitation with the above example (as noted in the comments) is that it doesn't handle returning results from the computation back to the original sender. One way to do that, while keeping the approach of using channels, is to introduce a result channel for each command. So instead of sending strings over the command channel, one can send structs of the following format:
type operation struct {
command string
result chan string
}
Commands would be enqueued onto the command channel as follows:
func enqueueCommand(opChan chan<- operation, cmd string) <-chan string {
var result = make(chan string)
opChan <- operation{command: cmd, result: result}
return result
}
This allows the command handler to send a value back to the originator of the command. Full example on the playground here.
defer lock.Unlock()
just afterlock.Lock()
might be preferable, just in case anything inside the logic ofimportantFunction
panics. That way theUnlock
call is guaranteed to happen. – Liebfraumilch