如何在Golang中使函数线程安全

How to lock a function or the body of a function from being called by two threads in golang?

My use case is that I have a webserver that is calling a serial interface which can only have one caller at a time, two calls will cancel each other out by creating noise for one another on the serial line.

Easiest way is to use sync.Mutex:

package main

import (
    "fmt"
    "sync"
    "time"
)

var lock sync.Mutex

func main() {
    go importantFunction("first")
    go importantFunction("second")
    time.Sleep(3 * time.Second)
}


func importantFunction(name string) {
    lock.Lock()
    defer lock.Unlock()
    fmt.Println(name)
    time.Sleep(1 * time.Second)
}

Here you'll see that "first" and "second" is printed one second apart even though they are go routines.

Go playground: https://play.golang.org/p/mXKl42zRW8

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
", 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.

There are two approaches to implementing non-reentrant functions:

  • Blocking: first caller runs the function, subsequent caller(s) block and wait till function exits, then run the function
  • Yielding: first caller runs the function, subsequent caller(s) abort while function is being executed

The two approaches have different merits:

  • Blocking non-reentrant functions are guaranteed to execute as many times as were attempted. However, they can be a backlog in case of long execution times, then bursts of executions following.
  • Yielding non-reentrant functions guarantee non congestion and no bursts, and can guarantee a maximum of execution rate.

Blocking non-reentrant functions are most easily implemented via mutex, as described in @Pylinux's answer. Yielding non-reentrant functions can be implemented via atomic compare & swap, as follows:

import (
    "sync/atomic"
    "time"
)

func main() {
    tick := time.Tick(time.Second)
    var reentranceFlag int64
    go func() {
        for range tick {
            go CheckSomeStatus()
            go func() {
                if atomic.CompareAndSwapInt64(&reentranceFlag, 0, 1) {
                    defer atomic.StoreInt64(&reentranceFlag, 0)
                } else {
                    return
                }
                CheckAnotherStatus()
            }()
        }
    }()
}

In the above, CheckAnotherStatus() is protected against re-entry such that the first caller sets reentranceFlag to 1, and subsequent callers fail to do the same, and quit.

Please consider my blog post, Implementing non re-entrant functions in Golang for a more elaborate discussion.