Goroutines - handles, signals, and signal handlers

343 views
Skip to first unread message

Mumbling Drunkard

unread,
Mar 16, 2022, 9:36:50 PM3/16/22
to golang-nuts
I'm currently working on a project where I emulate a RISC-V processor for the purpose of using Go when teaching concepts of operating systems.
I'd like for the emulator to resemble a "real" processor to the point that a realistic operating system can be implemented with most of the interesting challenges that this encompasses.
One of these challenges is the management of translation caches across multiple cores which may sometimes require TLB shootdowns.

A quick summary of TLB shootdowns: process P has two threads T1 and T2 that run on cores A and B respectively.
When T1 makes a request to deallocate a page, core A has to ensure that T2 on core B invalidates some cached translations before it marks the page as free, lest B (executing T2) might find the translation in cache and write to a page that T2 no longer should have access to.

My current approach to this is that instead of using a `sync.Mutex` and `Lock()`, I have created a custom `UntilLock(f func())` which takes in a function that should be repeatedly executed until the lock is acquired.

Current implementation: https://pastecord.com/hofoloxuru.go

Notice that every mutex where a `Lock`-`Unlock`-pair might wrap an interrupt, has to use the `UntilLock` as a `Lock` might cause the goroutine to block and never check interrupts.
The issue is further complicated if one resource R might be locked in function F and another resource S is locked in function G.
In this case, goroutine A might enter F, successfully acquire R, then signal goroutine B.
At the same time, B enters G, successfully acquires S, then signals goroutine A.
We have a deadlock.
This is pretty easily resolved though as we just add the interrupt check to the loop where F and G wait for the other goroutine to execute their handlers.

An idea came to me though...
Life would be much easier if Go had signalling between goroutines.
One could extend the syntax as `h := go f() handler` where `handler`
would have the signature: `func handler(signal int)`
You would then signal it with something like `s := sig h 1` to send a signal of 1 to the goroutine associated with h.

Idea for how code would look with signalling: https://pastecord.com/qunyqejexo.go

This would completely eliminate the issue I am currently facing and though I'm no expert in the field, I believe Go's scheduler could make the implementation require very little effort and the overhead would be practically non-existent.
This signalling would look a lot like OS signals and is a powerful feature that isn't too complex in my opinion.

Brian Candler

unread,
Mar 17, 2022, 4:19:26 AM3/17/22
to golang-nuts
I don't understand the semantics of this.  At what point is the handler function executed and in which goroutine? In other words, when you call `s := sig h 1`:

- is the handler executed in the goroutine of the caller? Then how is it different from `s := h(1)` ?
- is the handler executed in the goroutine of the target? But then how does that interact with the goroutine which is already running? Does it only execute when the goroutine is next blocked in a select { }, for example? In that case, why not just add a new branch to the select?
- is the handler executed asynchronously in a completely new goroutine? Then how is it different from this?

go func() { c<-h(1) }
s := <-c

Mumbling Drunkard

unread,
Mar 17, 2022, 8:31:52 AM3/17/22
to golang-nuts
> - is the handler executed in the goroutine of the target?

That's the idea yes.

> But then how does that interact with the goroutine which is already running? Does it only execute when the goroutine is next blocked in a select { }, for example? In that case, why not just add a new branch to the select?

This does not act as a channel at all, this would be a sort of new primitive I suppose?
It would require some help from the scheduler to manage the handling of it, but yes it would only happen when the goroutine blocks, sleeps, or is otherwise in a state of waiting.
I believe C# uses the underlying OS to achieve something similar: https://docs.microsoft.com/en-us/dotnet/api/system.threading.thread.interrupt?view=net-6.0
An issue I can see with this now is that it may leave the signallee in an unknown or undesired state which could easily lead to bugs.
Additionally, it seems C# uses exceptions to achieve this feat.
Further, the concurrency of Go might be cluttered by adding issues of reentrancy into the mix, something that doesn't really occur with non-interruptible goroutines.

In any case, I've done some more thinking and decided that my issue would currently best be solved with partial usage of something like a `TryMutex`.
With very careful implementation, I still believe signalling would be a valuable addition to the language.

peterGo

unread,
Mar 17, 2022, 8:47:47 AM3/17/22
to golang-nuts
Go1.18 has a new Mutex TryLock method.  

Petr

Mumbling Drunkard

unread,
Mar 17, 2022, 5:14:51 PM3/17/22
to golang-nuts
Thanks! It's nice to have it natively I suppose. Still doesn't completely solve my issue, but at least I won't have to implement custom lock primitives.

Kevin Chowski

unread,
Mar 21, 2022, 12:55:02 PM3/21/22
to golang-nuts
In Go, goroutines are meant to explicitly signal each other; further, it seems very intentional that goroutines are never interrupted in the middle of execution like an interrupt service routine might in a lower-level situation.

It was mentioned in a prior message a bit, but to add more details: I think I would solve this problem using channels and a select statement rather than busy waiting (e.g. looping on TryLock). Specifically, if you use a buffered channel of capacity 1 as a mutual exclusion guarantee (send to channel "locks" it, receive "unlocks" it), then you can use another channel as the "interrupt delivery" mechanism. Using a select statement allows you to simultaneously attempt locking and interrupt triaging. Of course this requires your goroutines to explicitly check for interrupts from time to time, but as I mentioned, I expect this is an intentional design choice of the Go programming language and I doubt things will change.

Note that this passing around of a channel (in the form of a context.Context: https://pkg.go.dev/context) is the "right" way to listen for timeout signals in Go too. Timeouts are sometimes implemented as some sort of interrupt in other languages, but the preference for Go is to explicitly check for that signal. This drastically simplifies the programming experience because avoids the need to reason about your goroutine arbitrarily executing different code in the middle of some other statement.

Reply all
Reply to author
Forward
0 new messages