通过Go语言中的通道实现双向通信

huangapple go评论137阅读模式
英文:

two way communication through channels in golang

问题

我有几个函数,我希望它们在执行时是原子的,因为它们涉及到敏感的数据结构。假设以下情况:
有两个函数:lock(sth)unlock(sth),可以随时由一个 goroutine 调用来锁定或解锁全局数组中的 sth。我考虑使用一个命令通道,这样 goroutine 可以将 lockunlock 命令发送到通道中,在通道的接收端,一种 handler 顺序地处理 lockunlock 请求,通过从通道中获取命令。这样做是可以的,但是如果 handler 想要将结果发送回请求者,是否可以使用 golang 通道来实现呢?我知道可以使用一些锁机制,比如互斥锁,但我想知道是否可以在这种情况下使用通道?我在某个地方看到建议使用通道而不是 goland 低级别的锁结构。

简单来说:

在容量为1的通道中,我希望接收端能够回复给发送消息的 goroutine。

或者等价地说:

一个 goroutine 发送一些东西到一个通道;消息被另一个 goroutine 接收并处理,产生一些结果;发送者如何得知结果?

英文:

I have several functions that I want them to be executed atomically since they deal with sensitive data structures. Suppose the following scenario:
There are two functions: lock(sth) and unlock(sth) that can be called anytime by a goroutine to lock or unlock sth in a global array. I was thinking about having a command channel so that goroutines send lock and unlock commands into the channel, and on the receive side of the channel, some kind of handler handles lock, unlock requests, sequentially, by grabbing commands from the channel. That's fine, but what if the handler wants to send the result back to the requester? Is it possible to do so use golang channels? I know that it is possible to use some kind of lock mechanism like mutex, but I was wondering if it's possible to use channels for such use-case? I saw somewhere that it is recommended to use channel instead of goland low-level lock structs.

In a single sentence:

In a channel with the capacity of 1, I want the receiver side to be able to reply back to the goroutine which sent the message.

or equivalently:

A goroutine sends something to a channel; the message is received by another goroutine and handled leading to some result; how does the sender become aware of the result?

答案1

得分: 2

sync包包含一个互斥锁sync.Mutex,可以以线程安全的方式从任何goroutine中锁定和解锁。不必使用通道发送锁定命令,可以直接使用互斥锁。

mutex := new(sync.Mutex)
sensitiveData := make([]string, 0)
// 当有人想要操作sensitiveData时,
// ...
mutex.Lock()
operate(sensitiveData)
mutex.Unlock()

当你说发送方如何意识到结果时,我认为你是在谈论处理程序如何接收结果,这可以通过chan来实现。你可以通过通道发送数据。

或者,如果你只是想要意识到结果,可以使用信号量sync.WaitGroup。这个结构体可以通过Add()方法增加计数,然后发送方可以使用wg.Wait()等待,直到处理程序调用wg.Done(),这将告诉正在等待的发送方处理程序已经完成了某些操作。


如果你的问题是关于使用锁还是通道,维基上有一个简洁的答案:

一个常见的Go初学者错误是过度使用通道和goroutine,仅仅因为它是可能的,或者因为它很有趣。如果互斥锁最适合你的问题,不要害怕使用sync.Mutex。Go在让你使用最适合解决问题的工具方面是务实的,并不会强迫你使用一种代码风格。

作为一般指南:

通道:传递数据的所有权,分发工作单元,异步通信结果
互斥锁:缓存,状态


如果你绝对想避免除了chan之外的任何东西:),尝试不要修改敏感数组。而是使用通道将数据发送到不同的goroutine,在每个步骤处理数据,然后将处理后的数据传输到最终类型的goroutine中。也就是说,完全避免使用数组,而是将数据存储在通道中。

正如格言所说,

不要通过共享内存进行通信;相反,通过通信共享内存。

英文:

The sync package includes a Mutex lock, sync.Mutex, which can be locked and unlocked from any goroutine in a threadsafe way. Instead of using a channel to send a command to lock something, how about just using a mutex lock from the sender?

mutex := new(sync.Mutex)
sensitiveData := make([]string, 0)
// when someone wants to operate on a sensitiveData,
// ...
mutex.Lock()
operate(sensitiveData)
mutex.Unlock()

When you say how does the sender become aware of the result, I think you're talking about how does the handler receive the result -- that would be with a chan. You can send data through channels.

Alternatively, if you just want to be aware, a semaphore, sync.WaitGroup might do the job. This struct can be Add()ed to, and then the sender can wg.Wait() until the handler calls wg.Done(), which will indicate to the sender (which is waiting) that the handler is done doing such and such.

<hr>

If your question is about whether to use locks or channels, the wiki has a terse answer:

> A common Go newbie mistake is to over-use channels and goroutines just because it's possible, and/or because it's fun. Don't be afraid to use a sync.Mutex if that fits your problem best. Go is pragmatic in letting you use the tools that solve your problem best and not forcing you into one style of code.
>
> As a general guide, though:
>
> Channel: passing ownership of data, distributing units of work, communicating async results
> Mutex: caches, state

<hr>

If you absolutely want to avoid anything but chans :), try not altering the sensitive array to begin with. Rather, use channels to send data to different goroutines, at each step processing the data, and then funneling the processed data into a final type goroutine. That is, avoid using an array at all and store the data in chans.

As the motto goes,

> Do not communicate by sharing memory; instead, share memory by communicating.

答案2

得分: 0

如果你想要防止竞态条件,那么使用sync原语应该可以很好地工作,就像@Nevermore的回答中所描述的那样。这样可以使代码更易读,更容易理解。

然而,如果你想要使用通道来进行同步,你可以尝试以下方法:

// 作为锁使用的全局共享通道。容量为1,只允许一个线程同时访问受保护的资源。
var lock = make(chan struct{}, 1)

// Operate函数对受保护的资源进行访问/修改。
func Operate(f func() error) error {
    lock <- struct{}{}
    defer func() { <-lock }()
    return f()
}

要使用Operate函数,传入一个访问受保护资源的闭包函数。

// 需要并发访问的某个值。
var arr = []int{1, 2, 3, 4, 5}

// 用于同步goroutine的WaitGroup。
var wg sync.WaitGroup
wg.Add(len(arr))

for i := 0; i < len(arr); i++ {
    go func(j int) {
        defer wg.Done()

        // 对arr的访问仍然受保护。
        Operate(func() error {
            arr[j] *= 2
            return nil
        })

    }(i)
}
wg.Wait()

工作示例:https://play.golang.org/p/Drh-yJDVNh

或者你可以完全绕过Operate函数,直接使用lock来提高可读性:

go func(j int) {
    defer wg.Done()

    lock <- struct{}{}
    defer func() { <-lock }()

    arr[j] *= 2
}(i)

工作示例:https://play.golang.org/p/me3K6aIoR7

如你所见,这里使用通道来保护对arr的访问。

英文:

If you want to prevent race conditions then sync primitives should work just fine, as described in @Nevermore's answer. It leaves the code much more readable and easier to reason about.

However, if you want channels to perform syncing for you, you can always try something like below:

// A global, shared channel used as a lock. Capacity of 1 allows for only
// one thread to access the protected resource at a time.
var lock = make(chan struct{}, 1)


// Operate performs the access/modification on the protected resource.
func Operate(f func() error) error {
	lock &lt;- struct{}{}
	defer func() { &lt;- lock }()
	return f()
}

To use this Operate, pass in a closure that accesses the protected resource.

// Some value that requires concurrent access.
var arr = []int{1, 2, 3, 4, 5}

// Used to sync up goroutines.
var wg sync.WaitGroup
wg.Add(len(arr))

for i := 0; i &lt; len(arr); i++ {
	go func(j int) {
		defer wg.Done()

        // Access to arr remains protected.
		Operate(func () error {
			arr[j] *= 2
			return nil
		})

	}(i)
}
wg.Wait()

Working example: https://play.golang.org/p/Drh-yJDVNh

Or you can entirely bypass Operate and use lock directly for more readability:

go func(j int) {
	defer wg.Done()
	
	lock &lt;- struct{}{}
	defer func() { &lt;- lock }()
	
	arr[j] *= 2
}(i)

Working example: https://play.golang.org/p/me3K6aIoR7

As you can see, arr access is protected using a channel here.

答案3

得分: -1

其他问题已经涵盖了锁定的内容,但我想解决问题的另一部分,即使用通道将响应发送回调用者。在Go语言中,有一种常见的模式是在请求中发送一个响应通道。例如,您可以通过通道将命令发送给处理程序;这些命令将是一个具有特定实现细节的结构体,并且该结构体将包含一个用于发送结果的通道,其类型为结果类型。每个发送的命令都会包含一个新的通道,处理程序将使用该通道发送响应,然后关闭它。示例如下:

type Command struct {
    // 命令参数等
    Results chan Result
}

type Result struct {
    // 在这种情况下,结果是什么
}

var workQueue = make(chan Command)

// 同步执行的示例
func Example(param1 string, param2 int) Result {
    workQueue <- Command{
        Param1: param1,
        Param2: param2,
        Results: make(chan Result),
    }
    return <-Results
}

以上是一个使用通道发送请求和接收响应的示例代码。

英文:

The other questions have covered locking well, but I wanted to address the other part of the question around using channels to send a response back to a caller. There is a not-uncommon pattern in Go of sending a response channel with the request. For example, you might send commands to a handler over a channel; these commands would be a struct with implementation-specific details, and the struct would include a channel for sending the result back, typed to the result type. Each command sent would include a new channel, which the handler would use to send back the response, and then close. To illustrate:

type Command struct {
    // command parameters etc
    Results chan Result
}

type Result struct {
    // Whatever a result is in this case
}

var workQueue = make(chan Command)

// Example for executing synchronously
func Example(param1 string, param2 int) Result {
    workQueue &lt;- Command{
        Param1: param1,
        Param2: param2,
        Results: make(chan Result),
    }
    return &lt;- Results

huangapple
  • 本文由 发表于 2017年6月8日 06:26:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/44423746.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定