-
Notifications
You must be signed in to change notification settings - Fork 17.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: runtime/mainthread: add mainthread.Do for mediating access to the main thread #70089
Comments
Related Issues and Documentation
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.) |
(I see that earlier versions of this proposal were already discussed at length elsewhere and I did try to catch up on it first, but I apologize if I'm asking a question that's redundant from earlier discussions.) If this function will panic when called from an environment where the Go runtime does not "own" the main thread, is it justified to also offer a function to test whether a call to this function is possible? That could, for example, allow a caller to choose to treat "I'm running in the wrong mode" as an error to be handled gracefully, rather than as an exception to be handled by panicking. package mainthread
// CanDo returns true if and only if a subsequent call to [Do] would not panic.
func CanDo() bool (Another variation of this would be for |
Just so you know, the current merge window closes 21 11, this would be a quick turn around time. There is the option of getting exceptions but theses are rare and usually limited to very low dangerous community impact. |
Does this API mean that if the main package imports a package that calls runtime.LockOSThread in init (for event loop in main thread) , If so, that means we may need to modify existing valid code when using the mainthread package, which I don't think is backward-compatible,see #64777 (comment). |
@qiulaidongfeng I believe your comment is addressed by #64777 (comment). In short, |
@apparentlymart the original proposal says to panic in c-shared/c-archive mode, but I'm not against CanDo or the like. |
Change https://go.dev/cl/628815 mentions this issue: |
This proposal has been added to the active column of the proposals project |
I think we need to re-ground this discussion in concrete use cases. I'm sure at least some of this will be me asking you to repeat what's already been said in #64777, but I think getting re-consolidating this information will be helpful. Let's define main thread to mean the OS-created thread that started the process, and define startup thread to mean the thread we run Go init functions on. In typical Go binaries, these are one and the same. In c-shared and c-archive mode, the Go runtime always creates a new thread to run init functions, and exits that thread after init functions are done, so the startup thread is not the main thread. There's also a library load thread, which is the thread that first calls into the Go runtime in c-shared and c-archive mode. This may be the main thread or may be another thread, but the Go runtime relinquishes control of this thread very quickly. What are the situations where a library needs to be called on the main thread (and not just consistently on some thread, and not just on the startup thread), and the platform doesn't provide a mechanism for calling code on the main thread? Can you give concrete examples so we have something to ground the requirements in? How are libraries even sensitive to this? Do they behave differently from C if you link against them at build time (statically or dynamically) versus if you |
Thank you for this nugget. I'm very surprised that the library load thread doesn't run Go init functions, which seems to imply that a c-shared/c-archive Go function may be called by C concurrently with Go init functions. This behaviour also seems inconsistent with constructors/ELF initializers which I believe complete before
I know of only one example. Windows StartServiceCtrlDispatcher called by (among others) golang.org/x/sys/windows/svc.Run[0]. I don't know of a Windows facility for calling functions on the main thread. Other issues mention Linux namespace ("container") APIs. However, from a very cursory glance (and no experience), they don't seem to require the main thread, merely some thread. @thediveo may have more information (from comment) [0]: Incidentally,
|
Correctly, in fact it is even better (while not strictly necessary) to do Linux namespace switching on OS-level threads (tasks) other than the main/initial thread, as in some cases you might end up with throw-away threads to not leak namespace state: if you do this on the main/initial thread this will become (in Go runtime parlance) "wedged", an idle thread. You cannot simply kill this thread because then some relevant process information becomes inaccessible to the other threads of the same process. And yes, to the Linux kernel, all threads are to some extend created equal, as the main thread representing the process is still only a thread, albeit a group leader for organizational/orchestration purposes. These tasks can share certain resources to make them look like threads of the same task, but the Linux kernel allows some highly useful things, like a thread opting out of this sharing to do some useful shenanigans. |
When this happens, the call to the Go function blocks until the Go init functions have completed. See https://go.googlesource.com/go/+/refs/heads/master/src/runtime/cgocall.go#409 and also https://go.googlesource.com/go/+/refs/heads/master/src/runtime/cgo/gcc_libinit.c#52. |
Thanks Ian for the clarification.
As far as I know, all main thread requirements originate in the OS kernel (or OS libraries), and leak through to libraries. For this reason, I don't think build time versus runtime library loading makes a difference. |
In proposal review, we're realizing that we've lost track of the motivation for this whole change. Thanks for the example of StartServiceCtrlDispatcher. In #64755, you mentioned:
Given that this proposal has been through a lot, are these still good driving examples to be focusing on? Are these the only examples we can find? |
Android GUI API also requires exclusive control of the main thread, but because you're forced to run in Only other API I can think of are Linux(/etc.?) container API, but I'm no expert. Perhaps @thediveo can provide examples. |
The APIs of Docker, containerd and the k8s CRI are all with rock solid client-server architecture, so there are no restrictions to specific OS threads. Podman only so when deploying it socket-activated as a server and using its remote API socket. Using the podman grpc client is a mess though, but that's concerning unwanted namespace moves of the calling process, not a main thread restriction. Linux per se has mainly the idiosyncracy that certain elements in the procfs become inaccessible when the main thread dies, but Go covers this by "wedging" G0 in this case. This also happens in a similar way with other operating systems. My understanding for this issue is that the can safely ignore all this, because we're dealing with the prominent use cases of especially UI libraries. |
Thanks. Given this, its our understanding that this API is thus only necessary for UI toolkits and then only on macOS Catalyst and iOS. Given this context, do we need to support The other issue was implementing this for c-shared and c-archive mode. Are those needed for macOS Catalyst or iOS? It sounds like there's currently not even a workaround in these build modes, suggesting it's somehow not a problem. @eliasnaur (or anyone else), could you provide more clarity on that? |
What about macOS' AppKit and Windows'
I agree.
By "workaround", do you mean the LockOSThread-during-init trick? There's no equivalent in c-archive or c-shared programs, but it's easy to arrange for the host environment to call into Go on the main thread[0]. It would be nice to allow a GUI Go package to work in all build modes, but the proposed panic behaviour does increase the amount of preparation to call //go:build darwin
package gui
/*
static void NSApp_run(void) {
[NSapp run];
}
// Run a function on the main thread using native API.
static void runOnMainThread(f uintptr) {
dispatch_async(dispatch_get_main_queue(), ^{
callGoFunc(f);
});
}
*/
import "C"
//export callGoFunc
func callGoFunc(h uintptr) {
f := cgo.Handle(h).Value().(func())
f()
}
var mainOnce sync.Once
func NewWindow() *Window {
mainOnce.Do(func() {
go func() {
defer func() {
if err := recover(); err != nil {
// Probably c-archive or c-shared mode.
}
})
// Note that C.runOnMainThread is not going to work
// as long as the Go runtime controls it.
mainthread.Do(C.NSApp_run) // never returns.
}
})
// Create a new window, knowing that the main thread event loop
// is running.
// Note that this is not using mainthread.Do, because the Go runtime
// no longer have control over the main thread; it is blocked inside
// [NSApp run].
C.runOnMainThread(cgo.NewHandle(func() {
...
}))
} The ceremony for calling
Ceremony suggests the API is not quite right. For the sake of comparison, here's hypothetical package mainthread
// Loop schedules f to be called on the main thread.
// Loop returns immediately and does not wait for f to return.
// Once a function is scheduled, every subsequent call is ignored.
//
// Calls when the runtime doesn't control the main thread are
// ignored. This applies to c-shared and c-archive programs and
// programs that call [LockOSThread] during init.
func Loop(f func())
func NewWindow() *Window {
mainthread.Loop(C.NSApp_run)
...
} [0]: In fact, Gio calls |
On this topic Fyne is just completing a thread model migration in which we implemented a What we discovered in the process is that you likely need two versions - one which will wait until completed and the other is just scheduling the call and returning without waiting (likely an immediate return). Honestly we have been blown away by how fast the goroutine context switching is, having more builtin functionality to handle these would be a boost for sure. I agree that the questions above about specificity are critical - as the "main" routine may or may not truly be what people need. If this API can tie it to a clearly defined thread in the Go ecosystem we should be best, rather than setting expectations based on OS or other system "thread". Part of me wonders if this may need to (or in the future consider) allowing insertion into a specified goroutine instead? (i.e. main vs startup vs graphics ...) |
Not if LockOSThread is involved #21827 |
Even with that on I was getting around 2'500'000 goroutine context changes per second which is surprising to me and more than enough for most apps. |
It sounds like the claim is that we need mainthread.Do only to run an event loop that never returns, and then at that point there is no portable way to run another Go function on the main thread. If apps know which event loop is running they are encouraged to use cgo to communicate directly with it. That seems a little unsatisfying, but perhaps it is sufficient. Is it? It sounds more like mainthread.Take than mainthread.Do. Perhaps it should panic when f returns? |
That's not my take on it at all - from Fyne's point of view the |
@andydotxyz Can you expand on exactly when Fyne needs to run something on the main thread, and why? Thanks. |
There are various places but in the most general form interactions with the operating system's graphical capabilities must happen on the correct thread. For example once I have a window open if I want to update it (set content, draw to screen) it must be executed on the correct thread. Varying bad outcomes if we don't - macOS will panic the app... |
This is true, but I argue that offering a general "call this on the main thread" facility is out of scope of the Go standard library. There's a longer analysis; the gist is that while the Go runtime sometimes has control of the main thread (buildmode=exe, before Therefore, this proposal is only about taking control of the main thread when the Go runtime has it, in order to call |
Ah yes, to solve only the boot problem that makes sense. But if this is being added to the standard library it would be great to pair |
In my mind running code on "the correct thread" is very different from running code on "the main thread." Code that has to run on the main thread is for a small, though important, set of cases. Code that has to run on a specific thread is a much larger set of cases, but fortunately is also much easier to implement. We shouldn't try to mix the complicated case of running on the main thread with the simpler case of running on a specific thread. |
Apologies if I was being too vague. "the correct thread" in my previous message meant the main thread on all platforms except macOS where it can be main or the thread which the graphics was initialised on (which is often main but does not have to be). I hope that helps. |
@andydotxyz Thanks, let's try to pin this down. In #70089 (comment) @aclements asked exactly when we need to support this. See their summary at #70089 (comment). I'm not sure that your requirements got recorded anywhere. Can you describe the exact environments in which code needs to run on the main thread? Thanks. |
My understanding is that to access the graphical context correctly you will need to call the functions from the main thread on Windows, Linux, Android and iOS. On macOS this is typically done as well by convention (When using Apple's AppKit I think that is the default) though technically it could be a different thread as long as it is consistent for the life of the app. Perhaps I read the title and assumed a larger scope than it is being whittled down to. Given that init/LockOSThread exists I find the |
I could certainly be wrong, but that is not my current understanding. My understanding is that on Linux systems, with some GUI packages, you need to consistently call the GUI on the same thread. But there is no requirement that this be the main thread of the application. It can be any thread, as long as it's consistent. Linux in general does not care about the main thread of the application at all (except that it sometimes makes a difference if the main thread exits). |
@andydotxyz, it sounds like even if those graphic frameworks do require running code on the main thread, they also require running an event loop (that they provide) on the main thread, so getting other code to run on the main thread necessarily requires coordination with the external event loop. That's not something Go can do easily, so I think @eliasnaur is saying that we should just provide a way to start the external event loop and then it's up to the client of that event loop to coordinate with it to run other Go code from time to time. The question is whether that's (1) true and (2) the only use case for running code on the main thread. Can anyone link to references that support answers to either of those questions? |
I don't think it's true to say that you must also run an event loop that the graphic framework provides. Though it depends on who "they" is I suppose. Yes some code needs to listen for events and probably coordinate activities in some way - but it doesn't require a dedicated event thread nor (in most cases) executing some C based event loop from the OS / graphics framework (except macOS perhaps). I'm not sure what makes Go unsuitable for this, but that's ok. Aside from these discussions Fyne has already implemented a thread management system and in the upcoming release this includes a |
This proposal is focused only on code that must execute on the main thread of the process, by which we mean the first thread created by the system when starting the process. Can you point us to the custom Go code that you had to implement? Thanks. |
Yes indeed, though it seems with the differentiation of
The main loop of our driver is at: This supports inserted functions called into the driver at: Which is made publicly available through a helper: The runloop is started when an app runs (i.e. after As you can see we use the init/LockOSThread (in loop.go) with this both the loop setup, and all requested functions, can run on the main thread. (all the links above go to the |
All of the GUI frameworks that require running code on the main thread also require their event loop to take (or have) control of that thread. This applies to macOS AppKit and Catalyst, iOS UIKit, Android. Windows and Linux GUI require some thread, but not the main thread in particular. As for your (2), I can only offer StartServiceCtrlDispatcher. It runs an event loop on the main thread, but it is otherwise not for GUI and AFAIK there is no other API can must run on the main thread (and so there's no facility to schedule code on the main thread either). StartServiceCtrlDispatcher does fit the |
@andydotxyz Thanks. From that description it sounds like I think it's too much to ask the Go standard library to support two different packages that must run on the main thread. |
This conversation is feeling a little circular now, the "Do" you've described sounds like it can only be executed once if the intent is for main loops. In which case the "Take" naming does make more sense. Anyhow, I'm not sure why a function that simply replaces init/LockOSThread is worth a new package. Maybe "os.TakeMainThread()" is more consistent with the current API and has a smaller impact by not needing the new package? |
Personally I think
Because init/LockOSThread can only be done in the main package. The goal of |
This feels like a strange characterisation, maybe I'm still not on the same page. When someone wants to use Fyne they import |
With |
By "optional" I guess you mean that app developers can choose to manage the maintop on their own through use of the So in response the libraries will all expose a "process my events" and "process draws" and any other internal details that was in the default loop before? |
No, I mean the GUI package that needs a running main loop can initialize it on demand, say in its |
Won't that conflict with whatever may be running in If the idea for this to be callable from anywhere but also that it would be for launching event loops will such a loop wait for the main to return, will it block the current main or will they operate interspersed both being in the main context? |
Go does not promise that the Except for the special case where an |
Good point, I had forgotten that. So in this case the scheduler would vacate the mainthread? |
Yes, the scheduler would preempt any goroutine running on the main thread and have it start running the |
Proposal Details
This is #64777 (comment) in proposal form. It is a reduced and compatible variant of #64777 (comment).
I propose to add a new package,
mainthread
, with a single function,Do
, that allows Go programs to execute a function on the main thread.The larger proposal (#64777 (comment)) adds
Yield
andWaiting
to support sharing the main thread in a Go program. However, the Go runtime doesn't always have control over the main thread, most notably in c-shared or c-archive mode on platforms such as Android. In those cases, the platform facility for mediating main thread access are strictly superior tomainthread.Do
. See #64777 (comment) for a detailed analysis and assumptions.In short, I believe it's better to accept this simpler proposal to only allow Go programs access to the main thread when the Go runtime has control over it, and let other cases be handled by platform API.
I hope this can be implemented in Go 1.24.
The text was updated successfully, but these errors were encountered: