title: Thread Safety sidebar_position: 100 id: thread_safety license: | Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
This guide covers concurrent usage patterns for Fory Go, including the thread-safe wrapper and best practices for multi-goroutine environments.
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! }()
For performance, Fory reuses internal state:
This avoids allocations but requires exclusive access.
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) }()
The thread-safe wrapper uses sync.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 }
// 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 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}) }()
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.
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)
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.
| 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 |
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
}
}
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) }
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 }
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) }
// 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.
// 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
// 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.