Behavior vs Data concurrency protection

Behavior vs Data concurrency protection

Multithreading is one of the hottest topics in C++. However, the most crucial question isn't just about running code across a vast number of cores, but rather how to synchronize all those threads effectively.

Synchronization can be broadly categorized into two main areas: data protection and behavior protection.

Data Protection Against Concurrent Access

Data protection is relatively straightforward. Consider a scenario where multiple threads share a variable—one thread writes to it while others read from it simultaneously, as illustrated below:

To ensure data integrity, this shared variable must be protected against concurrent access using synchronization primitives like mutexes, atomic variables, or other mechanisms. But why? While the standard mandates this, the underlying reason lies in processor architecture.

A CPU core doesn't interact directly with RAM for every read and write operation. Instead, it relies on cache memory, as shown below:

This caching mechanism means that two cores could hold copies of the same variable in their caches and update them independently. To prevent data inconsistency and ensure correct behavior, synchronization primitives are used, as in the following example:

Behavior Protection Against Concurrent Access

Behavior protection is more complex. Sometimes, a series of operations must be executed sequentially by a single thread, as depicted below:

While mutexes can be applied here as well:

This approach, while effective, can make the codebase more complex and error-prone. During refactoring, it's easy to accidentally move a variable outside of its mutex protection, introducing bugs.

A better approach in some cases is to refactor behavior synchronization. Where feasible, behavior synchronization can be transformed into data synchronization. In many cases, delegating tasks to a dedicated thread using a task queue is a more efficient solution, as shown below:

Libraries like Boost.Asio and Qt are useful here. For example, with Boost.Asio, you could implement the following:

With that example, the code is much cleaner and requires no explicit synchronization for multiple threads, as all operations are executed in a single thread.

Conclusion

In a multithreading environment, it's important to protect data against concurrent access. However, for situations where multiple operations need to be executed atomically, consider using a dedicated thread with a task queue. This approach simplifies synchronization and reduces the number of bugs.


要查看或添加评论,请登录

Nikolai Kutiavin的更多文章

  • Golang generic implementation for Producer-Consumer

    Golang generic implementation for Producer-Consumer

    Producer-consumer design pattern is well-known in software development. In a few words, one object produces values, and…

    6 条评论
  • Factory Method in Python with blackjack and ?h?o?o?k?e?r?s? decorators.

    Factory Method in Python with blackjack and ?h?o?o?k?e?r?s? decorators.

    Design patterns help organize code in a better way, improving readability and maintainability. Many design patterns can…

    8 条评论
  • Non-blocking synchronization for std::vector

    Non-blocking synchronization for std::vector

    In my previous post, I described how to protect a std::vector using std::mutex. It was straightforward.

    13 条评论
  • Unit-test in C++: what should you know.

    Unit-test in C++: what should you know.

    Unit tests are important for a single reason - they prove that a single component works as expected in isolation. If a…

    9 条评论
  • C++ transactional memory

    C++ transactional memory

    Sometimes I feel like an archaeologist, and today I’ll share one of my findings: an old proposal for the C++ standard…

    24 条评论
  • Why preprocessor directives are evil

    Why preprocessor directives are evil

    I'll start with a simple quiz: Do you think the code below is correct? Does it make more sense now? Preprocessor…

    42 条评论
  • Perl with classes

    Perl with classes

    Originally developed as a procedural language, Perl was later adapted to support modern Object-Oriented Programming…

    4 条评论
  • r-value reference: when and why?

    r-value reference: when and why?

    C++ has two commonly used types of references: l-value and r-value. An l-value reference points to an object with a…

    6 条评论
  • Network transport protocol: reliability and message-orientation out of the box

    Network transport protocol: reliability and message-orientation out of the box

    If I asked you to name a reliable network protocol for the transport layer, what would be the first one that comes to…

    4 条评论
  • File-backed allocator for STL containers

    File-backed allocator for STL containers

    The STL is renowned for its modular design, allowing universal algorithms to be applied to a wide range of containers…

社区洞察

其他会员也浏览了