Article by Ayman Alheraki on January 11 2026 10:33 AM
Multithreading in object-oriented programming (OOP) involves multiple threads working concurrently to improve performance and responsiveness in software. However, multithreading introduces a unique challenge: managing shared objects in a thread-safe manner. In this article, we’ll discuss thread-safe shared objects, why they are critical, common pitfalls, and best practices with examples in C++.
In multithreaded programs, shared objects are objects that can be accessed by multiple threads concurrently. Without proper synchronization, concurrent access can lead to issues such as race conditions, data corruption, and deadlocks. These problems arise because threads may attempt to modify the object’s state at the same time, leading to unpredictable and incorrect behavior.
Race Conditions: Two or more threads modify shared data at the same time, resulting in unexpected results.
Data Corruption: If one thread modifies data while another reads it, the data might be in an inconsistent state.
Deadlocks: Multiple threads wait for each other to release resources, resulting in a system freeze.
To safely share objects between threads, several strategies are commonly employed. These include mutexes, atomic operations, thread-safe containers, and smart pointers. Let's go over each with practical examples.
A mutex (mutual exclusion) is one of the most widely used synchronization primitives. It allows only one thread to access a shared resource at a time.
class SharedObject {public: int value; std::mutex mtx; // Mutex to protect value
void increment() { std::lock_guard<std::mutex> lock(mtx); // Lock the mutex ++value; // Critical section }};
void threadFunction(SharedObject &obj) { for (int i = 0; i < 100000; ++i) { obj.increment(); }}
int main() { SharedObject obj; obj.value = 0;
std::thread t1(threadFunction, std::ref(obj)); std::thread t2(threadFunction, std::ref(obj));
t1.join(); t2.join();
std::cout << "Final Value: " << obj.value << std::endl; return 0;}In this example, SharedObject contains an integer value that both threads modify. A std::mutex is used to ensure that only one thread can increment value at a time, thus avoiding race conditions.
Atomic variables provide a thread-safe way of updating shared data without the need for a mutex. C++ provides the std::atomic template for this purpose. This is ideal when working with simple data types like integers, where atomic operations ensure that changes to the variable happen in a single, indivisible step.
std::atomic for Shared Object
class SharedObject {public: std::atomic<int> value;
void increment() { ++value; // Atomic increment }};
void threadFunction(SharedObject &obj) { for (int i = 0; i < 100000; ++i) { obj.increment(); }}
int main() { SharedObject obj; obj.value = 0;
std::thread t1(threadFunction, std::ref(obj)); std::thread t2(threadFunction, std::ref(obj));
t1.join(); t2.join();
std::cout << "Final Value: " << obj.value.load() << std::endl; return 0;}Here, the std::atomic<int> ensures that the increment() method is thread-safe without needing a mutex. It is efficient and reduces locking overhead, making it suitable for basic types.
When dealing with collections of shared objects (like lists or queues), using thread-safe containers can simplify synchronization. C++ does not provide built-in thread-safe containers, but libraries like Boost and Intel’s Threading Building Blocks (TBB) offer options.
For instance, Boost provides boost::lockfree::queue, which is a lock-free, thread-safe queue suitable for multithreaded environments.
std::vector with Mutex Protection
class SharedObject {public: std::vector<int> vec; std::mutex mtx; // Mutex to protect the vector
void addValue(int value) { std::lock_guard<std::mutex> lock(mtx); // Protect vector vec.push_back(value); }
void print() { std::lock_guard<std::mutex> lock(mtx); for (int i : vec) { std::cout << i << " "; } std::cout << std::endl; }};
void threadFunction(SharedObject &obj) { for (int i = 0; i < 5; ++i) { obj.addValue(i); }}
int main() { SharedObject obj;
std::thread t1(threadFunction, std::ref(obj)); std::thread t2(threadFunction, std::ref(obj));
t1.join(); t2.join();
obj.print(); return 0;}In this case, the std::vector is protected using a mutex. Threads can safely add values to the vector without corrupting its state.
In multithreaded applications, managing ownership of shared objects can be tricky. Smart pointers (like std::shared_ptr and std::unique_ptr) handle automatic memory management, making it easier to share objects safely among threads.
std::shared_ptr for Shared Object
class SharedObject {public: int value;
void increment() { ++value; }};
void threadFunction(std::shared_ptr<SharedObject> obj) { for (int i = 0; i < 100000; ++i) { obj->increment(); }}
int main() { std::shared_ptr<SharedObject> obj = std::make_shared<SharedObject>(); obj->value = 0;
std::thread t1(threadFunction, obj); std::thread t2(threadFunction, obj);
t1.join(); t2.join();
std::cout << "Final Value: " << obj->value << std::endl; return 0;}In this example, the std::shared_ptr ensures that the shared object is safely accessed by multiple threads. The std::shared_ptr manages the lifetime of the shared object, ensuring that the object is not destroyed prematurely while still in use by a thread.
When a shared object is read frequently but rarely written to, a read-write lock can be more efficient than a simple mutex. A read-write lock allows multiple threads to read the object simultaneously but restricts write access to one thread at a time.
C++ provides std::shared_mutex for this purpose.
std::shared_mutex
class SharedObject {public: int value; std::shared_mutex rw_mtx; // Read-Write mutex
void readValue() { std::shared_lock<std::shared_mutex> lock(rw_mtx); // Shared lock for reading std::cout << "Read Value: " << value << std::endl; }
void writeValue(int newVal) { std::unique_lock<std::shared_mutex> lock(rw_mtx); // Exclusive lock for writing value = newVal; }};
void readFunction(SharedObject &obj) { obj.readValue();}
void writeFunction(SharedObject &obj, int newVal) { obj.writeValue(newVal);}
int main() { SharedObject obj; obj.value = 42;
std::thread reader1(readFunction, std::ref(obj)); std::thread reader2(readFunction, std::ref(obj)); std::thread writer(writeFunction, std::ref(obj), 100);
reader1.join(); reader2.join(); writer.join();
return 0;}In this example, multiple threads can safely read the value using a shared lock, while only one thread at a time can write to the object using an exclusive lock.
Multithreading introduces complexity in managing shared objects safely. Using techniques like mutexes, atomic operations, thread-safe containers, and smart pointers, you can ensure that your objects are accessed and modified correctly in a multithreaded environment. The right approach depends on the specific requirements of your application, balancing performance with correctness. By carefully managing access to shared objects, you can write efficient, safe, and scalable multithreaded code in OOP.