-
Notifications
You must be signed in to change notification settings - Fork 6
Feature: GC-safe TLS #23
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
Closed
Closed
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
7202103
tls: add gc-safe pthread helper
cpunion b7573e9
bdwgc: expose root registration
cpunion cb40649
docs: update tls example
cpunion 4034713
tls: fix concurrency and error handling
cpunion 0e64df5
docs: clarify tls example imports
cpunion File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| //go:build !nogc | ||
|
|
||
| package tls | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "unsafe" | ||
|
|
||
| "github.com/goplus/lib/c" | ||
| "github.com/goplus/lib/c/bdwgc" | ||
| "github.com/goplus/lib/c/pthread" | ||
| ) | ||
|
|
||
| const slotRegistered = 1 << iota | ||
|
|
||
| type slot[T any] struct { | ||
| value T | ||
| state uintptr | ||
| destructor func(*T) | ||
| } | ||
|
|
||
| type Handle[T any] struct { | ||
| key pthread.Key | ||
| destructor func(*T) | ||
| } | ||
|
|
||
| // Alloc creates a TLS handle whose storage is visible to the Boehm GC. | ||
| func Alloc[T any](destructor func(*T)) Handle[T] { | ||
| var key pthread.Key | ||
| if ret := key.Create(slotDestructor[T]); ret != 0 { | ||
| panic(fmt.Sprintf("tls: pthread_key_create failed (errno=%d)", ret)) | ||
| } | ||
| return Handle[T]{key: key, destructor: destructor} | ||
| } | ||
|
|
||
| // Get returns the value stored in the current thread's slot. | ||
| func (h Handle[T]) Get() T { | ||
| if ptr := h.key.Get(); ptr != nil { | ||
| return (*slot[T])(ptr).value | ||
| } | ||
| var zero T | ||
| return zero | ||
| } | ||
|
|
||
| // Set stores v in the current thread's slot, creating it if necessary. | ||
| func (h Handle[T]) Set(v T) { | ||
| s := h.ensureSlot() | ||
| s.value = v | ||
| } | ||
|
|
||
| // Clear zeroes the current thread's slot value without freeing the slot. | ||
| func (h Handle[T]) Clear() { | ||
| if ptr := h.key.Get(); ptr != nil { | ||
| s := (*slot[T])(ptr) | ||
| var zero T | ||
| s.value = zero | ||
| } | ||
| } | ||
|
|
||
| func (h Handle[T]) ensureSlot() *slot[T] { | ||
| if ptr := h.key.Get(); ptr != nil { | ||
| return (*slot[T])(ptr) | ||
| } | ||
| size := unsafe.Sizeof(slot[T]{}) | ||
| mem := c.Calloc(1, size) | ||
| if mem == nil { | ||
| panic("tls: failed to allocate thread slot") | ||
| } | ||
| s := (*slot[T])(mem) | ||
| s.destructor = h.destructor | ||
| if existing := h.key.Get(); existing != nil { | ||
| c.Free(mem) | ||
| return (*slot[T])(existing) | ||
| } | ||
| if ret := h.key.Set(mem); ret != 0 { | ||
| c.Free(mem) | ||
| panic(fmt.Sprintf("tls: pthread_setspecific failed (errno=%d)", ret)) | ||
| } | ||
| registerSlot(s) | ||
| return s | ||
| } | ||
|
|
||
| func slotDestructor[T any](ptr c.Pointer) { | ||
| s := (*slot[T])(ptr) | ||
| if s == nil { | ||
| return | ||
| } | ||
| if s.destructor != nil { | ||
| s.destructor(&s.value) | ||
| } | ||
| deregisterSlot(s) | ||
| var zero T | ||
| s.value = zero | ||
| s.destructor = nil | ||
| c.Free(ptr) | ||
| } | ||
|
|
||
| func registerSlot[T any](s *slot[T]) { | ||
| if s.state&slotRegistered != 0 { | ||
| return | ||
| } | ||
| start, end := s.rootRange() | ||
| if uintptr(end) > uintptr(start) { | ||
| bdwgc.AddRoots(start, end) | ||
| } | ||
| s.state |= slotRegistered | ||
| } | ||
|
|
||
| func deregisterSlot[T any](s *slot[T]) { | ||
| if s == nil || s.state&slotRegistered == 0 { | ||
| return | ||
| } | ||
| start, end := s.rootRange() | ||
| bdwgc.RemoveRoots(start, end) | ||
| s.state &^= slotRegistered | ||
| } | ||
|
|
||
| func (s *slot[T]) rootRange() (start, end c.Pointer) { | ||
| begin := unsafe.Pointer(s) | ||
| endPtr := unsafe.Pointer(uintptr(begin) + unsafe.Sizeof(*s)) | ||
| return c.Pointer(begin), c.Pointer(endPtr) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| //go:build nogc | ||
| // +build nogc | ||
|
|
||
| package tls | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "unsafe" | ||
|
|
||
| "github.com/goplus/lib/c" | ||
| "github.com/goplus/lib/c/pthread" | ||
| ) | ||
|
|
||
| type slot[T any] struct { | ||
| value T | ||
| destructor func(*T) | ||
| } | ||
|
|
||
| type Handle[T any] struct { | ||
| key pthread.Key | ||
| destructor func(*T) | ||
| } | ||
|
|
||
| func Alloc[T any](destructor func(*T)) Handle[T] { | ||
| var key pthread.Key | ||
| if ret := key.Create(slotDestructor[T]); ret != 0 { | ||
| panic(fmt.Sprintf("tls: pthread_key_create failed (errno=%d)", ret)) | ||
| } | ||
| return Handle[T]{key: key, destructor: destructor} | ||
| } | ||
|
|
||
| func (h Handle[T]) Get() T { | ||
| if ptr := h.key.Get(); ptr != nil { | ||
| return (*slot[T])(ptr).value | ||
| } | ||
| var zero T | ||
| return zero | ||
| } | ||
|
|
||
| func (h Handle[T]) Set(v T) { | ||
| s := h.ensureSlot() | ||
| s.value = v | ||
| } | ||
|
|
||
| func (h Handle[T]) Clear() { | ||
| if ptr := h.key.Get(); ptr != nil { | ||
| s := (*slot[T])(ptr) | ||
| var zero T | ||
| s.value = zero | ||
| } | ||
| } | ||
|
|
||
| func (h Handle[T]) ensureSlot() *slot[T] { | ||
| if ptr := h.key.Get(); ptr != nil { | ||
| return (*slot[T])(ptr) | ||
| } | ||
| size := unsafe.Sizeof(slot[T]{}) | ||
| mem := c.Calloc(1, size) | ||
| if mem == nil { | ||
| panic("tls: failed to allocate thread slot") | ||
| } | ||
| s := (*slot[T])(mem) | ||
| s.destructor = h.destructor | ||
| if existing := h.key.Get(); existing != nil { | ||
| c.Free(mem) | ||
| return (*slot[T])(existing) | ||
| } | ||
| if ret := h.key.Set(mem); ret != 0 { | ||
| c.Free(mem) | ||
| panic(fmt.Sprintf("tls: pthread_setspecific failed (errno=%d)", ret)) | ||
| } | ||
| return s | ||
| } | ||
|
|
||
| func slotDestructor[T any](ptr c.Pointer) { | ||
| s := (*slot[T])(ptr) | ||
| if s == nil { | ||
| return | ||
| } | ||
| if s.destructor != nil { | ||
| s.destructor(&s.value) | ||
| } | ||
| var zero T | ||
| s.value = zero | ||
| s.destructor = nil | ||
| c.Free(ptr) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| //go:build llgo | ||
| // +build llgo | ||
|
|
||
| package tls_test | ||
|
|
||
| import ( | ||
| "sync" | ||
| "testing" | ||
|
|
||
| "github.com/goplus/lib/c/tls" | ||
| ) | ||
|
|
||
| // slotValue is the type stored in the TLS tests. | ||
| type slotValue struct { | ||
| calls int | ||
| } | ||
|
|
||
| func TestAllocSetGetClear(t *testing.T) { | ||
| var mu sync.Mutex | ||
| var destructCount int | ||
| handle := tls.Alloc[*slotValue](func(v **slotValue) { | ||
| mu.Lock() | ||
| defer mu.Unlock() | ||
| destructCount++ | ||
| if v != nil && *v != nil { | ||
| (*v).calls = -1 | ||
| } | ||
| }) | ||
|
|
||
| if v := handle.Get(); v != nil { | ||
| t.Fatalf("unexpected initial value: %v", v) | ||
| } | ||
|
|
||
| val := &slotValue{calls: 1} | ||
| handle.Set(val) | ||
| if got := handle.Get(); got != val { | ||
| t.Fatalf("Get() returned %p; want %p", got, val) | ||
| } | ||
|
|
||
| handle.Clear() | ||
| if got := handle.Get(); got != nil { | ||
| t.Fatalf("expected nil after Clear, got %p", got) | ||
| } | ||
|
|
||
| // Ensure destructor runs when the goroutine exits. | ||
| done := make(chan struct{}) | ||
| go func() { | ||
| defer close(done) | ||
| tmp := &slotValue{calls: 42} | ||
| handle.Set(tmp) | ||
| if got := handle.Get(); got != tmp { | ||
| t.Errorf("nested goroutine Get = %p; want %p", got, tmp) | ||
| } | ||
| }() | ||
| <-done | ||
|
|
||
| mu.Lock() | ||
| deferCount := destructCount | ||
| mu.Unlock() | ||
| if deferCount == 0 { | ||
| t.Fatalf("expected destructor to run for goroutine TLS slot") | ||
| } | ||
| } | ||
|
|
||
| func TestAllocIsThreadLocal(t *testing.T) { | ||
| handle := tls.Alloc[int](nil) | ||
| handle.Set(7) | ||
| if got := handle.Get(); got != 7 { | ||
| t.Fatalf("main goroutine value = %d; want 7", got) | ||
| } | ||
|
|
||
| const want = 99 | ||
| done := make(chan struct{}) | ||
| go func() { | ||
| defer close(done) | ||
| if got := handle.Get(); got != 0 { | ||
| t.Errorf("new goroutine initial value = %d; want 0", got) | ||
| } | ||
| handle.Set(want) | ||
| if got := handle.Get(); got != want { | ||
| t.Errorf("new goroutine value = %d; want %d", got, want) | ||
| } | ||
| }() | ||
| <-done | ||
|
|
||
| if got := handle.Get(); got != 7 { | ||
| t.Fatalf("main goroutine value changed to %d", got) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # GC-safe Thread Local Storage | ||
|
|
||
| The `c/tls` package implements a small helper around pthread TLS that keeps the | ||
| slot visible to the Boehm garbage collector when it is enabled. It is useful for | ||
| runtime features (such as defer stacks) that keep GC-managed pointers inside the | ||
| pthread-specific area, which Boehm does not scan by default. | ||
|
|
||
| ## Key ideas | ||
|
|
||
| - The package exposes a generic `tls.Handle[T]` type. `tls.Alloc` creates a | ||
| handle and registers the slot with Boehm (`GC_add_roots`) in GC builds, or | ||
| falls back to a plain `pthread_setspecific` slot in `-tags nogc` builds. | ||
| - A slot stores a value of type `T` directly, so even non-pointer data (for | ||
| example counters) can be stored without an additional allocation. | ||
| - An optional destructor can be supplied when allocating the handle. It is | ||
| invoked whenever the owning thread exits, allowing callers to release | ||
| resources or reset global state. | ||
|
|
||
| ## Example | ||
|
|
||
| ```go | ||
| package main | ||
|
|
||
| import ( | ||
| "log" | ||
|
|
||
| "github.com/goplus/lib/c/tls" | ||
| ) | ||
|
|
||
| type stats struct { | ||
| handled int | ||
| } | ||
|
|
||
| var threadStats = tls.Alloc[*stats](func(s **stats) { | ||
| // Destructor runs when the thread exits. | ||
| if *s != nil { | ||
| log.Printf("thread handled %d requests", (*s).handled) | ||
| } | ||
| }) | ||
|
|
||
| func recordRequest() { | ||
| cur := threadStats.Get() | ||
| if cur == nil { | ||
| cur = &stats{} | ||
| } | ||
| cur.handled++ | ||
| threadStats.Set(cur) | ||
| } | ||
|
|
||
| func resetStats() { | ||
| threadStats.Clear() | ||
| } | ||
| ``` | ||
|
|
||
| In GC builds the slot is automatically registered as a root, which prevents | ||
| Boehm from reclaiming any GC-managed memory reachable from the `stats` | ||
| instances. In builds that use `-tags nogc`, the helper still relies on pthread | ||
| TLS but simply skips the root registration step. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example code uses
log.Printf, but thelogpackage is not imported in the code snippet. This makes the example incomplete and could be confusing for users. Please consider adding the import to the example to make it self-contained and runnable.