Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: //go:async function annotation #69296

Closed
myaaaaaaaaa opened this issue Sep 5, 2024 · 4 comments
Closed

proposal: //go:async function annotation #69296

myaaaaaaaaa opened this issue Sep 5, 2024 · 4 comments
Labels
Milestone

Comments

@myaaaaaaaaa
Copy link

myaaaaaaaaa commented Sep 5, 2024

Overview

Given the following program that needs to be optimized:

$ cat main.go
package main

import (
	"fmt"
	"time"
)

func mapVal(i int) int {
	time.Sleep(time.Millisecond * 100)
	return i * 2
}
func reduceVal(a, b, c, d int) int {
	time.Sleep(time.Millisecond * 100)
	return a + b + c + d
}
func main() {
	start := time.Now()

	m1 := mapVal(1)
	m2 := mapVal(2)
	m3 := mapVal(3)
	m4 := mapVal(4)
	r1 := reduceVal(m1, m2, m3, m4)

	time.Sleep(time.Millisecond * 100)

	fmt.Println("sum:", r1)

	fmt.Println("elapsed:", time.Since(start))
}

$ go run main.go
sum: 20
elapsed: 602.876704ms

We would like to have the ability to mark any function that can run asynchronously, in a separate goroutine:

 $ cat main.go
 package main
 
 import (
 	"fmt"
 	"time"
 )
 
+//go:async
 func mapVal(i int) int {
 	time.Sleep(time.Millisecond * 100)
 	return i * 2
 }
+//go:async
 func reduceVal(a, b, c, d int) int {
 	time.Sleep(time.Millisecond * 100)
 	return a + b + c + d
 }
 func main() {
 	...
 }
 
 $ go run main.go
 sum: 20
-elapsed: 602.876704ms
+elapsed: 202.876704ms

With a feature like this, library authors would be able to easily provide speedups for their users simply by annotating non-racy functions.

Older versions of Go will gracefully ignore this //go:async annotation and run the functions serially, like before.

Users would not need to do anything to receive these benefits.

Mechanism

Upon encountering a function marked as //go:async, the compiler should generate a wrapper function that accepts and returns promises:

type Promise[T any] func() T

//go:async
func reduceVal(a, b, c, d int) int { ... }

func asyncReduceVal(aProm, bProm, cProm, dProm Promise[int]) Promise[int] {
	var ret int
	done := make(chan int)

	go func() {
		ret = reduceVal(aProm(), bProm(), cProm(), dProm())
		close(done)
	}()

	return func() int {
		<-done
		return ret
	}
}

Any place where a //go:async function is called directly should be rewritten to call the generated wrapper instead:

 func main() {
 	start := time.Now()

-	m1 := mapVal(1)
-	m2 := mapVal(2)
-	m3 := mapVal(3)
-	m4 := mapVal(4)
+	m1 := asyncMapVal(1)
+	m2 := asyncMapVal(2)
+	m3 := asyncMapVal(3)
+	m4 := asyncMapVal(4)
-	r1 := reduceVal(m1, m2, m3, m4)
+	r1 := asyncReduceVal(m1, m2, m3, m4)
 
 	time.Sleep(time.Millisecond * 100)
 
 	fmt.Println("sum:", r1)
 
 	fmt.Println("elapsed:", time.Since(start))
 }

Any use of a returned promise, other than storing to a variable or passing to another async function, must be rewritten to be awaited on:

 func main() {
 	start := time.Now()
 
 	m1 := asyncMapVal(1)
 	m2 := asyncMapVal(2)
 	m3 := asyncMapVal(3)
 	m4 := asyncMapVal(4)
 	r1 := asyncReduceVal(m1, m2, m3, m4)
 
 	time.Sleep(time.Millisecond * 100)
 
-	fmt.Println("sum:", r1)
+	fmt.Println("sum:", r1())
 
 	fmt.Println("elapsed:", time.Since(start))
 }

Any parameters passed to an async function call must be wrapped inside a promise

 func main() {
 	start := time.Now()
 
-	m1 := asyncMapVal(1)
-	m2 := asyncMapVal(2)
-	m3 := asyncMapVal(3)
-	m4 := asyncMapVal(4)
+	m1 := asyncMapVal(func() int { return 1 })
+	m2 := asyncMapVal(func() int { return 2 })
+	m3 := asyncMapVal(func() int { return 3 })
+	m4 := asyncMapVal(func() int { return 4 })
 	r1 := asyncReduceVal(m1, m2, m3, m4)
 
 	time.Sleep(time.Millisecond * 100)
 
 	fmt.Println("sum:", r1())
 
 	fmt.Println("elapsed:", time.Since(start))
 }

It's probably a good idea to await on all promises before the function returns, to prevent the control flow from becoming too confusing, but this isn't strictly necessary

 func main() {
 	start := time.Now()
 
 	m1 := asyncMapVal(func() int { return 1 })
+	defer m1()
 	m2 := asyncMapVal(func() int { return 2 })
+	defer m2()
 	m3 := asyncMapVal(func() int { return 3 })
+	defer m3()
 	m4 := asyncMapVal(func() int { return 4 })
+	defer m4()
 	r1 := asyncReduceVal(m1, m2, m3, m4)
+	defer r1()
 
 	time.Sleep(time.Millisecond * 100)
 
 	fmt.Println("sum:", r1())
 
 	fmt.Println("elapsed:", time.Since(start))
 }
@gopherbot gopherbot added this to the Proposal milestone Sep 5, 2024
@gabyhelp
Copy link

gabyhelp commented Sep 5, 2024

Related Issues and Documentation

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

@seankhliao
Copy link
Member

It is design decision that Go's primary concurrency primitive is not async.
I don't see this as compatible with idiomatic Go.

@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Sep 5, 2024
@myaaaaaaaaa
Copy link
Author

On the other hand, there's also plenty of demand for promises, as can be seen in a previous proposal:

Are you sure that this doesn't at least warrant some consideration, even if it were ultimately to be declined?

@seankhliao
Copy link
Member

from that previous proposal it seems quite clear that the language won't consider such a feature, it would be a library function, and we'd like to see widespread use of such third party libraries before we consider them in the standard library.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants