Hacker News new | past | comments | ask | show | jobs | submit login

You can completely ignore that pretty print function as it not essential to the goal.

Revised example without pretty-print at https://go.dev/play/p/LkAT_g95BLO

As soon as you have completed `<-finished` in the main go-routine, it means `results[0]` has been populated and is ready for read.

If you want to wait till all results are available, then perform `<-finished`, `len(results)` times. (Or use sync.WaitGroup)

    package main

    import (
     "fmt"
     "math/rand"
     "sync/atomic"
     "time"
    )

    func main() {
     args := []int{5, 2, 4, 1, 8}
     var resultCount atomic.Int32
     resultCount.Store(-1)
     results := make([]int, len(args))
     finished := make(chan bool, len(args))

     slowSquare := func(arg int, fnNum int) {
      randomMilliseconds := rand.Intn(1000)
      blockDuration := time.Duration(randomMilliseconds) * time.Millisecond
      fmt.Printf("(#%d) Squaring %d, Blocking for %d milliseconds...\n", fnNum, arg, randomMilliseconds)
      <-time.After(blockDuration)
      resultIndex := resultCount.Add(1)
      results[resultIndex] = arg * arg
      fmt.Printf("(#%d) Squared %d: results[%d]=%d\n", fnNum, arg, resultIndex, results[resultIndex])
      finished <- true
     }

     for i, x := range args {
      go slowSquare(x, i)
     }
     fmt.Println("(main) Waiting for first finish")
     <-finished
     fmt.Println("(main) First Result: ", results[0])
    }



Chris wants to report the results in the same order as the inputs. So 25, 4, 16, 1, 64. Your code will report the results in an arbitrary order.


Ok, but that is even more simpler and shorter with plain `sync.WaitGroup`.

    package main

    import (
     "fmt"
     "math/rand"
     "sync"
     "time"
    )

    func main() {
     args := []int{5, 2, 4, 1, 8}
     results := make([]int, len(args))
     var wg sync.WaitGroup

     slowSquare := func(arg int, resultIndex int) {
      randomMilliseconds := rand.Intn(1000)
      blockDuration := time.Duration(randomMilliseconds) * time.Millisecond
      fmt.Printf("(#%d) Squaring %d, Blocking for %d milliseconds...\n", resultIndex, arg, randomMilliseconds)
      <-time.After(blockDuration)
      results[resultIndex] = arg * arg
      fmt.Printf("(#%d) Squared %d: results[%d]=%d\n", resultIndex, arg, resultIndex, results[resultIndex])
      wg.Done()
     }

     for i, x := range args {
      wg.Add(1)
      go slowSquare(x, i)
     }
     fmt.Println("(main) Waiting for all to finish")
     wg.Wait()
     fmt.Println("(main) Results: ", results)
    }


    (main) Waiting for all to finish
    (#4) Squaring 8, Blocking for 574 milliseconds...
    (#1) Squaring 2, Blocking for 998 milliseconds...
    (#2) Squaring 4, Blocking for 197 milliseconds...
    (#3) Squaring 1, Blocking for 542 milliseconds...
    (#0) Squaring 5, Blocking for 12 milliseconds...
    (#0) Squared 5: results[0]=25
    (#2) Squared 4: results[2]=16
    (#3) Squared 1: results[3]=1
    (#4) Squared 8: results[4]=64
    (#1) Squared 2: results[1]=4
    (main) Results: [25 4 16 1 64]


As soon as results[0] is ready he wants to print it. Then as soon as results[1] is read he wants to print it. Etc. Not waiting till the end to print everything, and not printing anything out of order.


If results[i] must be printed only AFTER print of results[0]...results[i-1], then you effectively need to wait for max of (time to compute results[0]...results[i]), since even if results[i] is computed earlier you can't print it out if results[0]..results[i-1] are not available. If result[0] takes the longest compute time, then you will definitely need to wait till the end.

Frankly, a simple sequential for loop seems to the simplest solution here :)

Anyways, I think this: https://go.dev/play/p/lFBpzUVVzUj satisfies all the constraints. Only look at the output with the "(main)" prefix, the other prints are for elucidation.

        package main

        import (
         "fmt"
         "math/rand"
         "time"
        )

        type Result struct {
         value    int
         computed bool
         consumed bool
        }

        func main() {
         args := []int{5, 2, 4, 1, 8}
         results := make([]Result, len(args))
         signal := make(chan bool)
         signalCount := 0

         slowSquare := func(arg int, index int) {
          randomMilliseconds := rand.Intn(1000)
          blockDuration := time.Duration(randomMilliseconds) * time.Millisecond
          <-time.After(blockDuration)
          square := arg * arg
          results[index] = Result{value: square, computed: true}
          fmt.Printf("(#%d)   Squared %d, index=%d, result=%2d, duration=%s, sending signal. \n", index, arg, index, square, blockDuration)
          signal <- true
         }

         for i, x := range args {
          go slowSquare(x, i)
         }

         for {
          if signalCount == len(results) {
           break
          }

          <-signal
          signalCount++

          for i := 0; i < len(results); i++ {
           if !results[i].computed {
            break
           }
           if !results[i].consumed {
            fmt.Printf("(main) Squared %d, index=%d, result=%2d\n", args[i], i, results[i].value)
            results[i].consumed = true
           }
          }
         }

        }

Example Output

        (#0)   Squared 5, index=0, result=25, duration=8ms, sending signal. 
        (main) Squared 5, index=0, result=25
        (#4)   Squared 8, index=4, result=64, duration=210ms, sending signal. 
        (#1)   Squared 2, index=1, result= 4, duration=777ms, sending signal. 
        (main) Squared 2, index=1, result= 4
        (#3)   Squared 1, index=3, result= 1, duration=867ms, sending signal. 
        (#2)   Squared 4, index=2, result=16, duration=924ms, sending signal. 
        (main) Squared 4, index=2, result=16
        (main) Squared 1, index=3, result= 1
        (main) Squared 8, index=4, result=64


That code has a race condition. One goroutine can modify an element of results at the same time a different goroutine is reading it. Running with `go run -race` detects the race.

This can be fixed with a mutex.

Now that we've got working code (with the mutex), we have to ask: have we proved Chris wrong? I don't think so. Chris never said it's impossible to implement this in Go. Chris just said that implementing it in Go is uglier than using an array of promises in some other language. And I think this code is uglier than an array of promises.

Although I think I disagree with Chris. He says that an array of channels is significantly uglier than an array of promises. I don't think so. I think an array of channels is fine (and is easier to understand than the signal, signalCount, computed, consumed, +mutex code).

    package main

    import (
     "fmt"
     "math/rand"
     "time"
    )


    func main() {
     args := []int{5, 2, 4, 1, 8}
     results := make([]chan int, len(args))

     slowSquare := func(arg int, index int) {
      randomMilliseconds := rand.Intn(1000)
      blockDuration := time.Duration(randomMilliseconds) * time.Millisecond
      <-time.After(blockDuration)
      square := arg * arg
      fmt.Printf("(#%d)   Squared %d, index=%d, result=%2d, duration=%s, sending signal. \n", index, arg, index, square, blockDuration)
      results[index] <- square
     }

     for i, x := range args {
      results[i] = make(chan int, 1)
      go slowSquare(x, i)
     }

     for i := 0; i < len(results); i++ {
      fmt.Printf("(main) Squared %d, index=%d, result=%2d\n", args[i], i, <-results[i])
     }
    }


Yes, I didn't quite get why he wanted a solution without multiple channels..it is clearly the best way. The only other option without multiple channels is to use one of the atomic types: `sync/Atomic.Int32` (to avoid the mutex) and then a single channel for signalling.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: