How to Use the ccontainer Package for Thread-Safe Object Management in Go
The ccontainer package provides a generic, thread-safe container that allows concurrent goroutines to safely read, write, swap, and wait for changes to a shared value using a broadcast-based synchronization primitive.
The ccontainer package is part of the aperturerobotics/util repository and implements a robust pattern for managing shared state in concurrent Go applications. Unlike standard mutex-protected variables, this package offers built-in wait primitives that let goroutines block efficiently until specific conditions are met, making it ideal for configuration management and lifecycle coordination.
Core Architecture of the ccontainer Package
The CContainer Struct
At the heart of the package is the CContainer[T comparable] struct defined in ccontainer/ccontainer.go (lines 10-15). This generic type holds a value of any comparable type T and embeds a broadcast.Broadcast instance to manage synchronization. The struct maintains the current value in a private val field and uses the broadcast mechanism to signal waiters when updates occur.
Broadcast-Based Synchronization
The container leverages the broadcast.Broadcast type from the broadcast/broadcast.go file to implement its locking strategy. Every public operation calls c.bcast.HoldLock, which provides exclusive access through a callback mechanism. Inside this callback, the container receives two critical functions: broadcast (to notify waiters of changes) and getWaitCh (to obtain a channel that closes when the next broadcast occurs).
This design enables fine-grained wake-ups—waiters block on channels that only close when the value actually changes, not on every lock release.
Custom Equality Handling
By default, CContainer uses Go's == operator for change detection because the type parameter T is constrained to comparable. However, for complex types like structs with slices or protobuf-generated messages, the package offers NewCContainerWithEqual (lines 22-25) to inject a custom equality function. Additionally, NewCContainerVT (lines 27-30) provides specialized support for protobuf types implementing the EqualVT interface.
Thread-Safe Operations in ccontainer
Reading Values with GetValue
The GetValue method (starting at line 32) provides zero-allocation reads of the current value. When invoked, it acquires the broadcast lock, retrieves c.val, and returns it immediately. This operation is safe for concurrent use and does not copy the underlying broadcast channel, making it efficient for high-frequency read scenarios.
Writing and Swapping Values
For mutations, the package offers two primary methods:
-
SetValue(lines 41-48): Accepts a new value, compares it with the current value using the configured equality function, and only broadcasts if a change occurred. This prevents unnecessary wake-ups when setting identical values. -
SwapValue(lines 51-67): Accepts a transformation functionfunc(T) T, applies it atomically while holding the lock, and returns the new value. This enables read-modify-write patterns without external synchronization.
Waiting for Value Changes
The container provides several blocking methods that leverage the broadcast mechanism:
WaitValue(lines 70-80): Blocks until the container holds a specific target value or the context is cancelled.WaitValueEmpty: Waits until the value becomes the zero value (nil or empty).WaitValueChange: Returns immediately with the current value, then blocks until any subsequent change occurs.WaitValueWithValidator(lines 82-95): Accepts a validator function and blocks until the value satisfies the predicate, allowing complex conditions like "wait until user.Role == admin".
These methods efficiently block on channels returned by getWaitCh, resuming only when broadcast() closes them due to actual value changes.
Practical Code Examples
Basic Container for Pointer Types
This example demonstrates managing a pointer to an integer with concurrent access:
package main
import (
"context"
"fmt"
"time"
"github.com/aperturerobotics/util/ccontainer"
)
func main() {
// Create a container initially holding nil
c := ccontainer.NewCContainer[*int](nil)
// Set a value from another goroutine
go func() {
v := 42
c.SetValue(&v) // broadcasts change
}()
// Wait for a non-nil value with timeout
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
val, err := c.WaitValue(ctx, nil)
if err != nil {
panic(err)
}
fmt.Printf("got value %d\n", *val) // prints: got value 42
// Atomically increment using SwapValue
incr := func(v *int) *int {
if v == nil {
n := 1
return &n
}
n := *v + 1
return &n
}
newVal := c.SwapValue(incr)
fmt.Printf("incremented to %d\n", *newVal)
}
Container with Custom Equality
For structs requiring custom comparison logic, use NewCContainerWithEqual:
type user struct {
ID string
Role string
}
// customEqual treats users as equal when IDs match
func customEqual(a, b *user) bool {
if a == nil || b == nil {
return a == b
}
return a.ID == b.ID
}
func main() {
c := ccontainer.NewCContainerWithEqual[*user](nil, customEqual)
// Set initial user
c.SetValue(&user{ID: "alice", Role: "viewer"})
// Wait for role change (ID stays same, but Role changes)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
go func() {
time.Sleep(100 * time.Millisecond)
c.SetValue(&user{ID: "alice", Role: "admin"})
}()
// Wait for specific condition
_, err := c.WaitValueWithValidator(ctx, func(u *user) (bool, error) {
return u != nil && u.Role == "admin", nil
}, nil)
if err == nil {
fmt.Println("User promoted to admin")
}
}
Protobuf VT Helper
For protobuf-generated types implementing EqualVT, use the specialized constructor:
import (
"github.com/aperturerobotics/util/ccontainer"
"google.golang.org/protobuf/proto"
)
type MyMessage struct {
proto.Message
// fields...
}
func main() {
// NewCContainerVT uses proto.CompareEqualVT internally
c := ccontainer.NewCContainerVT(&MyMessage{})
// Standard API works normally
c.SetValue(&MyMessage{})
val := c.GetValue()
}
Summary
- The
ccontainerpackage inaperturerobotics/utilprovides a generic, thread-safe container built on broadcast-based synchronization. - Core components include the
CContainer[T]struct, thebroadcast.Broadcastprimitive for locking and notification, and flexible equality handling viacomparableconstraints or custom functions. - Key operations include
GetValuefor zero-allocation reads,SetValuefor conditional updates,SwapValuefor atomic transformations, andWaitValuevariants for blocking until conditions are met. - File locations: Main implementation in
ccontainer/ccontainer.go(lines 10-95), tests inccontainer/ccontainer_test.go, and underlying broadcast primitives inbroadcast/broadcast.go.
Frequently Asked Questions
What makes ccontainer thread-safe?
The ccontainer package achieves thread-safety through the broadcast.Broadcast primitive, which provides a HoldLock method that serializes access to the internal value. All public methods acquire this lock before reading or modifying the container's state, ensuring that concurrent goroutines cannot race when calling GetValue, SetValue, or SwapValue.
How does ccontainer compare to using sync.RWMutex?
While sync.RWMutex allows multiple concurrent readers, ccontainer provides higher-level semantics specifically designed for shared state management. Unlike a raw mutex, ccontainer includes built-in change detection (preventing wake-ups when values are identical) and blocking wait operations (WaitValue, WaitValueWithValidator) that efficiently sleep until specific conditions are met, eliminating the need for manual condition variable management.
When should I use a custom equality function?
You should provide a custom equality function via NewCContainerWithEqual when your type T contains fields that == cannot compare (such as slices or maps), or when you want logical equality that differs from Go's default behavior. This is particularly useful for struct types where only specific fields (like an ID) should determine if a value has changed, or when working with protobuf-generated types that require EqualVT semantics.
Can I use ccontainer with protobuf-generated types?
Yes, the package provides the NewCContainerVT constructor specifically for protobuf-generated types that implement the EqualVT interface. This helper automatically configures the container to use proto.CompareEqualVT for change detection, ensuring that protobuf messages are compared according to their generated equality logic rather than Go's default == operator, which fails for protobuf messages containing slices or maps.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →