golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.07k stars 17.68k forks source link

Proposal: add "future" internal type (similar to channel) #17466

Closed funny-falcon closed 6 years ago

funny-falcon commented 8 years ago

"Future" is commonly used synchronization primitive used in many languages and environments. It allows several "receivers" to wait "computaion" of a single value, and single "receiver" to wait for several values to be computed using same convenient construction. Although Golang already has channels, it is more like "stream/queue of values" instead of "single value to be computed". To make several "receivers" to wait on single value, one ought to use relatively complex wrappers around channel (usually, waiting for channel to be closed). And usually it lacks of type information of value cause uses interface{} for value.

Proposal

Add future primitive derived and equal rights to channels. Obviously, it will share representation and a lot of code with channels.

    f := make(future string)
    var rf <-future string = f
    var sf future<- string = f
    f <- "hello"
    s := <- f

Note: some names construction similar to future as 'I-var' - "immutable variable". Looks like, it will introduce less name collisions with existing code, so type could be named as ivar:

    f := make(ivar string)
    var rf <-ivar string = f
    var sf ivar<- string = f
cznic commented 8 years ago

What this proposal buys wrt to proper use of sync.Once?

Merovius commented 8 years ago

Futures are a step back for go. They are added to other languages, which are lacking proper concurrency support, to make code look more sequential, so they replace callbacks and event loops. In go, the propper way to make your code look sequential, is to write it sequentially in the first place.

So, I don't think this is a good addition. I believe there are very view proper use cases for them (like, "to do a thing, I first need to kick of thing A, then do lots of other stuff and then wait for A to finish to use the result" is relatively rare).

For the rare cases where futures are useful, I'd rather suggest something like this:

func ThingA() func() string {
    ch := make(chan string)
    go func() {
        ch <- doThing()
        close(ch)
    }
    return func() { return <-ch }
}

which seems wordy; but that's fine as it shouldn't be necessary that often anyway. And ThingA can then be used like returning a proper future, with very little syntactic overhead.

Merovius commented 8 years ago

Also, the fact that you mention yourself how much functionality, code and syntax they'd share with channels, shows, that they are not at all orthogonal to them (they are pretty much colinear) and as such, the addition would violate one of the core design principles of go.

funny-falcon commented 8 years ago

@cznic futures are selectable, so you may combine them with timeouts, other futures and channels
@Merovius channels are not the same as a future, cause many receivers may wait for a same value from a future, but is is hard to wait same value from a channel.

Example:

type usersMap struct {
  m sync.Mutex
  u map[int] future User
}
var users = usersMap{ u: make(map[int] future User }
func userFuture(uid int) (f future User) {
    users.m.Lock()
    if f, ok := users.u[uid]; !ok {
        f = make(future User)
        go func() {
            f <- findUser(uid)
        }()
        users.u[uid] = f
    }
    user.m.Unlock()
    return f
}
func getWithTimeout(uid int, timeout time.Duration) (u User, err error) {
    f := userFuture(uid)
    to := time.NewTimer(timeout)
    select {
    case u = <- f: // that is how it differs from sync.Once()
        return u, nil
    case <-to.C:
        err = TimeoutError
        return
    }
}
// that is why channel is not suitable (several receivers of same value)
go func() {
   if user, err := getWithTimeout(1, time.Second); err != nil {
     sendToParis(user)
   }
}()
go func {
   if user, err := getWithTimeout(1, time.Millisecond); err != nil {
      giveAPresent(user)
   }
}()

It could be implemented with more wrapper code, as example: https://github.com/Workiva/go-datastructures/blob/master/futures/selectable.go But if it were internal type, then it will be more elegant and type-safe.

andlabs commented 8 years ago

I have a blog post pending on why JavaScript's futures (the Promise object) do not actually solve any problems (or at least any of the problems they aim to solve). Let's not ruin Go, please.

As for the case with "many receivers may wait for a same value from a future", use a RWMutex and release the W lock when the value is ready. Or cancel a context and store the result in a Value. Or maybe something in one of the many concurrency talks the Go team have made; I'm sure there's something somewhere.

funny-falcon commented 8 years ago

@andlabs I already know how to build future with code (as wrapper around mutex and channel) (and I've already posted a link above to such code). But every such code is inelegant and not type-safe.

I'm not propose bringing Javascript's futures/promises to Go. No "then+catch" callbacks.

This proposal is about bringing "Future" abstraction to language in a way suitable for Go language. And this abstraction is really useful.

Merovius commented 8 years ago

You can also do

func ThingA() (func() string, <-chan bool) {
    ch := make(chan bool)
    var s string
    go func() {
        s = doExpensiveThing()
        close(ch)
    }()
    return func() string {
        <-ch
        return s
    }, ch
}

and get selectability and an arbitrary number of receivers (and yes, I am aware that this is used syntactically different from what you propose).

However, that's not even the larger point. The larger point is, that they are a) not necessary, because the problem they usually are added for doesn't really exist in go and b) they are not orthogonal to channels. That c) you can already emulate them, which means they don't actually add more power to the language is, at least to me, more of a side-note.

Another issue I have with futures are, that they are poisonous; when your function returns a future, I suddenly need to care about concurrency when I use your stuff, whether I want to or not (and even if that only means adding an arrow). Whereas, if you just, like recommended in idiomatic go code, write your function to be synchronous and return a value, I can choose myself whether I want to call it asynchronously or keep my life simple by just treating it as a normal function call.

Thus, in a world with futures, soon all go code will be riddled with futures, because people decide they need to return a future whenever they do anything involving files or the network (and they are only really useful if returned by the function) and then all go code gets riddled with arrows, because in practice, people need the result of a function call immediately most of the time anyway.

Just write your code as taking a context.Context for cancellation and returning a regular value. And in the 1% of cases where futures are useful, people can add the two lines of code necessary for most of those use cases themselves. Or the twelve lines of code for the 1% of the 1% where you actually need selectability or multiple receivers.

funny-falcon commented 8 years ago

@Merovius your code looks like a simplest wrapper code for making future :-) But still you not convince me.

Converting "synchronous code" to "asynchronous" usually means running goroutine for every "synchronous->asynchronous conversion". It is really expensive and scales bad.

Practice shows that "asynchronous first" API is more scalable. Providing useful primitive for will lead to more scalable api, and it is good, IMHO.

For example, one kv storage has asynchronous network protocol and allows to send asynchronous requests. If one need to send bunch of independent queries (for example, batch insert, or batch fetch for meany ids), then he should send asynchronous requests to maximize throughput and reduce total time of computation.

But if library provides only synchronous API, then one should wrap every request with goroutine. Other option is "batch api", which will be very different from "regular api", and will duplicate functionality.

But if library provides "asynchronous first" api with futures, then it is only api every user need to use.

funny-falcon commented 8 years ago

proposal for select on conditional variables from @bradfitz looks similar in spirit:

16620

bradfitz commented 8 years ago

I've wanted something this (auto-pumped, single value 1:many channels) a number of times in the past too.

It might be nicer if it could be done without a language change, though. Maybe the runtime or reflect packages could just be extended a bit to provide a way to let this "future" behavior be provided by a normal package.

funny-falcon commented 8 years ago

@bradfitz, but giving Go has no generics, single way to make it type-safe is to change language a bit. I've tried to make this change as small as possible, reusing most of syntax and semantic from channels.

bradfitz commented 8 years ago

@funny-falcon, it's not the "single way". There are plenty of other ways one might imagine. Here's one example, which I'm not saying is good, but I'm giving as proof that a language change is not the "single way":

package runtime

// RegisterOnEmpty registers that the runtime should call fn when the channel
// given in ch transitions from 1 buffered element to 0. It panics if ch is
// not a channel or ch is not a buffered channel.
func RegisterOnEmpty(ch interface{}, fn func()) {
   // ....
}

Then you make a future in a regular package, and do:

package github_foo_future

// Set sets the value of the futureChan to value.
// It sends value and continues to send the value as it is received.
// Set panics if futureChan is not a buffered channel.
func Set(futureChan, value interface{}) {
     cv := reflect.ValueOf(futureChan)
     vv := reflect.ValueOf(value)
     send := func() { cv.Send(vv) }
     runtime.RegisterOnEmpty(futureChan, send)
     send()
}

Again, I'm not saying that's a good solution, but it's a solution that doesn't require a language change. (only a runtime/API change)

funny-falcon commented 8 years ago

@bradfitz this code has race-condition between fetching value and re-sending it : other goroutine may send other value to channel. And it is not "typesafe", cause it still uses interface{}, so compiler will not complaint on type error (ok, it is "runtime typesafe", but not "compile-time typesafe").

Instead of separate internal type it could be just a flag inside of channel. For example, "make" may accept some magic constant (probably, of non-integer type) to indicate value is a future (it will be translated by compiler to different function call).

    var f chan string
    f = make(chan string, runtime.ChanIsFuture)

But it will hide differences in "send" semantic from a compiler ("send" to future inside of "select" is restricted to non-blocking single case with default), and the fact "close" on future is meaningless.

andlabs commented 8 years ago

@bradfitz for what we want: what about a func reflect.DupChan(channel reflect.Value, n int) []reflect.Value or something like that? It takes ownership of the given (receivable) channel and returns a bunch of slice of receive-only channels that mirror anything sent on the original. These calls can be chained if more channels are needed. Closes are also reflected (so to close the whole tree you would close the original channel).

bradfitz commented 8 years ago

Like I said, I never claimed my counter proposal was good. It was just to show that you don't need language changes. Similarly, you could go a step further and replace your just-proposed make flag with a runtime call too:

package runtime

func SetFutureMode(ch interface{}) { ... }

But yes, it panics at runtime if you pass in a non-channel to it.

We've gone this long without futures, though, so I don't think it's worthwhile to add them in a hacky way, and it doesn't seem worth changing the language for them either.

I'd rather wait for a "Go 2" and clean up a bunch of stuff, considering it all at the same time, rather than misc ad-hoc changes like this one.

funny-falcon commented 8 years ago

@bradfitz SetFutureMode was my first thought, but flag for make looks clearer.

"Go 2" is so mythical creature, so no one expects it will be born. But even for "Go 2" there should be "list of stuff to cleanup". So, there should be proposal and agreement on it (to accept or not to accept)

And I still hope, it has chance to land before "Go 2" :-)

funny-falcon commented 8 years ago

it could be even "completely magic" flag, i.e. counted as syntax construction without real constant defined:

   f = make(chan string, future) // future is not a constant, but part of syntax
Merovius commented 8 years ago

@funny-falcon I find that really unconvincing

Converting "synchronous code" to "asynchronous" usually means running goroutine for every "synchronous->asynchronous conversion". It is really expensive and scales bad.

This may sound like an argument, but it really isn't. If you have a feature, you already need to run stuff in a separate goroutine. The order of concurrency is untouched by whether a function does stuff in a goroutine and sends on a future, or whether the goroutine is created by the caller and gets send on a channel.

Practice shows that "asynchronous first" API is more scalable.

[citation needed]

For example, one kv storage has asynchronous network protocol and allows to send asynchronous requests. If one need to send bunch of independent queries (for example, batch insert, or batch fetch for meany ids), then he should send asynchronous requests to maximize throughput and reduce total time of computation.

Again, this makes no sense. I don't think overall the code will be simplified, if you end up with a slice of futures that you need to wait on and convert to values.

But also, I never claimed there are no use cases for futures; I just said that I think a) they are relatively rare, b) they are better served by letting the user handle that concurrency themselves and c) that it's not worth poisoning most users of that code to make those few use cases minimally shorter.

In fact, this specific example, more or less, is what gave me such a strong opinion on futures in the first place. Because hashicorp's raft-implementation uses futures extensively, which now meant that we needed to riddle our source code with code to juggle those futures, even though they had literally zero use to us. We never made any call, that we didn't immediately needed the result to anyway. But because it's a future, you need to put it into a dedicated variable, then check the error, then extract the value, instead of something like res, err := r.Apply(cmd []byte).

Which also shows the next flaw of this proposal (not, that you had actually argued against any of the other ones mentioned so far): Probably 99% of the use cases of futures will require to also signal an error condition (because they'll involve the network or something like that, otherwise, why use a future in the first place). With a synchronous API, that is simple enough to do; you just return (res Result, err error) and I use an errgroup to handle the error. With futures, you need to actually wrap your result and error in a dedicated struct, which you can then send over the channel, or (ugh) return a separate error future, only send on one of them and hope, that you don't end up with leaks, because some reader is blocking on the wrong one.

Anyway. I think it's best for everyone if I leave this issue. I will be sincerely sad if this gets accepted, because the whole idea of futures has just so much wrong with it…

funny-falcon commented 8 years ago

@Merovius

This may sound like an argument, but it really isn't. If you have a feature, you already need to run stuff in a separate goroutine. The order of concurrency is untouched by whether a function does stuff in a goroutine and sends on a future, or whether the goroutine is created by the caller and gets send on a channel.

No, you need only working goroutines, no need goroutine per future. Given that kv storage with async protocol, there is only three goroutines: one for writing to tcp socket, one for reading from tcp socket and one waiting for "closing" signal from control channel. And single reading goroutine is responsible for filling all the futures. In fact, library for this kv storage first implements synchronous api, but users ask to provide async or batch api. It were decided to implement async api as more flexible. Synchronous api then were implemented on top of asynchronous api. It were decided to leave synchronous api inplace just for backward compatibility, though it were clearly redundant.

Again, this makes no sense. I don't think overall the code will be simplified, if you end up with a slice of futures that you need to wait on and convert to values.

Yes, but it is simpler than batch api, cause then you need a) separate batch api, b) you still need to convert array of interfaces to exact results. More often, it doesn't matter in your code: iteration over slice of futures is almost the same as iteration over slice of exact results, cause usually you fetch value once (from future or directly from slice), and then you do some operation over it.

You are right about returning error: one should pack it in a struct. Similar thing one ought to do if you provide slice or channel of independent result values which could be accompanied with error. While it is better to couple future with error passing, I've tried to make least invasive proposal.

Usually, when I implement future as a code, I give it method Result() (v inteface{}, err error), so there is no need for separate variable:

    if v, err = r.Action(args).Result(); err != nil {
        return err
    }
    useValue(v.(ActualType))

But it looks not much worse with wrapped struct:

    if res = <-r.Action(args); res.err != nil {
        return res.err
    }
    useValue(res.v)

As a variant, there could be "more invasive" proposal for allowing tuple type as a future value type:

    func Greating() (f fusture (string, error)) {
        f = make(future (string, error))
        go func() {
           if moonIsYoung() {
               f <- ("hello", nil)
           } else {
               f <- ("", errors.New("goodbuy")}
           }
        }()
        return f
    }
    if str, err = <-Greating() ; err != nil {
        return fmt.Sprintf("%v cruel world", err), err
    } else {
        return fmt.Sprintf("%s wonderful world", str), nil
    }

This tuple will be backed by unnamed struct type inferred by compiler. But, certainly such proposal has less chances to be accepted.

the whole idea of futures has just so much wrong with it…

It is just your experience. I've used futures for different tasks where they were suitable, and they always leads to more flexible and performant code. As I mentioned in a proposal header, usually futures are suitable when both cases could happen simultaneously:

Looks like you rarely face such requirements, but I face them quite often. Edit: excuse me for last sentence.

andlabs commented 8 years ago

Actually I originally deleted it thinking it was irrelevant but: necessary reading

(Go's colorlessness is one of those things that people don't talk about Go or realize is a thing about Go except in very specific points of hindsight. I really need to make a list of all these cases.)

funny-falcon commented 8 years ago

@andlabs, I read it, and it is irrelevant. "future" as proposed here doesn't hurt Go. It has same spirit as channel, it doesn't change "color of function", cause waiting on "future" is as blocking as waiting on "channel" is (uses same syntax and composable with channels using select). This proposal doesn't change language much, doesn't introduce "new way of writing code". It is just convenient and useful synchronization primitive made in spirit of Go.

ianlancetaylor commented 8 years ago

This is easily written in Go, except that it is not compile-time-type-safe. So, as for many people who want to write general purpose data structures in Go, I think this boils down to another request for some sort of generics.

funny-falcon commented 8 years ago

@ianlancetaylor , I agree, life with generics will be much easier. But this is not only "another request for ... generics".

Go is a language tailored towards concurency. It has light threads, channels. It even has mutexes and condition variables.

But it lacks well known, useful and broadly adopted (though in many different forms) communication and synchronization primitive - future. Given, its runtime implementation gently fits in already written code of channels, why not introduce it?

Why people need to implement it over and over, often with mistakes and in-efficiency? People, who understand what futures are for, write very high-loaded services/storages. They need fast and correct solution.

DmitriyMV commented 8 years ago

I don't like the idea of adding new semantics to :=<- and <- operations. It will make code harder to read (which we value in Go), because future mechanics are quite different from chan. I also don't see how resulting solution is any better than sync.Once + done chan. I agree that sync.Once + done chan requires a bit of boilerplate code, but that is another discussion (generics, lol).

djadala commented 8 years ago

does this implement all aspects of future ?

type future struct {
    w chan struct{}
    value interface{}
    once sync.Once

}

func NewFuture() *future {
    return &future {
        w: make(chan struct{}),
    }
}

func (f *future) Send( value interface{}) {
    f.once.Do( func() {
        f.value = value
        close( f.w)
    }
}

func (f *future) Receive() interface{} {
    <-f.w
    return f.value
}

// if you want channel to select on
func (f *future) Chan() chan <- struct{} {
    return f.w
}

// called after select on Chan()
func (f *future) Read() interface{} {
    return f.value
}
funny-falcon commented 8 years ago

@DmitriyMV semantic of :=<- and <- will not change much with this proposal.future mechanics is quite close to chan (though, is not equal). Difference: receive from future will not pop value from buffer, and send to future will awake all waiters as close on chan does.

@DmitriyMV @djadala sync.Once + done chan doesn't solve all problems that could be solved with future. See example for kv-storage: task is passed to "writting" goroutine, future is stored in a hash, and "reading goroutine" fills future. So there is no place for "sync.Once". It is not "callback".

@djadala There could be a lot of ways to reimplement future. But my point, that 99% of functionality is already in chan. Instead of reimplementing wheel in many different way, it is better to have one "blessed" implementation.

Do you like "chan"? I like it, cause it well tested, optimally implemented, and typesafe. I believe that "chan" is reliable. Sometimes someone wants something faster than "chan", or unlimited buffer, then he reimplements "chan". But 99% of us are satisfied with "chan" as a message queue.

I wish there will be same "strong and reliable" "future": without redundant "interface{}", without redundant mutexes (sync.Once uses mutexes, but hey, "chan" also uses mutexes, so why use another one?), with type-safety provided by compiler. I wish 99% of a people, who need "future", will have it reliable.

DmitriyMV commented 8 years ago

@funny-falcon One could write code like this:

f := make(future string)
f <- "Value"
s1 := <- f
s2 := <- f
s3 := <- f

And this will be a valid code. Change future to chan and it's becomes invalid (deadlock).

Also, from the internals point of view, IIRC chan\goroutine mechanic is quite straightforward. One send - one awake. Future will require setting value, setting "done" byte and releasing all waiting goroutines.

As for your example - you can use "close mechanics" of the channels. Simply change the f to chan struct{}, add data and error fields. Set them in "writer" goroutine and close the channel. Any subsequent attempt to read from the f will return struct{}{}, false, so your select block will automatically choose this case. Get the data/error value from the fields. Additional info can be found here: http://dave.cheney.net/2014/03/19/channel-axioms

DmitriyMV commented 8 years ago

As it stands currently - the only bad thing about current future design is that you either required to have implementation for each type of future, or rely on interface{} and type assertions. Both are valid points for having more "advanced" type system, rather than having new category of channels. But this is a topic for another discussion.

funny-falcon commented 8 years ago

@DmitriyMV

As for your example - you can use "close mechanics" of the channels.

Thank you, captain obvious.

bradfitz commented 8 years ago

Please keep this civilized. I'm going to lock this thread now. I believe all viewpoints have been heard at this point.

AlekSi commented 8 years ago

@funny-falcon You can close it now.

Personally, I encourage you to implement futures as a separate library: with clean and clear API, good documentation, good performance. Then propose it to /x/sync. I would like to see a single best implementation there rather than 20 half-baked third-party libraries.

dahankzter commented 7 years ago

Futures can indeed simplify code in some cases but the thing is that soon all your functions take and returns futures and the signatures and the meaning they confer is lost in the noise of all the futures.

I would really not like it if libraries started working with futures instead of straight up functions using real values that can easily be made concurrent as needed.

andlabs commented 7 years ago

Also worth keeping in mind (or to elaborate on the last sentence of the above comment) is that Go doesn't force you to choose a synchronous or asynchronous model:

func Sync() int
func Asyncify(s func() int) <-chan int {
    c := make(chan int)
    go func() { c <- s(); close(c) }()
    return c
}
// and if you are really pedantic about copying terminology you can throw this in too
func Await(c <-chan int) int { return <-c }

func Async() <-chan int
func Syncify(a func() <-chan int) int {
    return <-(a())
}

This blog post sums it up best, especially the line

Go has eliminated the distinction between synchronous and asynchronous code.

Ideally, a futures-in-Go system should have the same orthogonality.

DeedleFake commented 6 years ago

Not an argument for or against futures, but just an idea of a possible syntax that could be used for them that might fit better with the existing system: Allow close() to take an optional second argument of the channel's element type. If that argument is given, reads from the channel yield that value instead of the zero value. So, for example, the following contrivance would wait a second and then print 3:

c := make(chan int)
go func() {
    time.Sleep(time.Second)
    close(c, 3)
}()
println(<-c)

Edit: I just realized that this could be used to 'fix' the broadcasting 'problem' that channels have. One to many is annoyingly difficult to do properly in Go and very heavy on boilerplate. This would allow you to do the following:

type Data struct {
    Val  int
    Next <-chan Data
}

func Broadcast() <-chan Data {
    c := make(chan Data)
    go func(c chan Data) {
        for i := 0; true; i++ {
            time.Sleep(time.Second)

            next := make(chan Data)
            close(c, Data{
                Val:  i,
                Next: next,
            })
            c = next
        }
    }(c)
    return c
}

func main() {
    c := Broadcast()

    for i := 0; i < 3; i++ {
        go func(c <-chan Data) {
            for {
                data := <-c
                fmt.Println(data.Val)
                c = data.Next
            }
        }(c)
    }

    select {}
}
ianlancetaylor commented 6 years ago

I continue to believe that this can be implemented completely using generics, if we had generics. It could reasonably be added to the standard library. As a language feature, there is considerable overlap with channels, and I don't think it adds sufficient value over channels, or is sufficiently different from channels, to merit being added to the language itself.

Even without generics, it's easy to write using channels (as seen above), either using a known type, or using an interface version that is not compile-time-type-safe. This does not cross the threshold for a language feature.

The fact that many other languages offer futures is not convincing, since those languages generally do not offer channels. We don't add language features just because other languages have them.

Therefore, closing.