-
Notifications
You must be signed in to change notification settings - Fork 17.6k
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:
|
I don't know how the API would be to post a task to the main thread in the case of PostTaskToMainThreadSynchronously(func() {
PostTaskToMainThreadSynchronously(func() {})
}) On the other thand, this should never be blocked: RunOnMainThread(func() {
RunOnMainThread(func() {})
}) I thought the internal func could be run immediately on the same thread. Or, as @aclements suggested, RunOnMainThread might invoke a different goroutine running on the main thread. |
Thank you for thinking about this issue and for proposing something you may be able to stomach! If I understand your proposal correctly, main thread packages will replace However, given
What happens when invoking the various obnoxious main-thread APIs that assume complete control over the main thread? They include iOS' In other words, it seems to me that to be generally useful, In contrast, #67499 and #67694 are more limited, but allow sharing of the main thread among different packages albeit with careful cooperation from the main function/goroutine. [0] It's possible to avoid |
I'm not sure I understand what "relinquish scheduling control" actually looks like. With a function like
In my reply to #67694 I basically said the same thing, but I don't see how #67494 is much better than This just makes me think that there should maybe also be a way for a package to signal the intent to take full control of the main thread, so that other packages report a nice error message instead of hanging when they try to do the same thing. Or something.
I'm curious about the second sentence here: what is the issue with blocking? Does it require blocking on the main thread specifically? |
One way would be for the Go runtime to schedule RunOnMainThread goroutines using the native APIs. See below.
Maybe my imagination fails me, but I don't know of a main-thread API that doesn't assume the OS has control of the main thread. As far as I know, the programming model is (1) pass main thread control to the OS (2) call APIs from callbacks, scheduled manually with run-on-main-thread APIs or as a result of events such as "mouse clicked". In other words, no main thread API block, except for the one call to UIApplicationMain/NSApplicationMain/... . @hajimehoshi maybe you know of some?
It's true that more than 1 blocking call on the same thread is fundamentally non-composable. But as I stated above, there's only ever one blocking call (which may be interrupted), and every other API call won't block (as a matter of design, blocking the main/UI thread is considered very bad form because it results in GUI stutter). So for the sake of Go packages A and B to both use the main thread, your proposed RunOnMainThread offers too little and too much:
I can't think of salvaging RunOnMainThread without it becoming aware of UIApplicationMain somehow. That is:
Very messy.
Putting the powerful #67694 aside, and only assuming iter.Pull works with Cgo callbacks (#67499), two otherwise unrelated packages A and B may cooperate: package pkga
import "C"
func init() {
runtime.LockOSThread() // Lock main thread to the main goroutine
}
var mainYield func(struct{}) bool
//export mainLoopInitialized
func mainLoopInitialized() {
mainYield(struct{}{})
}
func runMainThreadEventLoopIfNecessary() {
if !C.isOnMainThread() { // https://developer.apple.com/documentation/foundation/nsthread/1412704-ismainthread
panic("must be called from the main goroutine")
}
if C.isMainLoopRunning() { // https://developer.apple.com/documentation/foundation/nsrunloop/1412652-currentmode
return
}
next, _ := iter.Pull(func(yield func(struct{}) bool) {
mainYield = yield
C.runMainLoop() // will call back mainLoopInitialized
})
mainNext = next
next()
// Here, the main loop is started, but suspended.
}
// MainThreadAPIA1 calls the native A1 API.
func MainThreadAPIA1(...) {
if C.isOnMainThread() {
runMainThreadLoopIfNecessary()
C.mainThreadAPI1(...) // doesn't block
} else {
done := make(chan struct{})
h := cgo.NewHandle(func() {
mainThreadAPI1(...)
close(done)
})
defer h.Delete()
C.runOnMainThread(h) // https://developer.apple.com/documentation/objectivec/nsobject/1414900-performselectoronmainthread
}
}
func WaitForEvents() {
if !C.isOnMainThread() {
panic("must be called from the main goroutine")
}
mainNext()
} Package B would contain a duplicate of most of the code (or share a common package): package pkgb
import "C"
func init() {
runtime.LockOSThread() // Lock main thread to the main goroutine
}
var mainYield func(struct{}) bool
//export mainLoopInitialized
func mainLoopInitialized() {
mainYield(struct{}{})
}
func runMainThreadEventLoopIfNecessary() {
if !C.isOnMainThread() { // https://developer.apple.com/documentation/foundation/nsthread/1412704-ismainthread
panic("must be called from the main goroutine")
}
if C.isMainLoopRunning() { // https://developer.apple.com/documentation/foundation/nsrunloop/1412652-currentmode
return
}
next, _ := iter.Pull(func(yield func(struct{}) bool) {
mainYield = yield
C.runMainLoop() // will call back mainLoopInitialized
})
mainNext = next
next()
// Here, the main loop is started, but suspended.
}
// MainThreadAPIB1 calls the native B1 API.
func MainThreadAPIB1(...) {
if C.isOnMainThread() {
runMainThreadLoopIfNecessary()
C.mainThreadAPI1(...) // doesn't block
} else {
done := make(chan struct{})
h := cgo.NewHandle(func() {
mainThreadAPI1(...)
close(done)
})
defer h.Delete()
C.runOnMainThread(h) // https://developer.apple.com/documentation/objectivec/nsobject/1414900-performselectoronmainthread
}
}
func WaitForEvents() {
if !C.isOnMainThread() {
panic("must be called from the main goroutine")
}
mainNext()
} Finally, the caller: package main
import "pkga"
import "pkgb"
func main() {
pkga.MainThreadAPIA1(...)
pkgb.MainThreadAPIB1(...)
// or even
go func() {
pkga.MainThreadAPIA2(...)
}()
for {
pkga.WaitForEvents()
// or
pkgb.WaitForEvents()
}
} Quite a bit of machinery needed, but notably:
Now, with something like #67694, you can get rid of 'WaitForEvents' and the isMainThread special cases, at the cost of the first of pkga and pkgb "stealing" the main thread through an package pkga
func init() {
if !C.isOnMainThread() {
// Someone else already stole the main thread.
return
}
next, _ := iter.Pull(func(yield func(struct{}) bool) {
go yield(struct{}{}) // Pass back a different thread.
C.runMainLoop()
})
next()
// Main thread "stolen" and passed to UIApplicationMain/...
}
// MainThreadAPIB1 calls the native B1 API.
func MainThreadAPIB1(...) {
done := make(chan struct{})
h := cgo.NewHandle(func() {
mainThreadAPI1(...)
close(done)
})
defer h.Delete()
C.runOnMainThread(h)
} Note that for Gio and probably most/all GUI packages, fixing #67499 is enough:
Correct. You may drive the event loop yourself on macOS, but it has to happen on the main thread. |
I implemented the mainthread package according to this idea.
Specifically in the mainthread package, the main thread and other thread is used as a coroutine through three functions within the package. If wants to run f in the mainthread, run
In the mainthread, calling mainthread.Yield will perform a coroutine switch, changing the function that the mainthread runs to f, thus showing the semantics that mainthread.Yield cedes the mainthread to mainthread.Do. Waiting returns a channel, and a struct{} is sent to the channel when Based on the above, if the mainthread package is implemented using coroutine semantics as shown in CL 609977, the question of whether nested calls to mainthread.Do nested call, it is fine by default, no need to do anything to support it, it's the other way around that needs dedicated support, because if call func(){
f1()
f2()
}() is completely different from mutex reentrant. I don't understand how this is like a mutex reentrant?What I can think of that makes mainthread.Do look like a lock is this document But in my opinion, even if it means locking the mainthread to ensure that f always runs on the mainthread, it does not mean that nested calls to mainthread.Do are like mutex reentrant, because if it is mutex, you can protect invariants by locking and unlocking, but mainthread.Do cannot do the mutex effect. Only need to call mainthread.Yield in mainthread.Do to give up the main thread to other mainthread.Do (if there are other mainthread.Dos), wait for other mainthread.Do to return, and it can take back the main thread. This is the behavior of mutex is completely different, it is exactly the behavior of coroutine (when one coroutine is running, the one that resumed it or yielded to it is not).
The documentation for my proposed new package looks like this // 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. If have other [Do] call, it can
// yield the main thread to other [Do] calls by calling [Yield].
//
// 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).
// In this situation, main must explicitly yield the main thread
// to allow other thread calls to Do are to proceed.
// See the documentation for [Waiting] for examples.
package mainthread // imported as "runtime/mainthread"
// Do calls f on the main thread.
// Nothing else runs on the main thread until f returns or calls [Yield].
//
// 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].
func Do(f func())
// Yield give the main thread to [Do]
// call that is blocked waiting for the main thread when Yield is called.
// It then waits to reacquire the main thread and returns.
// It only give main thread to one [Do] at a call it.
//
// Yield must only be called from the main thread.
// If called from a different thread, Yield panics.
func Yield()
// Waiting returns a channel that receives a message when a call to [Do]
// is blocked waiting for the main thread. A message is sent
// when a call to [Do] is blocked while waiting for the main thread.
// There is only one waiting channel; all calls to Waiting return the same channel.
//
// Programs that run a C-based API such as a GUI event loop on the main thread
// should arrange to share it by watching for events on Waiting and calling [Yield].
// A typical approach is to define a new event type that can be sent to the event loop,
// respond to that event in the loop by calling Yield,
// and then start a separate goroutine (running on a non-main thread)
// that watches the waiting channel and signals the event loop:
//
// go func() {
// for range mainthread.Waiting() {
// sendYieldEvent()
// }
// }()
//
// C.EventLoop()
//
func Waiting() <-chan struct{} I think using mainthread.Yield is code like this package main
import (
"runtime"
"runtime/mainthread
)
func init(){
runtime.LockOSThread()
}
func main(){
// other code
C.EventLoop()
for {
select {
// other case handle c event
case <-mainthread.Waiting():
mainthread.Yield()
}
}
} So, I think it is the best way to a Yield give main thread to a Do, because in this way, it can achieve what the document says (mediates access to the program's main thread) with a simple implementation. And a more complex implementation is not necessary, because if a Yield gives the main thread to multiple Do, it is more complicated to implement in order to properly synchronize concurrent programs, which has a cost (both when writing CL and when the program is running), but the same thing can be Waiting and Yield multiple times. Unless are sure that there is only one Do, the usual practice is to have select in the for loop so that you can wait and Yield multiple times, so I don't think the more complex implementation is worth it and not necessary. |
+1 to enable nested |
What about a case like mainthread.Do(func( ){
myMutex.Lock()
defer myMutex.Unlock()
// call C function on main thread
g()
// call C function on main thread
})
...
func g() {
mainthread.Do(func() {
// call C function on main thread
mainthread.Yield()
})
} In this case let's assume that the first function doesn't realize that That is, permitting nested |
If there are anything that require mutex protection, then mutex protection should always be used.
If something is sometimes protected with a mutex lock and sometimes not, I don't understand why this mutex lock is used?
For example, if want to perform concurrent operations on a global variable, then every operation on this global variable should be protected by a mutex lock.
And whether Yield yields main thread to one Do or multiple Do, the documentation also makes it clear that Yield yields the main thread, although Yield takes the main thread back. Along with the Do documentation, I think the documentation can deduce that if there is Yield in a Do then the main thread is not exclusive to a Do.
So Do is different from mutex locks. Do can call Yield to give up the main thread to other Do and retrieve the main thread after the other Do returns. However, mutex locks cannot give up the lock to other lock holders and ensure that they retrieve the lock upon their return.
Regarding the code presented, if there is a global state change before and after calling g, and a mutex lock is used in the outer layer of Do, the program is surprised by this situation. This only indicates that the program relies on the erroneous assumption that calling Do can ensure exclusive access to the main thread, without considering the situation where Yield is called inside. I think this is an incorrect synchronization.
If there is incorrect synchronization, it should be user code modification, not changes to the Go language standard library.
…---Original---
From: "Ian Lance ***@***.***>
Date: Wed, Sep 4, 2024 00:02 AM
To: ***@***.***>;
Cc: ***@***.******@***.***>;
Subject: Re: [golang/go] runtime/mainthread: new package to manage main thread(Issue #64777)
What about a case like
mainthread.Do(func( ){ myMutex.Lock() defer myMutex.Unlock() // call C function on main thread g() // call C function on main thread }) ... func g() { mainthread.Do(func() { // call C function on main thread mainthread.Yield() }) }
In this case let's assume that the first function doesn't realize that g is going to call mainthread.Do and mainthread.Yield. Since g is yielding the main thread, other functions can run. But since myMutex is locked, those other functions may see an inconsistent state.
That is, permitting nested mainthread.Do means that a function that thinks it is in sole control of the main thread may not in fact be in control. This is the sense in which a nested mainthread.Do is similar to a nested mutex lock.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: ***@***.***>
|
To me it seems perfectly reasonable that if my code calls If we permit recursive calls to If we do not permit recursive calls, then I can ensure exclusive access. The only way I could lose exclusive access would be if some other function that I don't know about calls So what I am suggesting is that preventing recursive calls to I think that if we are going to permit recursive calls to I hope that makes sense. |
I admit that if Do means exclusive threads, then this API is easier to use correctly. Thank you very much for pointing this out. I will update CL to prohibit nested calls to Do and update the documentation to explain why the invocation of LockOSThread by init during runtime does not crash when calling Do. These conversations are very valuable, thank you.
…---Original---
From: "Ian Lance ***@***.***>
Date: Wed, Sep 4, 2024 01:24 AM
To: ***@***.***>;
Cc: ***@***.******@***.***>;
Subject: Re: [golang/go] runtime/mainthread: new package to manage main thread(Issue #64777)
To me it seems perfectly reasonable that if my code calls Do and does not call Yield, then my code has exclusive access to the main thread.
If we permit recursive calls to mainthread.Do, then I can't ensure exclusive access.
If we do not permit recursive calls, then I can ensure exclusive access. The only way I could lose exclusive access would be if some other function that I don't know about calls mainthread.Yield, but it would be clearly incorrect for a package to call mainthread.Yield without calling mainthread.Do (or clearly documenting the call to mainthread.Yield).
So what I am suggesting is that preventing recursive calls to mainthread.Do will catch errors, just as preventing recursive calls to sync.Mutex.Lock catches errors.
I think that if we are going to permit recursive calls to mainthread.Do, we need a better reason than simplifying the implementation. We should not simplify the implementation at the cost of making the API harder to use correctly.
I hope that makes sense.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: ***@***.***>
|
DO NOT SUBMIT -- f call Do now not panic New Function Do,Yield,Waiting coordinate the main thread using two channal. Fixes golang#64777 Change-Id: Ib15a03c0efc6db83a7819acc865a896752797341
As asked in review of https://go-review.googlesource.com/c/go/+/609977/comments/667a4ab7_78cfd118, how should LockOSThread and UnlockOSThread behave during |
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. |
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: