User:Andrecaldas/SharedPtr

From FreeCAD Documentation

Introduction: sharing document objects all over!

Each FreeCAD document (App::Document) is composed of document objects (DocumentObject). Each document object should be an autonomous structure that is functional even if it is not inserted in a document. It's behaviour cannot be unexpected just because it is not attached to a document.

Many functions deal with document objects. They are usually passed a pointer to the object. But the more FreeCAD becomes multithreaded, the more we need warranties that a certain object is still functional, even if while processing takes place, the object is concurrently removed from the document, for example.

Today, assuring that a certain object will not be destroyed or removed from the document it belongs to is a complicated task that relies on verifying its internal status (usually in thread-unsafe ways). For example:

Things can become much simpler and much safer if we use shared_ptr. As long as someone is holding a shared_ptr to an object, the object is (almost) guaranteed to be valid. If we design things in such a way that the document is functional even without belonging to a document, everything shall simply work without giving unexpected results (crash).

An example of something that would crash if we were a little more multithreaded: accessing a Document that might not be available anymore.

The tree

Application, documents and document objects organized in a tree structure.
Application tree structure

The Application holds a list of instances of Document. And each document has a list of instances of DocumentObject.

The downward nodes are owned by the nodes above. Ideally, for a node to be functional, it shall not be required to be inserted in the tree. A DocumentObject shall not be required to have a Document associate to it to be functional. And a Document shall not be required to be managed by the application.

Some times, it might be desirable to have pointers upward the structure. Maybe the DocumentObject is interested in accessing its Document. This upward reference is optional and the object shall not be required to have this set in order to be functional.

Pointer management

Instances of Document and DocumentObject should be managed by shared_ptr.

Data inside a DocumentObject can be:

  • Part of the object;
  • Managed by unique_ptr; or
  • Managed by shared_ptr.

Downward reference

Downward reference means ownership. The owning object shall hold a unique_ptr or shared_ptr to the downward reference.

Preferably, once set, those smart pointers should not change. This way, functions that are called in a context where a valid unique_ptr or shared_ptr are assured to exist during the function call can take raw pointers. In particular, functions called by methods in the class that owns a resource can take raw pointers.

Upward reference

Since objects can be detached from their owners in threads different from ours, upward references should be weak_ptr.

Methods that use this weak_ptr shall deal gracefully with weak_ptr::lock() returning an invalid lock (shared_ptr). Those methods can either:

  1. Return a reasonable default (preferable); or
  2. Throw an exception.

Arbitrary reference

To reference an arbitrary object, FreeCAD uses an infrastructure named ObjectIdentifier. I do not understand much about ObjectIdentifier. But I propose a different solution I call "Accessor".

Source code: https://github.com/andre-caldas/FreeCAD/tree/NamedSketcher/src/Base/Accessor

More information: Accessor infrastructure.

A sequence of string identifies an object. Actually, it identifies some data exported by an object. It can be an integer, a string, another object... anything. When the path is resolved, a shared_ptr to the resource is returned. This shared_ptr is not supposed to be stored. It is supposed to be used locally by some function. While the returned shared_ptr exists, the accessed resource is guaranteed to exist. Because of multithreading, It might be removed from the document. It might not satisfy the path anymore... but it is assured to exist.

It is not implemented, yet... but the Accessor can have a URI that can be used to refer to an object in a different document.

Each object shall have a UUID and an optional name that can be set by the user. The UUID is assigned when the object is first created and is stored and restored when the object is serialized (saved to a file) and unserialized.

Python

Python objects are implemented by a "trampoline" class. Those shall use a shared_ptr to hold a reference to the original object. This way, the C++ counter part does not need to care about python reference counter.

Using weak_ptr in python seems not viable. Therefore, care should be taken in a way such that the shared_ptr (probably returned by the Accessor reference to some object) is not stored, but only used locally.

Advantages

TODO: list to pages with further explanation.

  • True thread safety.
  • Non blocking parallel processing.
  • The GUI thread can become very light-weight... making it more responsive.


Unneeded signal/slot synchronization

As far as I understood, in FreeCAD, we use Boost::Signals2 signal/slot infrastructure to synchronize things. However, ideally signals should not need to synchronize too much.

Any member variable that can be designed to work with atomic shared_ptr does not need a synchronous signal/slot mechanism. Of course, signals can still be used to "inform" about changes. But actions do not need to be synchronized.

Who ever holds a copy of the old data still has a valid "data pointer". If the slot takes too long to trigger, we might have even newer data! And while the slot is accessing this data, it is assured not to change. There is no need to send signals back and forth for this kind of synchronization.

This can be implemented gradually. It can be done for data that are not modified. Just like Part::TopoShape has a TopoDS_Shape that is never edited. It is read by getShape() or entirely replaced by setShape().


Lightweight GUI

Suppose a certain Part (Part::Feature) object has updated its Shape variable. For example, FreeCAD::recompute() might have been called and the Shape was updated in a separate thread. If Part::TopoShape is made thread safe with shared_ptr, then after setting the new shape, the parallel thread just has to "inform" FreeCAD to redraw the GUI through the Application event loop.

This way, the main thread can be lightweight and worry only about the GUI.

Who ever holds a copy of the old shape still has a valid "old shape". If the slot takes too long to trigger, we might have an even newer Shape! And while the slot is accessing this shape, it is assured not to change. There is no need to send signals back and forth for this kind of synchronization.

Automatic connection management

When dealing with signals and slots, we disconnect them during destruction. This is however not thread safe! We can make the destructors much simpler and disconnection thread safe through the use of std::shared_ptr (it does not have to be boost::shared_ptr). This is called automatic connection management. The signal slot mechanism holds a weak_ptr and the slot is called only if the weak_ptr can be locked. This assures that:

  1. The slot is called on a valid object.
  2. The object will remain valid until the slot finishes processing.

When the weak_ptr cannot be locked, this means the object was destructed, is being destructed or is about to be destructed. In this case, the slot is not called. In fact, in this case the slot is automatically disconnected. FreeCAD should use this in objects that are managed by shared_ptr.

This is just for boost::signals2. The Qt signaling mechanism should probably only be used for the GUI event loop, because it is not thread-safe.