<p>I'm sure most of us have heard this mantra over and over - yet I still typically find myself taking the lazy/comfortable path (e.g. sync.Mutex) whenever I have to deal with concurrent data access. </p> <p>I figured I'd finally take some time to try out a solution using channels & first class functions. I wrote a little cache library, and I'm really happy with the results. I'd be happy to hear feedback/criticisms. You can check it out at:</p> <p><a href="https://github.com/zpatrick/go-cache">https://github.com/zpatrick/go-cache</a> </p> <p>I was heavily influenced by Dave Cheney's article, which I highly recommend reading if you haven't already:</p> <p><a href="https://dave.cheney.net/2016/11/13/do-not-fear-first-class-functions">https://dave.cheney.net/2016/11/13/do-not-fear-first-class-functions</a></p> <hr/>**评论:**<br/><br/>earthboundkid: <pre><p>I like this blog post I wrote: <a href="https://blog.carlmjohnson.net/post/share-memory-by-communicating/">https://blog.carlmjohnson.net/post/share-memory-by-communicating/</a></p> <p>To boil it down, try to keep your variables local to one goroutine where practical. Don't be afraid to have a single "dispatcher" routine that handles coordinating work among a number of workers that are essentially synchronous. The async comes from the coordination, not the workers themselves. </p></pre>earthboundkid: <pre><p>Here's the beginning of how I would write an expiring cache: <a href="https://play.golang.org/p/-2ArLCGvW4">https://play.golang.org/p/-2ArLCGvW4</a></p> <p>There's a bit of hand waving in there, but it does compile and hopefully you can see how I mean for it to work.</p> <p>Basically, if there's one big for loop with a select statement in it that controls everything concurrent, that makes it really easy to reason about who is doing what when and prevent race conditions etc.</p></pre>zpatrick319: <pre><p>Nice! This was my first approach as well. </p> <p>That being said, the <a href="https://dave.cheney.net/2016/11/13/do-not-fear-first-class-functions" rel="nofollow">https://dave.cheney.net/2016/11/13/do-not-fear-first-class-functions</a> article actually goes into how this pattern can be improved by using a single channel that takes a first class function. The "one big for loop with a select statement in it that controls everything concurrent" is exactly the same, but you get the bonus of only needing a single channel to implement any behavior. </p> <p>As the example in the article shows, this:</p> <pre><code>func (m *Mux) Add(conn net.Conn) { m.add <- conn } func (m *Mux) Remove(addr net.Addr) { m.remove <- addr } func (m *Mux) loop() { conns := make(map[net.Addr]net.Conn) for { select { case conn := <-m.add: m.conns[conn.RemoteAddr()] = conn case addr := <-m.remove: delete(m.conns, addr) } } </code></pre> <p>can become this:</p> <pre><code>func (m *Mux) Add(conn net.Conn) { m.ops <- func(m map[net.Addr]net.Conn) { m[conn.RemoteAddr()] = conn } } func (m *Mux) Remove(addr net.Addr) { m.ops <- func(m map[net.Addr]net.Conn) { delete(m, addr) } } func (m *Mux) loop() {
conns := make(map[net.Addr]net.Conn) for op := range m.ops { op(conns) } } </code></pre> <p>As you add more functionality, the loop() function will never need to change. </p></pre>earthboundkid: <pre><p>Hmm, it looks cool, but I'm not convinced it's better than a mutex. :-) What does the loop of closures buy you in terms of code clarity that you couldn't get by just grabbing a lock?</p></pre>mcouturier: <pre><p>For one you follow the open-closed principle: the system is open to extensions, but closed to modifications (loop func).</p></pre>mcouturier: <pre><p>I'm just wondering how you cleanup that thing (shutdown function)</p></pre>zpatrick319: <pre><p>It's not exposed in my example or the article, but running close() on a channel will stop the range loop: <a href="https://gobyexample.com/range-over-channels" rel="nofollow">https://gobyexample.com/range-over-channels</a></p></pre>mcouturier: <pre><p>Yes but sending on a closed channel will also panics. I guess you have to state that any call to the methods after a close will do.</p></pre>teepark: <pre><p>Your Items method should make a copy and send that back over the chan. As it is the caller could mutate the (shared) map after getting it.</p></pre>zpatrick319: <pre><p>good eye - thanks man </p></pre>alasijia: <pre><p>"Share memory by communicating" is not a good description for the functionalities of channels. "Transfer value ownership" is a better one.</p></pre>Femaref: <pre><p>it's a quote from <a href="https://golang.org/doc/effective_go.html#sharing" rel="nofollow">effective go</a>. What they describe there is what you are describing as well.</p></pre>
