6 ms·
Go and my realization about what I'll call the 'Promises' pattern
- ekimekim 2y agoIt's been a while since I did much Go, but I think you can handle this cleanly by making one channel per task, and having an array of channels similar to the array of promises. Each channel takes that task's result, then closes. The caller waits on each channel in sequence.
- Jtsummers 2y agoThat's exactly what the author ends up with, too. It's also how you might handle this in any other language that allows for some form of "join", either intended or usable as a join. Waiting on promises, thread joins, channels (as a signal mechanism), it's all the same pattern: Instantiate a bunch of asynchronous activities and queue up a "handler" corresponding to it, wait on the handlers in your desired order.
- davery22 2y agoSo: A channel with buffer size 1, so long as it is only written to once and read from once, feels a lot like a Promise.
- teraflop 2y agoA variable you can only read once is a lot less useful than a variable you can read as many times as you want. Same goes for promises.
- caleblloyd 2y agoShortcut could be to create an array of sync.OnceValue, immediately invoking each element in a goroutine. Then iterate through the array and call each function again.
- XorNot 2y agoThe "in-order" requirement makes this a weird problem to think you have. There's no situation where the channels are going to be heavier then the go-routines you're spawning to handle the processing: in fact the last line lamenting leaving blocked go-routines around is weird, because a blocked go-routine is still using less resources then one actually doing things - it's totally fine for them to block waiting to write to the channel because of the "in-order" requirement. Your worst case scenario is you spawn N go-routines, and they complete 1 by 1 in reverse order from N so your entire dataset is waiting in memory for in-order completion - so no other concern here matters at all.
- pbnjay 2y agoIsn't it even simpler in Go? No channels necessary, Each goroutine gets an index or slice, and writes the results to a shared array. All you need is the goroutines to report when done via a WaitGroup.
- teraflop 2y agoThat doesn't satisfy the "report the results as they become available" requirement. The desired behavior is that printing the first result should only wait for the first result to be available, not for all the results to be available.
- pbnjay 2y agoWould be trivial to show the results live though, especially for atomic values: https://go.dev/play/p/PRzzO_skWoJ https://go.dev/play/p/PRzzO_skWoJ
- valleyer 2y agoThat's not "live"; that's just polling.
- lenkite 2y agoModified the above to https://go.dev/play/p/DRXyvRHsuAH https://go.dev/play/p/DRXyvRHsuAH You get the first result in results[0] thanks to `atomic.Int32`. package main import ( "fmt" "math/rand" "sync/atomic" "time" ) func main() { args := []int{5, 2, 4, 1, 8} var indexGen atomic.Int32 indexGen.Store(-1) results := make([]int, len(args)) finished := make(chan bool) slowSquare := func(arg int, index int) { randomMilliseconds := rand.Intn(1000) blockDuration := time.Duration(randomMilliseconds) * time.Millisecond fmt.Printf("Squaring %d, Blocking for %d milliseconds...\n", arg, randomMilliseconds) <-time.After(blockDuration) idx := indexGen.Add(1) results[idx] = arg * arg fmt.Printf("Squared %d: results[%d]=%d\n", arg, idx, results[idx]) finished <- true } prettyPrinter := func() { for range time.NewTicker(time.Second).C { fmt.Println("Results: ", results) } } go prettyPrinter() for idx, x := range args { go slowSquare(x, idx) } <-finished fmt.Println("First Result: ", results[0]) fmt.Println("So-far Results: ", results) }
- sandreas 2y ago.NET 9 solves this with Task.WhenEach where you had to use some boilerplate before .NET 9. Nick Chapsas has an interesting youtube video on this explaining what's the problem and what Microsoft did to solve this[1]. 1: https://www.youtube.com/watch?v=WqXgl8EZzcs https://www.youtube.com/watch?v=WqXgl8EZzcs
- baranul 2y agoGood catch. Thanks for posting the video.
- kimi 2y agoHad a similar problem in Elixir - spawn a pool of HTTP requests and notify immediately of the first one that completes with a status of "ok". Task.Supervisor.async_stream( My.TaskSupervisor, my_list_of_urls, &assess_url_exists/1, ordered: false, max_concurrency: 100, timeout: 30_000 ) |> Enum.find(fn {:ok, {:ok, _url}} -> true _ -> false end) And - that's it. Do I want it ordered? just change the "ordered" flag. Tweaking concurrency? it's there. But the really best part is that any pending computation at the point when the correct result is found, it is automatically cancelled. No waiting for its completion. No memory leaks, no file descriptors left open. It just goes away. Boom. Just sayin'...
- tptacek 2y agoI don't understand why you would want an entire new execution scheduling mechanism just to print results in a specific order. The post dismisses the idea of including an index (a "sequence number" if you really want to be snooty about the idea of counting your requests), but "asynchronously waitable objects" is much, much more mechanism than that. The irony of all of this is that the core thing the post wants to do, of posting results in order to reduce confusion for users, is also probably (I don't know the complete use case, but in general) wrong! Print when you get the results back! Don't block results behind a slow early request! It's a command line program, what's the virtue of queueing fast responses? The only point of the program is to print the output.
- alexjurkiewicz 2y ago> The only point of the program is to print the output. A consistent structure helps humans parse the output. I have host(1)'s output order in my mental memory.
- tptacek 2y agoAnother way to think about this: if the only point of the program is to print the output, why not just serialize, or pipeline only one request past the current one? Or you could just use a sync.WaitGroup and not print anything until all the results are in, which is what the proposed design does anyways in the case where an early request is the slowest to resolve.
- klodolph 2y agoWhy not serialize / just print one? Parallel is faster. Why not wait? Because it’s nice to see results as they come in. Yes, there are pathological possibilities here, if an early request is slow. But this complaint sounds like a complaint about the author’s goals, which are stated clearly enough. “The author has the wrong desires or goals”, your comment seems to say.
- tptacek 2y ago
- eptcyka 2y agoI love how most of the responses to this article here blame the author for wanting the wrong thing. One could implement a future struct that is awaitable and then the scaffolding to wait on a bunch of them. I have done things like this in other languages, it can be immensely useful. Similarly, when some wanted generics, they were bemoaned for wanting the wrong thing. Go is a wonderful language, even if it is not my cup of tea. But must its users be this defensive?
- inopinatus 2y agoit is possible they fear being replaced with a few lines of erlang
- tptacek 2y agoI don't think this is defensiveness so much as that attempting to model promises in languages like Go will result in bad, nonidiomatic code, and that is a thing that happens semiregularly with Go in particular, as it has so many refugees from other languages with sharply different idioms. Here, we're discussing implementing future structs just to print DNS results in order. I don't think the UX proposed in the post is good, but stipulate that it is; this is not a hard problem to solve in idiomatic Go.
- arethuza 2y ago"refugees from other languages with sharply different idioms" Reminds me of the time I had to decipher some numerical analysis code written in Common Lisp but in the style of Occam...
- surfingdino 2y ago> refugees from other languages with sharply different idioms Mostly Java or JavaScript devs unable to make a dent in their main programming language space looking for a shortcut to jump to a better paying job by pretending to make a huge difference to the other programming language. I cringe every time a frontend dev who hasn't fully grasped React yet utters the words "I hear Rust is the future. I am going to port a JS package to it..." They have already started making inroads into Golang with half-implemented modules.
- carpdiem 2y agoLordy, I clicked on this, fully expecting a discussion of a shape-pattern in the board game of Go, and somehow didn't have a thought in my mind for the real, completely different, meaning. This reminds me of "trompe l'oeil foods"... dishes that appear to be one thing, but are another entirely. Or maybe optical illusions more generally.
- deleted 2y ago[deleted]
- taosx 2y agoThe way I would do it without seeing the implementation: - Atomic to track print order (name it counter). - Results slice stores items: {index, err, data}. - Goroutines: do work, store result, then loop-check if counter matches index to print (with tiny sleep). Edit: Another solution would be to put printing loop outside with a channel to signal work finished but I don't know how I would avoid using nested loops, one for the items in the list and one to check if the result is back.
- binary132 2y agoThat is unnecessarily complicated, and you don’t need a sleep or atomics.... Just make an array of result channels, and loop over it awaiting each result. Each result has a producer either working, or waiting for the channel reader to consume it.
- heavyairship 2y ago> However, this time around I wanted asynchronous requests but to be able to report on completed work in order. This seems similar to JavaScript's for await...of construct https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe.... A while ago I added a golang version here https://github.com/heavyairship/gopherawaitof https://github.com/heavyairship/gopherawaitof.
- grahamj 2y agoYeah this is a one-liner in JS for await of (const name in ips.map(getName)) console.log(name) Although you’d want more frills to deal with rejections
- remram 2y agoWhy use an array of channels when you can just have the goroutines write to a regular array?
- Jtsummers 2y agoYou'd have to poll something to determine that there was data written to the array, which means you'd need some mechanism (mutex, cv, channel) per entry. If you just constantly spin on the array entries you're wasting time, and you're introducing race conditions (distinguishing partial writes from completed writes). So your array would end up being something like [(bool, str)], with the bool set to true at the end of the write to signal completion. Now you're handling the race condition (probably) but you're still actively spinning, which is wasteful if the request takes very long. There's a reason people don't use spin locks much and use mutexes (which may spin, but it's been tuned and doesn't spin forever) instead. Channels end up being effectively a (bool,str) pair but instead of spinning the consumer blocks waiting for the channel to produce a value. This is simpler, and probably much more efficient than spinning on a boolean variable. NB: The author wants to print results, in order, as soon as possible, not delay until all results are available. If you can wait for all results before printing, then an array of results + waitgroup works fine. If you don't care about the order, then this can be much simpler.
- remram 2y agoThat's what WaitGroup is for. You can only get the full sorted array when every goroutine is done. You don't need to spin...
- Jtsummers 2y agoBut then you can't print them as they become available. Which was one of the two things the original author wanted: Print in order, but don't delay for all of them to be finished. He wants to make N requests, and print all N in the order made, not in the order of completion. And he wants to print them as early as possible, minimizing delay to first feedback (except in the worst case that the first request happens to be the last to complete, obviously). Literally quoting the OP: > I want asynchronous execution but to report the results in order, as each becomes available. 99% of the comments in this thread are people ignoring those two requirements. WaitGroup does let you get them in order (store in an array in order), but does not satisfy the second requirement. This isn't hard to understand.