RWLEnvelope is a header-only C++ template library that provides a simple, convenient envelope-access model for thread-safe access to objects using reader-writer locks. Our goal is to:
-
Avoid re-implementing the rw-lock: The standard C++ library (since C++14) provides excellent reader-writer lock implementations via
std::shared_mutex,std::unique_lock, andstd::shared_lock. -
Simplify thread-safe access patterns: We provide a convenient layer that makes it easy to work with shared data in multi-threaded applications without exposing the complexity of manual lock management.
-
Enable safe concurrent access: Support multiple concurrent readers while ensuring exclusive access for writers, with automatic lock management and RAII semantics.
-
Minimize boilerplate code: Reduce the amount of locking code needed to safely access shared objects through intuitive APIs.
WE DO NOT IMPLEMENT a read-writer lock; the standard C++ library has one.
We provide a header-only package simplifying the locking code around thread-safe access to your underlying type.
NOT a wrapper; an envelope.
Writing thread-safe code is hard. Without RWLEnvelope, you'd need to:
// Without RWLEnvelope - verbose and error-prone
std::shared_mutex mutex;
std::map<std::string, int> data;
// Reading
{
std::shared_lock lock(mutex);
auto it = data.find("key");
if (it != data.end()) {
std::cout << it->second << std::endl;
}
} // Lock released here
// Writing
{
std::unique_lock lock(mutex);
data["key"] = 42;
} // Lock released hereWith RWLEnvelope, the same code becomes cleaner and safer:
// With RWLEnvelope - clean and safe
siddiqsoft::RWLEnvelope<std::map<std::string, int>> data;
// Reading
data.observe<void>([](const auto& m) {
auto it = m.find("key");
if (it != m.end()) {
std::cout << it->second << std::endl;
}
});
// Writing
data.mutate<void>([](auto& m) {
m["key"] = 42;
});-
Automatic Lock Management: Locks are acquired and released automatically via RAII. No risk of forgetting to unlock.
-
Clear Intent:
observe()for reads andmutate()for writes makes your code's intent explicit and self-documenting. -
Reduced Boilerplate: No need to manually create lock objects or manage scopes. The library handles it.
-
Type Safety: The template enforces that you're working with the correct type. No accidental type mismatches.
-
Exception Safe: If your callback throws, the lock is still released properly. No deadlocks or resource leaks.
-
Flexible Access Patterns: Choose between callback-based access (for simple operations) or direct lock access (for complex operations).
-
Zero Overhead: Header-only implementation with no runtime overhead beyond the standard library's mutex.
-
Works with Any Type: Not limited to JSON or maps. Works with any type that supports move semantics.
Configuration Management:
siddiqsoft::RWLEnvelope<AppConfig> config;
// Multiple threads reading config
config.observe<std::string>([](const auto& cfg) {
return cfg.getDatabaseUrl();
});
// Single thread updating config
config.mutate<void>([](auto& cfg) {
cfg.setDatabaseUrl("new_url");
});Cache Implementation:
siddiqsoft::RWLEnvelope<std::unordered_map<std::string, CacheEntry>> cache;
// Fast concurrent reads
cache.observe<CacheEntry>([](const auto& c) {
return c.at("key");
});
// Exclusive writes
cache.mutate<void>([](auto& c) {
c["key"] = computeValue();
});Shared State in Services:
siddiqsoft::RWLEnvelope<ServiceState> state;
// Multiple reader threads
state.observe<bool>([](const auto& s) {
return s.isHealthy();
});
// Single writer thread
state.mutate<void>([](auto& s) {
s.updateMetrics();
});For comprehensive API documentation, including detailed descriptions of all methods, usage patterns, and examples, see API.md.
- You must be able to use
<shared_mutex>and<mutex>. - Minimal target is
C++17. - The build and tests are for Visual Studio 2019 under x64.
- We use
nlohmann::jsononly in our tests and the library is aware to provide a conversion operator if library is detected.
- Use the nuget SiddiqSoft.RWLEnvelope
- Copy paste..whatever works.
- The idea is to not "wrap" the underlying type forcing you to either inherit or re-implement the types but to take advantage of the underlying type's interface whilst ensuring that we have the necessary locks.
- Two methods:
- Observer/mutator model with callback and custom return to limit access and to focus the where and how to access the underlying type.
- Take advantage of init-statement in if-statement to get the contained object within a lock and have the compiler auto-release once we leave scope.
- A sample implementation (say you want a std::map with reader-writer lock)
using RWLMap = siddiqsoft::RWLEnvelope<std::map>;
#include "gtest/gtest.h"
#include "nlohmann/json.hpp"
#include "siddiqsoft/RWLEnvelope.hpp"
TEST(examples, AssignWithCallbacks)
{
siddiqsoft::RWLEnvelope<nlohmann::json> docl; // we will assign later
nlohmann::json doc2 {{"baa", 0x0baa}, {"fee", 0x0fee}, {"bee", 0x0bee}};
// Move assign here post init
docl.reassign(std::move(doc2));
// Must be empty since we moved it into the envelope
EXPECT_TRUE(doc2.empty());
// Check we have pre-change value.. Note that here we return a boolean to avoid data copy
EXPECT_TRUE(docl.observe<bool>([](const auto& doc) -> bool {
return (doc.value("fee", 0xfa17) == 0x0fee) && (doc.value("baa", 0xfa17) == 0x0baa) && (doc.value("bee", 0xfa17) == 0x0bee);
}));
EXPECT_EQ(3, docl.observe<size_t>([](const auto& doc) { return doc.size(); }));
}
TEST(examples, AssignWithDirectLocks)
{
siddiqsoft::RWLEnvelope<nlohmann::json> docl({{"foo", "bar"}, {"few", "lar"}});
nlohmann::json doc2 {{"baa", 0x0baa}, {"fee", 0x0fee}, {"bee", 0x0bee}};
// Previous document has two items..
if (auto const& [doc, rl] = docl.readLock(); rl) { EXPECT_EQ(2, doc.size()); }
// Modify the item (replace the initial with new)
if (auto [doc, wl] = docl.writeLock(); wl) { doc = std::move(doc2); };
//doc2 -> Must be empty since we moved it into the envelope
EXPECT_TRUE(doc2.empty());
// Check we have post-change value..
if (const auto& [doc, rl] = docl.readLock(); rl) { EXPECT_EQ(3, doc.size()); }
}Additional examples.
The library includes comprehensive test coverage across multiple categories:
- Simple Operations: Basic envelope creation and mutation
- Callback-Based Access: Testing
observe()andmutate()methods with various return types - Direct Lock Access: Testing
readLock()andwriteLock()with structured bindings - Reassignment: Testing
reassign()method for replacing envelope contents - Snapshot Operations: Testing
snapshot()for independent copies - Move Semantics: Testing move constructors and move assignment
- Default Construction: Envelopes with default-constructed objects
- Return Value Forwarding: Callbacks returning various types (void, int, string, bool, size_t)
- Non-JSON Types: Testing with
std::vector<int>,std::string, and other types - Move Constructor Behavior: Verifying source envelope state after move operations
- RWA Counter Tracking: Validating the read-write-action counter accuracy
- Exception Safety: Testing behavior when callbacks throw exceptions
- Independent Snapshots: Verifying snapshots are truly independent copies
- Multiple Reassignments: Testing repeated reassignment operations
- Two-Thread Tests: Concurrent readers and writers with callbacks and direct locks
- Monotonic Counter Integrity: Verifying counter never goes backwards under concurrent access
- Snapshot Consistency: Ensuring snapshots return internally consistent state
- Concurrent Reassign: Testing reassign racing with observe and snapshot operations
- Zero-Sleep Maximum Contention: All threads hammer the lock without delays
- Mixed API Concurrency: All 5 API methods used concurrently on the same envelope
- Shared Read Lock Concurrency: Multiple readers accessing simultaneously without blocking
- Concurrent Observe with Return: Readers returning values under write contention
- RWA Counter Accuracy: Verifying mutation counter matches exact mutate() count
- Paired Field Consistency: Ensuring related fields remain synchronized
- Version-Data Pairing: Validating version and data fields stay in sync during reassignment
- Total Test Cases: 20+ comprehensive test cases
- Concurrency Levels: Tests with up to 16 concurrent reader threads and 8 writer threads
- Iteration Counts: Stress tests with 5,000-10,000 iterations per thread
- Coverage Areas:
- ✅ All public API methods
- ✅ Thread safety guarantees
- ✅ Lock semantics (shared vs. exclusive)
- ✅ Exception safety
- ✅ Move semantics
- ✅ Data consistency under contention
- ✅ Return value forwarding
- ✅ JSON serialization (when available)
Tests are built using Google Test (gtest) and can be run via the CMake build system:
cmake --preset Apple-Debug
cmake --build --preset Apple-Debug
ctest --preset Apple-DebugCoverage reports are generated and tracked via Azure Pipelines CI/CD.
For detailed test results and analysis, see TEST_RESULTS.md.
We take testing seriously. The library includes 38 comprehensive tests covering:
- ✅ 4 Example tests - Real-world usage patterns
- ✅ 4 Critical race condition tests - Writer-writer contention, deadlock prevention, reassign-mutate racing
- ✅ 3 High-priority race condition tests - Exception safety, API equivalence verification
- ✅ 2 Medium-priority race condition tests - Snapshot isolation, RWA counter accuracy
- ✅ 3 Low-priority race condition tests - Memory visibility, variable lock durations
- ✅ 4 Basic concurrency tests - Multi-threaded reader-writer scenarios
- ✅ 10 Edge case tests - Exception handling, move semantics, type flexibility
- ✅ 8 Stress tests - High-contention scenarios with 5,000-10,000 iterations
All 38 tests pass successfully in ~7.6 seconds:
- Zero failures - 100% pass rate
- No race conditions detected - Verified with up to 16 concurrent threads
- Exception safe - Callbacks throwing exceptions don't corrupt state
- Deadlock-free - Explicit timeout-based deadlock detection
- Memory safe - All writes visible to readers, no torn reads
The test suite includes aggressive stress tests:
- Maximum contention: All threads hammer the lock without delays
- Mixed API usage: All 5 API methods used concurrently
- Variable lock durations: Realistic lock hold times
- Concurrent exceptions: Exception safety under contention
- Rapid reassignments: Multiple threads reassigning simultaneously
We want to hear about your usage scenarios! If you encounter:
- Edge cases not covered by our tests
- Performance issues in your specific use case
- Compatibility problems with your type or compiler
- Feature requests for additional functionality
- Documentation gaps or unclear explanations
Please open an issue on GitHub with:
- Your use case: How are you using RWLEnvelope?
- The scenario: What specific situation triggered the issue?
- Expected behavior: What should happen?
- Actual behavior: What actually happened?
- Reproduction steps: How can we reproduce it?
- Environment: Compiler, OS, C++ version
Your feedback helps us:
- Identify edge cases we haven't tested
- Optimize for real-world usage patterns
- Improve documentation and examples
- Ensure the library works reliably in your scenarios
Even if you don't have issues, we'd love to hear about:
- How you're using RWLEnvelope
- Performance characteristics in your application
- Types you're enveloping
- Patterns that work well for you
Contributions are welcome! Whether it's:
- Additional test cases for your scenarios
- Performance optimizations
- Documentation improvements
- Bug fixes
Please submit a pull request or open an issue to discuss your ideas.
© 2021 Siddiq Software LLC. All rights reserved.