-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
Comments
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. |
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:
The disadvantages are:
// 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() |
Gentle ping. I believe #64777 (comment) addresses the objections @aclements brought up here and on #64755. |
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 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 |
The number of direct uses of 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
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: |
|
I believe a fix to #67499 will enable |
Thanks, but wouldn't I still need to call |
Yes, something would have to drive the For Gio, I plan to keep |
Isn't this proposal |
Yes, but it seems unlikely to be accepted now that It's even possible to eliminate the restriction and "steal" the main thread during, say, a package In light of the above, and assuming rangefuncs and |
One possible thing |
Can you elaborate? I don't think |
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 @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()) |
@mknyszek and I were discussing how to implement this and I wanted to capture our thoughts:
|
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. |
There's another problem, as @eliasnaur said |
I don't see a strong reason for It's a good point that |
Can it be understood that calling LockOSThread in mainthread.Do always affects the goroutine of the Do?
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. |
Can't we use OS APIs to get main threads? (e.g. |
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. func SetMainThreadId(id uint) But,this is a new proposal. |
Yes. What else could it affect?
Are you talking about the case where code calls 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. |
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. package main
import (
"runtime"
"runtime/mainthread"
)
func init() {
runtime.LockOSThread()
}
func main(){
// other code
for {
select {
case <-mainthread.Waiting():
mainthread.Yield()
// other case
}
}
} |
I'm not sure I fully understand this. Can you show a complete example? What do you think should happen? |
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. More information: |
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. |
I don't see a problem if the example in #64777 (comment) panics. |
Does this mean, if one of dependencies invokes |
No, unless mainthead.Yield is not called on the main thread.
…---Original---
From: "Hajime ***@***.***>
Date: Thu, Sep 26, 2024 11:00 AM
To: ***@***.***>;
Cc: ***@***.******@***.***>;
Subject: Re: [golang/go] runtime/mainthread: new package to manage main thread(Issue #64777)
Does this mean, if one of dependencies invokes LockOSThread in its init, all the other libraries would not be able to use mainthread?
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: ***@***.***>
|
Ok thanks |
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. |
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 |
There's no guarantee, AFAIK Java's #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 Thinking about the above, I now have doubts whether (1) Packages that wants to abstract the starting of the platform main event loop (calling (2) Code that wants to call native main-thread API on platforms with a platform main loop. They can use the platform API today ( (3) Code that wants to call native main-thread API where there is no platform main loop. They can use I'm assuming that:
Use-cases (2) and (3) can co-exist with each other and (1). Multiple (1) packages will see the earliest caller of What did I miss? Why are we adding the |
Gentle ping. With the lesser usefulness of
|
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. |
You can arrange for A better way is to keep LockOSThread-during-init for Go < 1.24, and use |
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
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. |
@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. |
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: 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. 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. |
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 As the main thread is a single piece of resource, and both Either way, for things to compose, I think one needs to have some control of the main package, either to |
Thanks @cherrymui. My issue with this proposal is that package 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 |
Gentle ping. This missed Go 1.24, but I'd love to see a decision in ample time for a Go 1.25 implementation. |
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. |
I still favor #70089, but I can support this proposal if the main thread is defined to be the thread that runs 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. |
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,This is the complete proposal.
Variants
Just like #64755, an alternative spelling is
syscall.RunOnMainThread
optionally limited toGOOS=darwin
andGOOS=ios
.The text was updated successfully, but these errors were encountered: