Thread Safety
This guide covers concurrent usage patterns for Fory Go, including the thread-safe wrapper and best practices for multi-goroutine environments.
Default Fory Instance
The default Fory instance is not thread-safe:
f := fory.New()
// NOT SAFE: Concurrent access from multiple goroutines
go func() {
f.Serialize(value1) // Race condition!
}()
go func() {
f.Serialize(value2) // Race condition!
}()
Why Not Thread-Safe?
For performance, Fory reuses internal state:
- Buffer is cleared and reused between calls
- Reference resolvers are reset
- Context objects are recycled
This avoids allocations but requires exclusive access.
Thread-Safe Wrapper
For concurrent use, use the threadsafe package:
import "github.com/apache/fory/go/fory/threadsafe"
// Create thread-safe Fory
f := threadsafe.New()
// Safe for concurrent use
go func() {
data, _ := f.Serialize(value1)
}()
go func() {
data, _ := f.Serialize(value2)
}()
How It Works
The thread-safe wrapper uses sync.Pool:
- Acquire: Gets a Fory instance from the pool
- Use: Performs serialization/deserialization
- Copy: Copies result data (buffer will be reused)
- Release: Returns instance to pool
// Simplified implementation
func (f *Fory) Serialize(v any) ([]byte, error) {
fory := f.pool.Get().(*fory.Fory)
defer f.pool.Put(fory)
data, err := fory.Serialize(v)
if err != nil {
return nil, err
}
// Copy because underlying buffer will be reused
result := make([]byte, len(data))
copy(result, data)
return result, nil
}
API
// Create thread-safe instance
f := threadsafe.New()
// Instance methods
data, err := f.Serialize(value)
err = f.Deserialize(data, &target)
// Generic functions
data, err := threadsafe.Serialize(f, &value)
err = threadsafe.Deserialize(f, data, &target)
// Global convenience functions
data, err := threadsafe.Marshal(&value)
err = threadsafe.Unmarshal(data, &target)
Type Registration
Type registration should be done before concurrent use:
f := threadsafe.New()
// Register types BEFORE concurrent access
f.RegisterStruct(User{}, 1)
f.RegisterStruct(Order{}, 2)
// Now safe to use concurrently
go func() {
f.Serialize(&User{ID: 1})
}()
Thread-Safe Registration
The thread-safe wrapper handles registration safely:
// Safe: Registration is synchronized
f := threadsafe.New()
f.RegisterStruct(User{}, 1) // Thread-safe
However, for best performance, register all types at startup before concurrent use.
Zero-Copy Considerations
Non-Thread-Safe Instance
With the default Fory, returned byte slices are views into the internal buffer:
f := fory.New()
data1, _ := f.Serialize(value1)
// data1 is valid
data2, _ := f.Serialize(value2)
// data1 is NOW INVALID (buffer was reused)
Thread-Safe Instance
The thread-safe wrapper copies data automatically:
f := threadsafe.New()
data1, _ := f.Serialize(value1)
data2, _ := f.Serialize(value2)
// Both data1 and data2 are valid (independent copies)
This is safer but has allocation overhead.
Performance Comparison
| Scenario | Non-Thread-Safe | Thread-Safe |
|---|---|---|
| Single goroutine | Fastest | Slower (pool overhead) |
| Multiple goroutines | Unsafe | Safe, good scaling |
| Memory allocations | Minimal | Per-call copy |
| Buffer reuse | Yes | Per-pool-instance |
Benchmarking
func BenchmarkNonThreadSafe(b *testing.B) {
f := fory.New()
f.RegisterStruct(User{}, 1)
user := &User{ID: 1, Name: "Alice"}
for i := 0; i < b.N; i++ {
data, _ := f.Serialize(user)
_ = data
}
}
func BenchmarkThreadSafe(b *testing.B) {
f := threadsafe.New()
f.RegisterStruct(User{}, 1)
user := &User{ID: 1, Name: "Alice"}
for i := 0; i < b.N; i++ {
data, _ := f.Serialize(user)
_ = data
}
}
Patterns
Per-Goroutine Instance
For maximum performance with known goroutine count:
func worker(id int) {
// Each worker has its own Fory instance
f := fory.New()
f.RegisterStruct(User{}, 1)
for task := range tasks {
data, _ := f.Serialize(task)
process(data)
}
}
// Start workers
for i := 0; i < numWorkers; i++ {
go worker(i)
}
Shared Thread-Safe Instance
For dynamic goroutine count or simplicity:
// Single shared instance
var f = threadsafe.New()
func init() {
f.RegisterStruct(User{}, 1)
}
func handleRequest(user *User) []byte {
// Safe from any goroutine
data, _ := f.Serialize(user)
return data
}
HTTP Handler Example
var fory = threadsafe.New()
func init() {
fory.RegisterStruct(Response{}, 1)
}
func handler(w http.ResponseWriter, r *http.Request) {
response := &Response{
Status: "ok",
Data: getData(),
}
// Safe: threadsafe.Fory handles concurrency
data, err := fory.Serialize(response)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(data)
}
Common Mistakes
Sharing Non-Thread-Safe Instance
// WRONG: Race condition
var f = fory.New()
func handler1() {
f.Serialize(value1) // Race!
}
func handler2() {
f.Serialize(value2) // Race!
}
Fix: Use threadsafe.New() or per-goroutine instances.
Keeping Reference to Buffer
// WRONG: Buffer invalidated on next call
f := fory.New()
data, _ := f.Serialize(value1)
savedData := data // Just copies the slice header!
f.Serialize(value2) // Invalidates data and savedData
Fix: Clone the data or use thread-safe wrapper.
// Correct: Clone the data
data, _ := f.Serialize(value1)
savedData := make([]byte, len(data))
copy(savedData, data)
// Or use thread-safe (auto-copies)
f := threadsafe.New()
data, _ := f.Serialize(value1) // Already copied
Registering Types Concurrently
// RISKY: Concurrent registration
go func() {
f.RegisterStruct(TypeA{}, 1)
}()
go func() {
f.Serialize(value) // May not see TypeA
}()
Fix: Register all types before concurrent use.
Best Practices
- Register types at startup: Before any concurrent operations
- Clone data if keeping references: With non-thread-safe instance
- Use per-worker instances for hot paths: Eliminates pool contention
- Profile before optimizing: Thread-safe overhead may be negligible