简体   繁体   中英

Using copy-on-write in multithreading environment in C++

In my Qt C++ application, I have two main threads: the GUI thread and the render thread. The GUI thread manages a list of DataObject structs that can be created and edited, while the render thread draws the objects (the render thread is unable to modify the data objects). I need a way to share the data objects between the two threads (the render thread can render thousands of objects per frame and the objects usually store hundreds of pairs of floating point numbers). Currently, my design strategy consists in storing shared_ptr of data objects and pass a list of shared pointers from the GUI thread to the render thread. I'm using mutexes and locks to prevent data races when the GUI thread is modifying a data object and the render thread is also trying to read the same data object.

On the other hand, looking at the Qt source code I see that a lot of use of the Copy-on-write technique which essentially involves shallow copies of the data objects when the access is read only and deep copies for write access. Qt also provide convenient classes like QSharedData classes to implement your own implicitly shared objects.

So in my case, I was contemplating the use of copy-on-write so that I could implement my DataObject using this technique. Instead of passing shared pointers to the render thread, I could directly pass DataObject instances (of course internall the data is still stored using a pointer) and in case the GUI thread tries to modify a data object, it would just copy the data and the two threads would continue as if they own the data object (the render thread would eventually discard its copy). Now, it seems to me that copy-on-write is quite unpopular inside the C++ world (outside of Qt) and I haven't found many examples of its use in other codebases. What would be the disadvantages of using copy-on-write in my scenario? Are there any pitfalls that I should be aware of?

There is one very simple approach that makes locks unnecessary to some extent:

  • While creating an object that is at some point shared with a different thread, use std::unique_ptr<T> . This ensures that only a restricted amount of code has access to the object. Note that you obviously don't share the pointer at this stage, otherwise this doesn't work. In summary, while writing, you want exclusive access.
  • If you want to pass the object to a different thread which does other things with it, you use std::shared_ptr<T const> . This allows sharing and makes sure that the last owner deletes the dynamic resource after use. The restriction here is that it only allows read-only access. In summary, you give up writing, because you want shared access.

Notes:

  • You need to be strict about const correctness. If an object x contains a pointer p , you can still write using this pointer, even if x is a reference-to-const. The pointer itself will be constant, but the pointee will not inherit that. This remains the same when using smart pointer, too!
  • If you pass an object to a thread with ownership transfer semantics, you can use a unique_ptr for that. This makes mutex synchronization on the object unnecessary as well, due to exclusive ownership. Typical application is to use a "task" object with both parameters and results. One thread (producer) creates the task object with the parameters, the other then takes ownership (consumer) and computes the results. It then often returns the object to the initial producer or passes it elsewhere, acting as producer itself.
  • All this doesn't address the way you transfer the (unique or shared) pointers between threads. This still needs to be done in a threads-safe way, typically involving a queue.

Using copy-on-write semantics in this case may be useful, but performance (boost) may depend on the amount of data that is changed between each render cycle. If much data is changed, almost all data will be copied nevertheless. Just copying (or moving) the data to the render thread in the first place may be more efficient as it avoids the copy-on-write checks overhead. Another drawback of using copy-on-write behavior is the Implicit sharing iterator problem , which occurs when an implicit shared object is detached when an iterator is still active.

Using copy-on-write, has some advantages in your case (compared to mutexes and/or deep copy to render thread):

  • Lock-free
  • Render thread doesn't see (partial) changes that are made by the GUI thread during the render cycle.
  • Avoids copying data as much as possible (especially useful if most of the data stay the same between two consecutive render cycles.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM