Sure, feel free to do what you are comfortable with. I just couldn't understand why there is a need for the Update function at all.
Hope that your Go transition goes smoothly :) -Agniva On Thu, 27 Sep 2018 at 21:29 Michael Ellis <michael.f.el...@gmail.com> wrote: > Thanks for the feedback, Agni. I think I may not have included enough > detail in my original post. > > At present, the app is in the middle of transitioning to an all-Go > implementation. For business reasons, that process will be stretched out > over the next several months or longer. In the meantime, it needs to keep > working so I've already implemented something quite similar to what you > describe by porting the busiest Python processes first and providing a > socket interface that replaces the ZeroMQ interface in a way that allows > the calls from the remaining Python processes to have the same signature > and behavior as before. That's working quite well so far and we're already > seeing better performance and improved reliability. > > I should also mention that the target for the system is an inexpensive > Linux SBC, a Raspberry Pi. In the pure Python version, the overhead for > serializing, transmitting and de-serializing data had grown to use far more > CPU than seemed wise in a system that needs to run reliably for months at a > time at customer sites around the world. > > Within the collection of processes that are now goroutines, I can't see a > benefit to incurring the overhead to marshal and unmarshal the data if > there's a concurrency-safe way to perform updates and get snapshots of the > state data. > > Sending functions over a channel seems to be a match made in heaven for > our use cases. It nicely supports our application logic that parcels out > responsibility for updating pieces of the state struct to long-running > goroutines. Most of them are loops that fetch a snapshot of the state, > inspect it to make decisions about what actions to take, perform the > actions and update the state. > > In that context, sending anonymous update functions wrapped in struct with > a disposable "done" channel doesn't seem overcomplicated at all. The > update actions in my first post boil down to three lines that can be > wrapped up in a single function, e.g. > > func Update(f func(*Big)) { > u := update{make(chan struct{}), f} > upch <- u // upch is a toplevel var visible to all goroutines > <-u.done > } > > and an (oversimplified) goroutine that, say, reads a new value from a > sensor looks like > > func gopher() { > var newA int > f := func(b *Big) { > b.A = newA > } > for { > newA = ReadSensor() > Update(f) > } > } > > As I see it, the beauties of this approach are: > > 1. f is locally defined and therefore has access to gopher's locals, > 2. gopher has no direct access to the instance of Big that holds the > state. > 3. Update pauses gopher until the state update has completed in the > single goroutine that reads from upch. So no worries about gopher changing > its own locals before the update is complete. > 4. Benchmarking shows it's more than fast enough for our needs. > 5. The compiler catches any type mismatches instead of depending on > the runtime to report it at deserialization. > > > Sorry for the long-winded reply. I really appreciate the feedback and the > opportunity to learn from you and others who've been using Go far longer > than I. > > > > > > > > On Thu, Sep 27, 2018 at 12:48 AM Agniva De Sarker < > agniva.quicksil...@gmail.com> wrote: > >> I think you are overcomplicating this a bit. It seems like a simple >> pattern of broadcasting a change to multiple agents. You send the change >> over a REQ-REP pair, and broadcast it to others over a PUB-SUB pair. >> >> Why do you need to copy the struct again ? Just get the struct from the >> REP socket and push it to the PUB socket ? >> >> Here is what I would do - >> >> Use protobuf to marshal/unmarshal the config struct when >> sending/receiving it through the socket. That way you have perfect interop >> between Go and Python. >> >> And use a for-select pattern to do the job of receiving and broadcasting. >> Pseudo code as follows >> >> package main >> >> func main() { >> // setup REP socket in a goroutine, unmarshal the msg, send the struct to >> updateChan >> // create PUB socket, store it in mem somewhere >> >> go run() >> >> // setup signal handlers, and send signal to done chan when >> ctrl-c is received. >> } >> >> func run() { >> for { >> select { >> case msg := <-updateChan: >> // push msg to the PUB socket >> case <-done: >> // you have to send signal to this from main() >> return >> } >> } >> } >> >> Isn't this what you are trying to do ? >> >> >> >> On Wednesday, 26 September 2018 00:09:07 UTC+5:30, Michael Ellis wrote: >>> >>> Hi, new gopher here. >>> I considered asking this on SO, but they (rightly, IMO) discourage "Is >>> this a good way to do it?" questions. Hope that's ok here. >>> >>> By way of background, I'm porting a largish industrial control >>> application from Python to Go. The Python version uses multiple processes >>> (about a dozen in all) communicating over ZeroMQ. One process, called the >>> statehouse, controls access to the application state. The others obtain >>> copies and send updates over REQ sockets. The data are serialized as JSON >>> objects that map nicely to Python dicts. >>> >>> Since there's no direct equivalent in Go to a Python dict that can hold >>> a mixture of arbitrary types, I need to use a struct to represent the >>> state. No problem with that but I've been struggling with how to allow the >>> goroutines that will replace the Python processes to read and write to the >>> state struct with concurrency safety. >>> >>> This morning I came up with an idea to send functions over a channel to >>> the main routine. I put together a little test program and after some >>> refinements it looks promising. Some rough benchmarking shows I can get a >>> million updates in under 1 second on a 2012 vintage Mac Mini. That's more >>> than good enough for this application where the time between events is >>> usually more than 100 milliseconds. >>> >>> Here's the link to my test on the Go Playground: >>> https://play.golang.org/p/8iWvwnqBNYl . It runs there except that the >>> elapsed time comes back 0 and the prints from the second goroutine don't >>> show up. I think that's got something to do with the artificial clock in >>> the playground. It works fine when I run it locally. I've pasted the code >>> at the bottom of this message. >>> >>> So my big questions are: >>> >>> - Is this actually concurrency safe as long as all goroutines only >>> use the update mechanism to read and write? >>> - Is there a more idiomatic way to do it that performs as well or >>> better? >>> - What are the potential problems if this is scaled to a couple >>> dozen goroutines? >>> - Does it sacrifice clarity for cleverness? (not that it's all that >>> clever, mind you, but I need to think about handing this off to my >>> client's >>> staff.) >>> >>> >>> Thanks very much, >>> Mike Ellis >>> >>> code follows ... >>> >>> package main >>> >>> import ( >>> "fmt" >>> "time" >>> ) >>> >>> >>> // Big defines the application's state variables >>> type Big struct { >>> A int >>> B string >>> /* and hundreds more */ >>> } >>> >>> >>> // update is a struct that contains a function that updates a Big and >>> // a signal channel to be closed when the update is complete. An update >>> // may also be used to obtain a current copy of a Big by coding f to >>> // do so. (See gopher2 below.) >>> type update struct { >>> done chan struct{} >>> f func(*Big) >>> } >>> >>> >>> // upch is a channel from which main receives updates. >>> var upch = make(chan update) >>> >>> >>> // gopher defines a function that updates a member of a Big and >>> // sends updates via upch. After each send it waits for main to >>> // close the update's done channel. >>> func gopher() { >>> var newA int >>> f := func(b *Big) { >>> b.A = newA >>> } >>> for i := 0; i < n; i++ { >>> newA = i >>> u := update{make(chan struct{}), f} >>> upch <- u >>> <-u.done >>> } >>> } >>> >>> >>> // gopher2 uses an update struct to obtain a current copy of a Big >>> // every 100 microseconds. >>> func gopher2() { >>> var copied Big >>> f := func(b *Big) { >>> copied = *b >>> } >>> for { >>> time.Sleep(100 * time.Microsecond) >>> u := update{make(chan struct{}), f} >>> upch <- u >>> <-u.done >>> fmt.Println(copied) >>> } >>> } >>> >>> >>> // main creates a Big, launches gopher and waits on the update channel. >>> When >>> // an update, u, arrives it runs u.f and then closes u.done. >>> func main() { >>> var state = Big{-1, "foo"} >>> fmt.Println(state) // --> {-1, "foo"} >>> go gopher() >>> go gopher2() >>> start := time.Now() >>> for i := 0; i < n; i++ { >>> u := <-upch >>> u.f(&state) >>> close(u.done) >>> } >>> perUpdate := time.Since(start).Nanoseconds() / int64(n) // Note: >>> always 0 in playground >>> fmt.Printf("%d updates, %d ns per update.\n", n, perUpdate) >>> fmt.Println(state) // --> {n-1, "foo"} >>> } >>> >>> >>> var n = 1000 // number of updates to send and receive >>> >>> >>> >>> >>> >> -- >> You received this message because you are subscribed to a topic in the >> Google Groups "golang-nuts" group. >> To unsubscribe from this topic, visit >> https://groups.google.com/d/topic/golang-nuts/KiNpAxobbec/unsubscribe. >> To unsubscribe from this group and all its topics, send an email to >> golang-nuts+unsubscr...@googlegroups.com. >> For more options, visit https://groups.google.com/d/optout. >> > -- > Write music faster with Tbon. > <https://github.com/Michael-F-Ellis/tbon/#tbon> > -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/d/optout.