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

runtime/mainthread: new package to manage main thread #64777

Open
eliasnaur opened this issue Dec 17, 2023 · 117 comments · May be fixed by #69185
Open

runtime/mainthread: new package to manage main thread #64777

eliasnaur opened this issue Dec 17, 2023 · 117 comments · May be fixed by #69185
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. Proposal Proposal-Accepted
Milestone

Comments

@eliasnaur
Copy link
Contributor

eliasnaur commented Dec 17, 2023

Update The proposal is #64777 (comment).

Proposal Details

This proposal is a less flexible but perhaps easier to implement and maintain alternative to #64755. See that issue for background and motivation for non-main packages to take control of the main thread.

I propose adding a new function, runtime.RunOnMainThread, for running a function on the startup, or main, thread. In particular,

package runtime

// RunOnMainThread runs a function immediately after program initialization on a
// goroutine wired to the startup thread. It panics if called outside an init
// function, if called more than once, or if [runtime.LockOSThread] has already
// been called from an init function.
// Once RunOnMainThread is called, later LockOSThread calls from an init function
// will panic.
func RunOnMainThread(f func())

This is the complete proposal.

Variants

Just like #64755, an alternative spelling is syscall.RunOnMainThread optionally limited to GOOS=darwin and GOOS=ios.

@aclements
Copy link
Member

The combination of "It panics if called outside an init function" and "[it panics] if called more than once" is rather unfortunate. If a project happen to (perhaps transitively) pull in two packages that both might need main thread functionality, but the project doesn't actually need that functionality from both packages, they're now in a pickle because the mere act of importing the package must run init functions, and those must call RunOnMainThread if there's any chance the package may need to use the main thread.

@eliasnaur
Copy link
Contributor Author

eliasnaur commented Dec 18, 2023

Here's a neat variant that you may like better:

// RunOnOSThread runs the function in a new goroutine
// wired to the thread of the caller. If the caller has locked
// the thread with `LockOSThread`, it is unlocked before
// returning. The caller continues on a different thread.
func RunOnOSThread(f func())

The advantages are:

  • No panics nor blocking.
  • Works any time for any thread, not just the main.
  • Complements LockOSThread.
  • Composable: multiple callers don't interfere with each other.
  • Easier to implement than LockMainOSThread (I believe). In particular, RunOnOSThread performs a context switch similar to what the runtime will do to make iterators efficient.

The disadvantages are:

  • Explicitly starting a goroutine is a weird API, but contrary to the original proposal, the function is executed immediately.
  • Calling RunOnOSThread during init switches the remaining initialization to a different thread. If this is unacceptable, we could say that
// RunOnOSThread panics if run from an init function.

at the cost of a panic condition and forcing main thread APIs to require some call from the main goroutine. E.g.

package app // gioui.org/app

// Window represent a platform GUI window.
// Because of platform limitations, at least once one of Window's
// methods must be called from the main goroutine.
// Otherwise, call [Main].
type Window struct {...}

// Main must be called from the main goroutine at least once,
// if no [Window] methods will be called from the main goroutine.
func Main()

@ianlancetaylor ianlancetaylor moved this to Incoming in Proposals Jan 2, 2024
@eliasnaur eliasnaur added the compiler/runtime Issues related to the Go compiler and/or runtime. label Jan 20, 2024
@eliasnaur
Copy link
Contributor Author

Gentle ping. I believe #64777 (comment) addresses the objections @aclements brought up here and on #64755.

@ianlancetaylor
Copy link
Contributor

As I understand it the goal here is for an imported package to be able to run non-Go code on the initial program thread. The RunOnOSThread function variant is kind of useless if run on anything other than the initial program thread; if you don't care that thread you are on, it could be replaced by a go statement that starts by calling runtime.LockOSThread. So RunOnOSThread seems like a confusing way to accomplish the goal.

Honestly it does not seem so terrible to me if an operation that has to take place on the initial program thread requires some cooperation from the main function. I understand that that is a bit annoying. But it's a bit annoying for frameworks to require running on the initial program thread.

@eliasnaur
Copy link
Contributor Author

As I understand it the goal here is for an imported package to be able to run non-Go code on the initial program thread. The RunOnOSThread function variant is kind of useless if run on anything other than the initial program thread; if you don't care that thread you are on, it could be replaced by a go statement that starts by calling runtime.LockOSThread. So RunOnOSThread seems like a confusing way to accomplish the goal.

RunOnOSThread tries to juggle several goals: orthogonality, few special cases, easy to describe, implement and maintain. I think it achieves those goals fairly well, but I understand if you think it's too general for its special purpose. Would placing it in package cgo, syscall, or x/sys make a difference? Would spelling it RunOnMainThread and panicing if called without the main thread wired?

Honestly it does not seem so terrible to me if an operation that has to take place on the initial program thread requires some cooperation from the main function. I understand that that is a bit annoying.

The number of direct uses of RunOnOSThread is probably low, say a handful of distinct cases. But please consider the larger indirect impact: every GUI program and Windows service written in Go (at least).

The alternative is not entirely trivial either. Consider a straightforward CLI tool that you want to add an optional GUI to, or a CLI service you optionally want to run as a Windows service. You then have to change your program flow, e.g. rewriting your logic as an interface with callbacks, and you must call the API from your main function. Both requirements are alien to Go programmers not familiar with the underlying frameworks.

Case in point, it took me a while to figure out why svc.Run didn't work from a goroutine and I'm used to macOS main thread APIs. Further, the changes to turn my otherwise straightforward http.ListenAndServe program to conform to svc.Handler were frankly obnoxious and felt disproportional to the effect.

But it's a bit annoying for frameworks to require running on the initial program thread.

So let's not pass on the annoyance to Go programmers :-)

Go often makes quality-of-life changes that are not strictly necessary, sometimes at non-trivial cost: cgo.Handle, replacing // +build with the more intuitive //go:build, runtime.Pinner come to mind. I hope RunOnOSThread (or something with similar effect) can be another such change, delighting programmers by making their programs simpler.

@hajimehoshi
Copy link
Member

RunOnMainThread is very useful to improve user experience with some UI libraries. Now we don't have it, we have to force users to call some functions (e.g. Gomobile's app.Main) on the main thread, which is the initial OS thread on init/main functions.

@eliasnaur
Copy link
Contributor Author

I believe a fix to #67499 will enable iter.Pull to be used to share the UI main thread between user code and the system event loop. Perhaps that's enough for your use cases?

@hajimehoshi
Copy link
Member

Thanks, but wouldn't I still need to call iter.Pull at the head of the main function, which is the same restriction as app.Main?

@eliasnaur
Copy link
Contributor Author

Yes, something would have to drive the iter.Pull iterator that controls the main thread. The difference is that you can pass control back to the user and thus (almost) hide the main thread limitation from them.

For Gio, I plan to keep app.Main as is, but also make the blocking app.Window.Events API start (and drive) the main event loop if called from the main goroutine. The only user-visible restriction is that if they don't call app.Main, they need to run event handling from the main goroutine for at least one window. Basically, the main goroutine must be blocked in the app package somehow.

@hajimehoshi
Copy link
Member

Isn't this proposal RunOnMainThread enabling the function call on the main thread without any explicit initialization like app.Main at the head of func main?

@eliasnaur
Copy link
Contributor Author

eliasnaur commented May 29, 2024

Yes, but it seems unlikely to be accepted now that RunOnMainThread can (almost) be achieved with iterators, see the example at #67499 (comment) where there's no explicit initialization call. With #67499 fixed, the only limitation of the iterator approach is that users must call a function in your main-thread API to advance the platform specific main event loop. Unlike app.Main that function may return control to the user at each event. In case of Gio, Window.Events naturally fits that role.

It's even possible to eliminate the restriction and "steal" the main thread during, say, a package init. See #67694.

In light of the above, and assuming rangefuncs and iter.Pull are released in Go 1.23 or later, I'm closing this in favor of #67694.

@hajimehoshi
Copy link
Member

One possible thing RunOnMainThread could do and iter.Pull could not is that RunOnMainThread can be called even from the main thread without deadlock by detecting whether the current running thread is the main thread or not, but iter.Pull could cause a deadlock IIUC.

@eliasnaur
Copy link
Contributor Author

Can you elaborate? I don't think iter.Pull itself can ever deadlock, regardless of #67694.

@aclements
Copy link
Member

Yes, but it seems unlikely to be accepted now that RunOnMainThread can (almost) be achieved with iterators

Honestly, I would rather have the right dedicated API for this than have to do anything weird in iter.Pull to make this work (if it was a happy coincidence that iter.Pull made this possible, that would be a different matter, but we always seem to be a few steps away from that).

Your RunOnOSThread proposal is certainly interesting, but I think it has a few blocking issues. With your proposed semantics, if some package were to call it from init, the rest of the init functions wouldn't run on the locked main thread, which I would consider to break the current guarantee that init functions run locked to the main thread. For example, we currently guarantee that user code cannot "undo" that lock. This would give them a way to undo that lock. So, I think it would have to panic if called from init, as you suggested. But that's both a half solution, forcing users to call something from main, and an odd limitation on composability.

@mknyszek and I were trying to answer the question, "if you could have a dedicated API for this, what would be ideal?" and we came up with the following API. I don't know if this is practical to implement; it might be a heavy lift.

// RunOnMainThread starts a new goroutine running f that is
// locked to the main thread started by the OS.
//
// This is intended only for interacting with OS services that
// must be called from the main thread.
//
// There can be multiple main thread goroutines and the Go
// runtime will schedule between them when possible.
// However, if a goroutine blocks in the OS, it may not be
// possible to schedule other main thread goroutines.
func RunOnMainThread(f func())

@aclements
Copy link
Member

@mknyszek and I were discussing how to implement this and I wanted to capture our thoughts:

  • These main thread goroutines are locked to the main M. We try to reuse the LockOSThread scheduler mechanism as much as possible.
  • There's a separate queue of runnable main thread goroutines.
  • The scheduler invariant is: if there's no running main thread goroutine, but there are runnable main thread goroutines, then the head of the main thread queue is in some regular (P or global) run queue.
  • We enforce this invariant by:
    • Unparking a main thread goroutine adds it to the main thread queue. If it's the head of the queue and there's no running main thread goroutine, add it to the P run queue like a normal unpark.
    • If the scheduler picks a main thread goroutine from the run queue, switch to the main thread and run there. This is probably exactly the LockOSThread mechanism.
    • When a main thread goroutine gets descheduled (including if its P gets retaken), and the main thread runnable queue is not empty, add the head of the queue to the regular run queue.

@hajimehoshi hajimehoshi reopened this May 30, 2024
@github-project-automation github-project-automation bot moved this from Done to In Progress in Go Compiler / Runtime May 30, 2024
@qiulaidongfeng
Copy link
Member

qiulaidongfeng commented Sep 24, 2024

how should LockOSThread and UnlockOSThread behave during mainthread.Do? They may panic or be effectively ignored, but either choice should be documented.

Another option, as CL does now, is to call LockOSThread and UnlockOSThread at mainthread.Do, which are both valid calls, it will act on the goroutine of f running on the main thread.
Specifically speaking:
If Do is called on the main thread, LockOSThread and UnlockOSThread affect the goroutine of Do.
If Do is called on a non-main thread, the LockOSThread and UnlockOSThread affect the goroutine of Yield.

@qiulaidongfeng
Copy link
Member

There's another problem, as @eliasnaur said What happens if the Go runtime is initialized by a non-main thread (e.g. c-shared build mode)?
I have studied it and I don't know how to judge whether it is on the mainthread. If others have no way to judge whether it is on the mainthread, the mainthread package can only panic all the time in this case.

@ianlancetaylor
Copy link
Contributor

I don't see a strong reason for LockOSThread to behave any differently if called during mainthread.Do. I agree that calling LockOSThread without calling UnlockOSThread can lead to deadlock for other calls to mainthread.Do. But I think such a bug is not particularly likely and easy to understand if it happens. I don't think that we want mainthread.Do to interact badly if it calls code in some other package that for some reasons wants to use LockOSThread.

It's a good point that mainthread.Do can't work in code complied in c-archive or c-shared mode. I guess that in that case a panic is the only option. At least it seems like the option we should start with; maybe we can think of something else later.

@qiulaidongfeng
Copy link
Member

qiulaidongfeng commented Sep 24, 2024

I don't see a strong reason for LockOSThread to behave any differently if called during mainthread.Do.

Can it be understood that calling LockOSThread in mainthread.Do always affects the goroutine of the Do?
By the way, this is happening because if Do is not on the main thread, Do and Yield are running on two different goroutine, we can forward LockOSThread to Do's goroutine, and panic and Goexit is the same?

I agree that calling LockOSThread without calling UnlockOSThread can lead to deadlock for other calls to mainthread.Do. But I think such a bug is not particularly likely and easy to understand if it happens. I don't think that we want mainthread.Do to interact badly if it calls code in some other package that for some reasons wants to use LockOSThread.

We can implement with stronger restrictions first and relax them later, just like comparable, which is more restrictive in go1.18 and looser in go1.20.

@hajimehoshi
Copy link
Member

Can't we use OS APIs to get main threads? (e.g. Looper.getMainLooper() on Android, dispatch_get_main_queue() on iOS)? If mainthread is not available with c-archive, this is not available on mobiles (with gomobile-bind)...

@qiulaidongfeng
Copy link
Member

qiulaidongfeng commented Sep 24, 2024

Perhaps there is a more general approach, which is to have the user call an API function during c-archive and c-shared to tell go runtime the id of the main thread.
However, this requires a new API, such as

func SetMainThreadId(id uint)

But,this is a new proposal.
And this is likely to silently succeed when the user gives the wrong thread ID, rather than panic.

@ianlancetaylor
Copy link
Contributor

Can it be understood that calling LockOSThread in mainthread.Do always affects the goroutine of the Do?

Yes. What else could it affect?

By the way, this is happening because if Do is not on the main thread, Do and Yield are running on two different goroutine, we can forward LockOSThread to Do's goroutine, and panic and Goexit is the same?

Are you talking about the case where code calls LockOSThread and then calls mainthread.Do? I don't think we should forward the LockOSThread in that case.

I don't think we should worry too much about c-archive and c-shared right now. If we panic today, then if somebody comes up with a real use case we can reconsider.

@qiulaidongfeng
Copy link
Member

What I mean is that if Do is called from a non main thread, it runs on a different goroutine than the Yield called from the main thread. Because if Do is not on the main thread, in order to respect the fact that the main thread may have already called LockOSThread like this code template, we cannot schedule other goroutines to the main thread. Instead, we can use a channal to send f from Do to Yield, allowing Yield to call f on the main thread. After f's call ends, we can send a signal to Do from another channal.
code template:

package main
import (
    "runtime"
    "runtime/mainthread"
)

func init() {
    runtime.LockOSThread()
}

func main(){
    // other code
    for {
        select {
            case <-mainthread.Waiting():
                mainthread.Yield()
            // other case
        }
    }
}

@ianlancetaylor
Copy link
Contributor

I'm not sure I fully understand this. Can you show a complete example? What do you think should happen?

@qiulaidongfeng
Copy link
Member

qiulaidongfeng commented Sep 25, 2024

For example:

package main
import (
    "runtime"
    "runtime/mainthread"
)

func init() {
    runtime.LockOSThread()
}

func main(){
    go func(){
        mainthread.Do(func(){
            runtime.LockOSThread()
        })
    }()
    for {
        select {
            case <-mainthread.Waiting():
                mainthread.Yield()
        }
    }
}

Running this example with the current CL will definitely cause a panic.
Because in the main thread, in order to respect runtime.LockOSThread,we cannot schedule Do goroutine to main thread ,so mainthread.Do on non main thread use channal send f to mainthread.Yield , and f is called in the mainthread.Yield on the main thread. As a result, the Yield goroutine calls runtime.LockOSThread twice but does not call runtime.UnlockOSThread.

More information:
There is an example in the test added to proc_test.go in CL that illustrates the mainthread.Do, What if I don't forward something like runtime.Goexit.
If mainthread.Yield does not forward runtime.Goexit to mainthread.Do, in main thread t.Fatal called in mainthread.Do will call runtime.Goexit. As a result, the goroutine of TestMain is exit, and the test will never exit normally. So, from this example, forwarding runtime.Goexit and panic can make mainthread.Do easier to use without worrying about the main thread goroutine (usually the main function goroutine) exit due to executing f.
But, for runtime.LockOSThread, I cannot come up with an example to prove that it should be forward.

@qiulaidongfeng
Copy link
Member

So, I think we should forward runtime.Goexit and panic to mainthread.Do, and let it act on Do goroutine. For runtime.LockOSThread, I'm not sure what to do.

@ianlancetaylor
Copy link
Contributor

I don't see a problem if the example in #64777 (comment) panics.

@hajimehoshi
Copy link
Member

Does this mean, if one of dependencies invokes LockOSThread in its init, all the other libraries would not be able to use mainthread?

@qiulaidongfeng
Copy link
Member

qiulaidongfeng commented Sep 26, 2024 via email

@hajimehoshi
Copy link
Member

Ok thanks

@eliasnaur
Copy link
Contributor Author

I don't think we should worry too much about c-archive and c-shared right now. If we panic today, then if somebody comes up with a real use case we can reconsider.

I don't know if it qualifies as a real use case, but note that Android forces Go code into c-shared mode and calls into the Java UI libraries must happen on the main thread.

@ianlancetaylor
Copy link
Contributor

How does that work today on Android? That is, how does Go code know what is the main thread? Is there some guarantee that the library is loaded on the main thread, so that LockOSThread will work?

@eliasnaur
Copy link
Contributor Author

There's no guarantee, AFAIK Java's System.loadLibrary loads and initializes the native library on the thread that called it. The way to use the main thread on Android is to use the Java equivalent of mainthread.Do: Looper.getMainLooper and Handler.post.

#64777 (comment) suggests using the platform APIs to detect whether the calling thread is the main thread, but that's annoying because there's no Looper.getMainLooper in the C NDK. Another option is to say that in c-shared or c-archive mode, the main thread is defined to be the thread bound to the first goroutine calling mainthread.Yield. If the thread is created by Go, Yield panics. Not great.

Thinking about the above, I now have doubts whether mainthread is too much for its use-cases. I can come up with:

(1) Packages that wants to abstract the starting of the platform main event loop (calling UIApplicationMain on iOS, NSApp.run on macOS, etc.) away from the user. They need a single call to mainthread.Do that never returns.

(2) Code that wants to call native main-thread API on platforms with a platform main loop. They can use the platform API today (Looper.getMainLooper on Android, performSelectorOnMainThread on iOS, etc.). This is not an additional burden compared to mainthread.Do, because they were going to call native API anyway.

(3) Code that wants to call native main-thread API where there is no platform main loop. They can use mainthread.Do, because no main thread API will block.

I'm assuming that:

  • For every platform, there is at most one blocking main-thread API (the "main event loop").
  • If there is a main event loop, there is native API to wake it up and run callbacks on the main thread.

Use-cases (2) and (3) can co-exist with each other and (1). Multiple (1) packages will see the earliest caller of mainthread.Do "win" the main thread, and the rest block forever. With Yield, every use-case (1) loser would end up being scheduled, calling some native isMainEventLoopRunning (e.g. NSApplication.isRunning on macOS) and exit early.

What did I miss? Why are we adding the Waiting/Yield machinery that's already available as native API?

@eliasnaur
Copy link
Contributor Author

eliasnaur commented Oct 20, 2024

Gentle ping. With the lesser usefulness of mainthread.Do in c-archive mode, and in light of my 3 use-case classes above, I propose limiting the proposal to just the essential Do:

// Package mainthread mediates access to the program's main thread.
//
// Most Go programs do not need to run on specific threads 
// and can ignore this package, but some C libraries, often GUI-related libraries,
// only work when invoked from the program's main thread.
//
// [Do] runs a function on the main thread. No other code can run on the main thread
// until that function returns.
//
// Each package's initialization functions always run on the main thread,
// as if by successive calls to Do(init).
//
// For compatibility with earlier versions of Go, if an init function calls [runtime.LockOSThread], 
// then package main's func main also runs on the main thread, as if by Do(main).
package mainthread // imported as "runtime/mainthread"

// Do calls f on the main thread.
// Nothing else runs on the main thread until f returns.
// If f calls Do, the nested call panics.
//
// Package initialization functions run as if by Do(init).
// If an init function calls [runtime.LockOSThread], then package main's func main
// runs as if by Do(main), until the thread is unlocked using [runtime.UnlockOSThread].
//
// Do panics if the Go runtime is not in control of the main thread, such as in build modes
// c-shared and c-archive.
func Do(f func())

@qiulaidongfeng
Copy link
Member

qiulaidongfeng commented Oct 20, 2024

Does this API mean that if need to use the main thread, either runtime.LockOSThred in init or mainthread.Do, one or the other , don`t incoexistence.

@eliasnaur
Copy link
Contributor Author

You can arrange for UnlockOSThread to be called from main, but that's may be surprising to the user that expects the main thread locked at all times.

A better way is to keep LockOSThread-during-init for Go < 1.24, and use mainthread.Do on Go >= 1.24.

@qiulaidongfeng
Copy link
Member

You can arrange for UnlockOSThread to be called from main, but that's may be surprising to the user that expects the main thread locked at all times.

A better way is to keep LockOSThread-during-init for Go < 1.24, and use mainthread.Do on Go >= 1.24.

Sounds like the original proposal was better,there is no need to use different methods depending on the go version.

See https://go.dev/doc/go1compat

  • The APIs may grow, acquiring new packages and features, but not in a way that breaks existing Go 1 code.
  • Go 1 defines two things: first, the specification of the language; and second, the specification of a set of core APIs, the "standard packages" of the Go library.
  • Compatibility is at the source level.

If in order to use the new package,user need to not only add new code, but also modify the old valid code, even if just compile different codes in different go versions through build tags, I don't think this is consistent with go1compat. According to my understand, add new functions may require new code. For example, the standard library has a new API after added range-over-func, but it cannot affect the old code. For example, sync.Map.Range was directly called before. This kind of code does not need to adapt to the new go version , it will run as is.

@eliasnaur
Copy link
Contributor Author

@rsc @ianlancetaylor in the interest of moving this forward, I've separated the simpler variant of this proposal into #70089. I hope that's appropriate; I don't know the procedure for changing an accepted proposal because of new information.

@qiulaidongfeng I don't know why you bring up the Go compatibility guarantee. Both the original proposal and the reduced version (#70089) are backwards compatible.

@qiulaidongfeng
Copy link
Member

qiulaidongfeng commented Oct 29, 2024

In my opinion, when adding new features, backward compatibility should mean that existing valid code does not need any modification (even if it is just adding build tags when using new features or new versions), and using new features may require adding code that did not exist before.

For example:
We can iterate with sync.Map.Range(f). After adding range-over-func, we can iterate for range sync.Map.Range, but we don't need to modify the original valid code sync.Map.Range(f).

Here the original proposal means that it is possible not to modify the existing valid code. To use the new features, only add code that didn't exist before.
Like #70089, I can't figure out how to add non-existent code to use the mainthread package without modifying the existing valid code for the runtime.LockOSThread+event loop.

Both the original proposal and #70089 can leave the existing valid code intact without using the mainthread package, but that's not the problem, the problem is whether the existing valid code can be left intact when using the mainthread package? Whether at most you only need to add code that doesn't exist.

@cherrymui
Copy link
Member

I haven't read the recent discussions in detail, but IMHO it is not compatibility, but composability. Compatibility ensures that existing code that works with Go 1.23 continues to work, and this proposal or the variants above don't affect it. Here the problem is that the existing code (that uses LockOSthread in init) doesn't compose will with new code (that uses mainthread.Do).

As the main thread is a single piece of resource, and both LockOSthread and mainthread.Do provide exclusive control over it, it is probably never going to compose perfectly well, without some form of coordination. The accepted proposal has Yield and Waiting partly for this kind of coordination. But it sounds like from @eliasnaur 's comment this type of coordination wouldn't work well in practice? Because main thread-using code would usually want exclusive control of the main thread (therefore they would never call Yield)?

Either way, for things to compose, I think one needs to have some control of the main package, either to UnlockOSThread or call Yield if it is locked to the main thread during init.

@eliasnaur
Copy link
Contributor Author

eliasnaur commented Oct 31, 2024

Thanks @cherrymui. My issue with this proposal is that package mainthread doesn't work in c-shared/c-archive mode, in particular on the Android platform. Yet, the platform specific facilities for running code on the main thread (e.g. Looper.getMainLooper on Android) always works, regardless of build mode. In addition, if you're calling mainthread.Do, you're almost certainly calling platform API anyway, so calling the platform Do doesn't seem a large burden. In conclusion, the platform specific facilities seem strictly superior to Yield/Waiting.

That leaves #70089 for the reduced use case: allowing Go package A to gain (permanent or temporary) control of the main thread from the Go runtime. In my analysis #64777 (comment) I claim that every platform either offers an event loop API or non-blocking API, but never a mix. Therefore, there's no need to mediate main thread access through Yield and Waiting.

@eliasnaur
Copy link
Contributor Author

Gentle ping. This missed Go 1.24, but I'd love to see a decision in ample time for a Go 1.25 implementation.

@gophun
Copy link

gophun commented Nov 17, 2024

It seems that this proposal was accepted before being properly thought through. Once a proposal is accepted, the API should be clearly defined, and what was agreed upon should simply be implemented.

@eliasnaur
Copy link
Contributor Author

I still favor #70089, but I can support this proposal if the main thread is defined to be the thread that runs init functions. For regular programs that's the main thread, for c-shared programs that's the thread that loads the shared library. Users of the c-shared library would be responsible for loading the c-shared library from the main thread.

This behaviour is contrary to my "not great" comment. I changed my mind because I realized that's what LockOSThread does: "All init functions are run on the startup thread. Calling LockOSThread from an init function will cause the main function to be invoked on that thread."

@ianlancetaylor what do you think? You suggested the panic behaviour in your comment, #64777 (comment).

If so, we should also reconsider whether "main thread" and "mainthread" are the right term and package name. LockOSThread uses "startup thread" which is more precise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. Proposal Proposal-Accepted
Projects
Status: In Progress
Status: Accepted
Development

Successfully merging a pull request may close this issue.

14 participants