proposal: runtime: add RunOnMainThread to take control of the main thread
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.
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:
- 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,RunOnOSThreadperforms 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
RunOnOSThreadduring 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()
Gentle ping. I believe https://github.com/golang/go/issues/64777#issuecomment-1861759343 addresses the objections @aclements brought up here and on https://github.com/golang/go/issues/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 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.
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
RunOnOSThreadfunction 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 agostatement that starts by callingruntime.LockOSThread. SoRunOnOSThreadseems 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
mainfunction. 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.
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.
I believe a fix to https://github.com/golang/go/issues/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?
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?
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.
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?
Yes, but it seems unlikely to be accepted now that RunOnMainThread can (almost) be achieved with iterators, see the example at https://github.com/golang/go/issues/67499#issuecomment-2137400571 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 https://github.com/golang/go/issues/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 https://github.com/golang/go/issues/67694.
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.
Can you elaborate? I don't think iter.Pull itself can ever deadlock, regardless of https://github.com/golang/go/issues/67694.
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())
@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.
@eliasnaur
Can you elaborate? I don't think iter.Pull itself can ever deadlock, regardless of https://github.com/golang/go/issues/67694.
I don't know how the API would be to post a task to the main thread in the case of iter.Pull, but wouldn't this be deadlocked?
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 LockOSThread during initialization with calls to RunOnMainThread.
However, given
// However, if a goroutine blocks in the OS, it may not be // possible to schedule other main thread goroutines.
What happens when invoking the various obnoxious main-thread APIs that assume complete control over the main thread? They include iOS' UIApplicationMain, macOS' NSApplicationMain[0], Windows' StartServiceCtrlDispatcher. They all require the main thread and never return. API can invoke a callback on the captured main thread, but RunOnMainThread and the Go scheduler doesn't know about them.
In other words, it seems to me that to be generally useful, RunOnMainThread and the Go scheduler should somehow detect the use of the above APIs and seamlessly relinquish scheduling control to the platform.
In contrast, https://github.com/golang/go/issues/67499 and https://github.com/golang/go/issues/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 NSApplicationMain and drive the main thread event loop yourself. However, some gestures such as window resizing will still block until completed.
In other words, it seems to me that to be generally useful, RunOnMainThread and the Go scheduler should somehow detect the use of the above APIs and seamlessly relinquish scheduling control to the platform.
I'm not sure I understand what "relinquish scheduling control" actually looks like. With a function like UIApplicationMain you're basically wedging the main thread from the perspective of the Go runtime. There's only one "main thread" per process, so asking to call multiple things on the main thread is just fundamentally non-composable. But perhaps you were just speaking rhetorically, and I took it too literally. :)
In contrast, https://github.com/golang/go/issues/67499 and https://github.com/golang/go/issues/67694 are more limited, but allow sharing of the main thread among different packages albeit with careful cooperation from the main function/goroutine.
In my reply to #67694 I basically said the same thing, but I don't see how #67494 is much better than RunOnMainThread in the specific case you describe. Something must fall over because there's only one main thread.
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.
[0] It's possible to avoid NSApplicationMain and drive the main thread event loop yourself. However, some gestures such as window resizing will still block until completed.
I'm curious about the second sentence here: what is the issue with blocking? Does it require blocking on the main thread specifically?
In other words, it seems to me that to be generally useful, RunOnMainThread and the Go scheduler should somehow detect the use of the above APIs and seamlessly relinquish scheduling control to the platform.
I'm not sure I understand what "relinquish scheduling control" actually looks like.
One way would be for the Go runtime to schedule RunOnMainThread goroutines using the native APIs. See below.
With a function like
UIApplicationMainyou're basically wedging the main thread from the perspective of the Go runtime.
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?
There's only one "main thread" per process, so asking to call multiple things on the main thread is just fundamentally non-composable. But perhaps you were just speaking rhetorically, and I took it too literally. :)
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:
- too little, because it gives up once some goroutine calls UIApplicationMain
- too much, because if you're using RunOnMainThread, you're almost certainly going to call some native main-thread API. Why not just call the native RunOnMainThread function?
I can't think of salvaging RunOnMainThread without it becoming aware of UIApplicationMain somehow. That is:
- The first call to RunOnMainThread will call, say, UIApplicationMain, ask for a callback on the main thread and resume execution from that. UIApplicationMain is essentially suspended while any main thread goroutine is running. UIApplicationMain is resumed when all main thread goroutines are blocking or returns.
- Every subsequent call to RunOnMainThread will ask for a callback from UIApplicationMain.
Very messy.
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.
In my reply to #67694 I basically said the same thing, but I don't see how #67494 is much better than
RunOnMainThreadin the specific case you describe. Something must fall over because there's only one main thread.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.
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:
- No shared state between pkga and pkgb
- No callbacks in the exported APIs of pkga and pkgb
- No special cooperation from the Go runtime
Now, with something like https://github.com/golang/go/issues/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 iter.Pull during initialization:
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 https://github.com/golang/go/issues/67499 is enough: WaitForEvents fits naturally inside the blocking gui.Window.Events iterator. It may even be enough for Windows services, because a service also waits for events from the OS.
[0] It's possible to avoid NSApplicationMain and drive the main thread event loop yourself. However, some gestures such as window resizing will still block until completed.
I'm curious about the second sentence here: what is the issue with blocking? Does it require blocking on the main thread specifically?
Correct. You may drive the event loop yourself on macOS, but it has to happen on the main thread.
OK, I think I'm beginning to understand what problem you're looking to solve exactly. Thanks for bearing with me, and for all the additional context. I didn't realize the platform APIs also provided a "run on main thread" function.
I think there may be a bug in your protocol, here:
runMainThreadLoopIfNecessary()
C.mainThreadAPI1(...) // doesn't block
Specifically, runMainThreadLoopIfNecessary always gives up the main thread, so C.mainThreadAPI1 will fail. (Though, correct me if I'm wrong, I might be misunderstanding.) I don't mean to nitpick too much, but if I'm right, I do think this is a good example of where the fact that iter.Pull can result in the loss of thread lock state is very unintuitive.
At a high level, what I think you want (and again, correct me if I'm wrong) is a way for multiple packages to composably say "start the event loop if needed." Kind of like a sync.Once. Though maybe the event loop needs to be re-started multiple times? (AFAICT, it doesn't?) And at least as you pictured here, with the existing platform APIs, you want to be able to say "this other goroutine gets the main thread, not the one I'm on." I agree that RunOnMainThread on its own, as proposed, won't solve this problem.
But if the call to RunOnMainThread was made in a third package, pkgc behind a sync.Once, would that work? It's true that if some other package (not conforming to this structure) tried to use RunOnMainThread they would be block forever, but that's OK. You don't get WaitForEvents, but it sounds like you don't really need that anyway.
Am I understanding this right? Looking at your list of benefits, I think this hits all of them, except for the shared state bit. But that shared state (a sync.Once) is hidden behind a third package, pkgc, which it sounds like you weren't opposed to having anyway. Is there a place where this falls over?
OK, I think I'm beginning to understand what problem you're looking to solve exactly. Thanks for bearing with me, and for all the additional context. I didn't realize the platform APIs also provided a "run on main thread" function.
No, thank you for taking the time and effort to understand the issues and pushing a solution forward. I'm aware that main thread issues are subtle and outside Go's usual use-cases.
I think there may be a bug in your protocol, here:
runMainThreadLoopIfNecessary() C.mainThreadAPI1(...) // doesn't blockSpecifically,
runMainThreadLoopIfNecessaryalways gives up the main thread, soC.mainThreadAPI1will fail. (Though, correct me if I'm wrong, I might be misunderstanding.) I don't mean to nitpick too much, but if I'm right, I do think this is a good example of where the fact thatiter.Pullcan result in the loss of thread lock state is very unintuitive.
I believe you misunderstood. In my first scenario, I only assume a fix to the (less controversial?) #67499 (threadlocked iter.Pull not panicing when called from Cgo). runMainThreadLoopIfNecessary does not (and cannot) steal the main thread; it merely starts the event loop and immediately regains control of the main thread with the callback to mainYield. From the perspective of the platform, the subsequent C.mainThreadAPI1 is called from mainLoopInitialized.
In short, with (only) #67499 fixed, multiple Go packages and even multiple goroutines may share the main thread with the platform event loop using some combination of C.isMainThread and C.runOnMainThread.
The downside is that the main goroutine must drive the event loop by calling some blocking WaitForEvents API for every (unrelated) main-thread Go package in use. This is somewhat ameliorated because func WaitForEvent will probably be spelled func LifecycleEvents(yield func(LifecycleEvent) bool) to receive application-level lifecycle events (example). Note that this is composable because only one package saw C.isMainLoopRunning return false from its runMainThreadLoopIfNecessary. All the other packages will return immediately from their func LifecycleEvents.
However, I want to point out that the separate, thread-stealing #67694 also answers the question of what happens when a thread-locked iter.Pull is called from a different thread than the originator, in a way semantically similar to rangefuncs. If you don't expect to ever allow that case, I agree that #67694 is probably too powerful and subtle.
At a high level, what I think you want (and again, correct me if I'm wrong) is a way for multiple packages to composably say "start the event loop if needed." Kind of like a
sync.Once. Though maybe the event loop needs to be re-started multiple times? (AFAICT, it doesn't?) And at least as you pictured here, with the existing platform APIs, you want to be able to say "this other goroutine gets the main thread, not the one I'm on." I agree thatRunOnMainThreadon its own, as proposed, won't solve this problem.But if the call to
RunOnMainThreadwas made in a third package,pkgcbehind async.Once, would that work? It's true that if some other package (not conforming to this structure) tried to useRunOnMainThreadthey would be block forever, but that's OK. You don't getWaitForEvents, but it sounds like you don't really need that anyway.
Correct, except you do need LifecycleEvents as described above.
Am I understanding this right? Looking at your list of benefits, I think this hits all of them, except for the shared state bit. But that shared state (a
sync.Once) is hidden behind a third package,pkgc, which it sounds like you weren't opposed to having anyway. Is there a place where this falls over?
Your analysis is correct, with the clarification that I didn't mind a pkgc as a way to save copy-pasting subtle boiler plate across related main thread API packages. Once pkgc contains state, you're effectively forcing all main thread API packages to agree to use pkgc, right? In addition, RunOnMainThread would only be useful for the one call starting the main event loop, and not as a portable run-this-code-on-the-main-thread?
In summary: assuming #67499 is fixed to allow an iter.Pull thread-locked to the main thread to be used from a Cgo callback, what does your proposed RunOnMainThread allow that my proposed main-thread protocol doesn't?
To respond to the original question by @hajimehoshi,
One possible thing
RunOnMainThreadcould do anditer.Pullcould not is thatRunOnMainThreadcan be called even from the main thread without deadlock by detecting whether the current running thread is the main thread or not, butiter.Pullcould cause a deadlock IIUC.
Why is C.isMainThread not enough to detect that you're already running on the main thread and thus avoid the deadlock? In fact, why not always just call C.runOnMainThread? As far as I know, it never deadlocks.
Why is C.isMainThread not enough to detect that you're already running on the main thread and thus avoid the deadlock? In fact, why not always just call C.runOnMainThread? As far as I know, it never deadlocks.
Are you assuming Darwin? On Darwin, yes, these are available. In this case, in theory RunOnMainThread or iter.Pull are not needed. What about the other OSes? I assumed RunOnMainThread would be a nice portable wrapper to invoke such APIs, while your suggestion iter.Pull would not.
Why is C.isMainThread not enough to detect that you're already running on the main thread and thus avoid the deadlock? In fact, why not always just call C.runOnMainThread? As far as I know, it never deadlocks.
Are you assuming Darwin? On Darwin, yes, these are available. In this case, in theory
RunOnMainThreadoriter.Pullare not needed. What about the other OSes? I assumedRunOnMainThreadwould be a nice portable wrapper to invoke such APIs, while your suggestioniter.Pullwould not.
I'm assuming that everywhere you would need RunOnMainThread, a run-on-main-thread facility is also available. Do you have a counter-example?
I'm assuming that everywhere you would need RunOnMainThread, a run-on-main-thread facility is also available. Do you have a counter-example?
No. So, RunOnMainThread would be a wrapper, right?
I'm assuming that everywhere you would need RunOnMainThread, a run-on-main-thread facility is also available. Do you have a counter-example?
No. So, RunOnMainThread would be a wrapper, right?
At which point I ask: if you're going to call, say, C.someMainThreadRequiringAPI why not call C.nativeRunOnMain(C.someMainThreadRequiringAPI)? In other words, what is runtime.RunOnMainThread buying us?
At which point I ask: if you're going to call, say, C.someMainThreadRequiringAPI why not call C.nativeRunOnMain(C.someMainThreadRequiringAPI)? In other words, what is runtime.RunOnMainThread buying us?
I would be able to write
// ExportedAPIForUser is concurrent safe and can be called from any goroutine.
func ExportedAPIForUser() {
runtime.RunOnMainThread(platformSpecificImplOnMainThread())
}
func platformSpecificImplOnMainThread() {
// Assume this is in the main thread
C.someMainThreadRequiringAPI()
}
instead of
// ExportedAPIForUser is concurrent safe and can be called from any goroutine.
func ExportedAPIForUser() {
platformSpecificImpl()
}
func platformSpecificImpl() {
C.LockOSThread()
defer C.UnlockOSThread()
// C.nativeRunOnMain can be different on each platform. The developer has to be careful not to cause deadlock in the case when this is called from the main thread.
C.nativeRunOnMain(func() {
C.someMainThreadRequiringAPI()
})
}
Another thing is that runtime.RunOnMainThread might be available in init function before the main loop starts. This might require implementation in runtime.
At which point I ask: if you're going to call, say, C.someMainThreadRequiringAPI why not call C.nativeRunOnMain(C.someMainThreadRequiringAPI)? In other words, what is runtime.RunOnMainThread buying us?
I would be able to write
// ExportedAPIForUser is concurrent safe and can be called from any goroutine. func ExportedAPIForUser() { runtime.RunOnMainThread(platformSpecificImplOnMainThread()) } func platformSpecificImplOnMainThread() { // Assume this is in the main thread C.someMainThreadRequiringAPI() }instead of
// ExportedAPIForUser is concurrent safe and can be called from any goroutine. func ExportedAPIForUser() { platformSpecificImpl() } func platformSpecificImpl() { C.LockOSThread() defer C.UnlockOSThread()
Why is locking the thread necessary? C.nativeRunOnMain can be called from any thread.
// C.nativeRunOnMain can be different on each platform. The developer has to be careful not to cause deadlock in the case when this is called from the main thread.
I don't know of any C.nativeRunOnMain APIs that would deadlock on the main thread. Do you?
C.nativeRunOnMain(func() { C.someMainThreadRequiringAPI() })}
Why is locking the thread necessary? C.nativeRunOnMain can be called from any thread.
Well, technically this is not necessary.
I don't know of any C.nativeRunOnMain APIs that would deadlock on the main thread. Do you?
Windows with PostMessage, for example? (If the thread is already the main thread, nativeRunOnMain must run the given function immediately without PostMessage)
I don't know of any C.nativeRunOnMain APIs that would deadlock on the main thread. Do you?
Windows with
PostMessage, for example? (If the thread is already the main thread, nativeRunOnMain must run the given function immediately withoutPostMessage)
What main thread APIs are there on Windows, except StartServiceCtrlDispatcher which is the call that starts the event loop (for Windows services)?