跳到主要内容
版本:dev

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:

  1. Acquire: Gets a Fory instance from the pool
  2. Use: Performs serialization/deserialization
  3. Copy: Copies result data (buffer will be reused)
  4. 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

ScenarioNon-Thread-SafeThread-Safe
Single goroutineFastestSlower (pool overhead)
Multiple goroutinesUnsafeSafe, good scaling
Memory allocationsMinimalPer-call copy
Buffer reuseYesPer-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

  1. Register types at startup: Before any concurrent operations
  2. Clone data if keeping references: With non-thread-safe instance
  3. Use per-worker instances for hot paths: Eliminates pool contention
  4. Profile before optimizing: Thread-safe overhead may be negligible