Vortex Studio SDK Advanced - Threading And Synchronisation


This document will give practical guidelines for thread safety when using Vortex SDK. Care must be taken when developing plugin extensions, objects and modules. This document is targeted to developers who make their own extensions and modules in c++.

What is thread safety for Vortex SDK

Vortex SDK is a real-time simulation framework that uses several multi threading techniques to improve performances. Vortex SDK simulation is considered thread safe when all extensions are used by only one thread at a time.

A Vortex SDK application has a main simulation thread; this is where VxApplication::update() is called. It is the heartbeat of the simulation. The VxApplication and several modules will create other threads for specific purposes. The dynamics module will spawn several threads to efficiently calculate a simulation step. The graphics module will also spawn threads to efficiently feed data to the graphic card. The QtModule will also create a thread in which the UI will be updated. In general, any module (even user modules) could spawn threads to efficiently perform its tasks in ISimulatorModule update callbacks. Those user threads will use the extensions managed by the module to get data and to output data: how and when to get data and to inject data is the main topic of this document.

In order to avoid threading and synchronisation issues, it is important to understand the order of operations in a Vortex application. VxApplication does simply one thing: it updates modules one by one. In the simulation main thread (called simply the simulation thread), the following operations are done during application update():

  1. Event-based operations
    • changing application mode, 
    • any asynchronous operations that were requested
    • events are dispatched to their event listeners
    • delayed functions are executed ( see VxSim::ApplicationContext::execute:Later ).
  2. Simulation time and frame number is incremented
  3. On a network slave, the network data related to the current simulation frame is deserialized and pushed to the extensions, potentially calling observers on extensions.
  4. The callback ISimulatorModule::preUpdate() is called on all modules
  5. The callback ISimulatorModule:update() is called on all modules
    1. if present, the Dynamics module will use several threads to process the current content and calculate the object positions at the current simulation time
  6. The callback ISimulatorModule::postUpdate() is called on all modules
    1. if present, the Graphics module will use several threads to do the 3D visualization using the current simulation content
  7. On the network master, the network data is serialized and sent.
  8. Any pending delayed functions are executed.

All those actions are done sequentially. There is one VxApplication per process.

There are several helper threads in a Vortex application, but they are not relevant to thread safety issues. They are completely managed by the SDK and deal generally with hardware: devices, network, graphics, sound. 

The thread which is particularly vulnerable to threading issues is the UI thread since it needs to get current information about the simulation.

General Vortex Studio SDK thread safety guidelines

Object lifetime management

All first class objets (VxExtension, VxObject, VxSimulatorModule and VxApplication) are reference counted and should always be kept in smart pointers. For more information on reference counting see Wikipedia. There are four flavors of smart pointers in Vortex SDK. Vx::VxSmartPtr<T> and VxSim::VxSmartInteface<U> are strong references and will increment the reference count to keep an object alive. Vx::VxWeakPtr<T> and VxSim::VxWeakInterface<U> are weak references and will simply track the lifetime of the object they refer.

Creation and destruction of objects

Creation of any object is done with the factory key and VxExtensionFactory. It is recommended to call VxExtensionFactory::create() only in the simulation thread. Currently, some objects must be created in the simulation thread : creating them in any other thread might cause initialization problems.

Since extensions are referenced counted, they are destroyed when the reference count reaches 0. There can be no guarantee that the destructor is called in any specific thread: it is called when the last reference is gone. Therefore, extension should not assume it will be called in the simulation thread.

Extension constructor and destructor are not the ideal place to initialize internal values of the extension. The correct way is to override IExtension::onActive() and IExtension::onInactive(). Those callbacks are guaranteed to be called in the simulation thread, when the extension is preparing to be used in the simulation. This is true for all IExtension, IObject and ISimulatorModule.

VxSmartPtr is for Objects : VxExtension, VxObject, VxSimulatorModule and VxApplication

VxSmartPtr and VxWeakPtr do not allow any casting. It can only keep first class objects directly, and does not give easy access to the plugin interfaces so their use is very limited. 

Using VxSmartPtr<T> ensures that the object is not deleted until it is no longer referenced. When an object is referenced with a VxSmartPtr, its reference count is increased thus making certain the object is not deleted. The reference count will be decreased (and the object may be deleted) when the object is removed from the VxSmartPtr or if the VxSmartPtr is deleted.

VxWeakPtr<T> is a weak reference to an object. It is used to keep track of the lifetime of an object. A weak pointer is typically used to break reference cycles.  VxWeakPtr can be queried for validity, and the method VxWeakPtr<T>::lock() must be used to have access to the object itself.

VxSmartInterface is for Interfaces : for class derived from ISimulatorModule, IObject and IExtension

VxSmartInterface and VxWeakInterface give immediate access to the desired plugin interface. The also allow east casting between interfaces in the same object.

VxSmartInterface<U> is used to reference an extension that has interface U. It is similar to VxSmartPtr, but gives immediate access to the interface. It can be used to check the presence of an interface. It will ensure the object is not deleted until it is no longer referenced.

VxWeakInterface<U> is a weak reference to an object having the interface U. Its usage is similar to that of VxWeakPtr.

In general, VxSim::VxSmartInterface<T> is what you want.

Extensions thread safety guidelines

User defined callbacks

The user defined callback are the virtual methods on an extension interface (IDynamics, IGraphics, IExtension, ISimulatorModule, etc..) that a developer must implement to give a specific behavior to a user defined extension. Those methods are either pure virtual (therefore must be defined) or are simply virtual (the default behavior is to do nothing).

As a general rule, it is safe to assume that any user defined callbacks will be called in the simulation thread. So it is safe to query and use other extensions in those callbacks.

There are very few exceptions to this rule. 

  • IExtension

IExtension::onStateSave() can be called on a different thread. This is a performance optimization when getting the data for a Keyframe.

  • IObject

IObject::isCompatible() can be called from any thread. IObject first checks to see if adding is possible, then it will dispatch the actual addition to be done in simulation thread.

  • IQtWindow and IQtPage

Several callback functions are invoked in the Qt thread since their purpose is to create or manage QWidget

    • IQtPage::createPage
    • IQtWindow::onCreateWindow()
    • IQtWindow::onWindowCreated()
    • IQtWindow::onDestroyWindow()
    • IQtWindow::onPageAdded()
    • IQtWindow::onPageRemoved()

Interface accessor and mutator methods

Most interfaces offer, in addition to callback that can be overloaded, some data accessors that are use to query the current state of the functionality handled or modified by the user extension. Accessor methods are usually const. Those accessors are safe to be called in any thread. However, when using those accessors outside of the simulation thread, the values obtained might rapidly become stale and no longer accurate. So, calls to several accessors in a UI thread might result in mutually incoherent values.

Mutators are methods that will change the internal data of the extension, or that request an operation to be performed. It is generally not safe to call mutators outside of the simulation thread, except where specially indicated in the Interface documentation.

It is highly recommended that extension developers make there own mutators as thread safe as possible by delaying the operation until the next application update, or by using thread synchronisation techniques. 

Extension fields

Inputs, Outputs and Parameters should be treated as accesors : their values can be read at any time, in any thread. Inputs and Parameters should also be treated as mutators, their value should only be set in the simulation thread.

Observers on Inputs or Parameters are the main reason problem occurs while setting values to inputs or parameters. If an operation is triggered immediately by an observer, it will be executed in the same thread where the field was modified. Observers can be made a lot safer by setting a flag when triggered, and the operation needed is performed at the next simulation update (in the simulation thread).

IExtensoin::onStateSave() will be called in a worker thread, for performance optimization. No values should be changed, nor can any other extension be modified during that callback.

IObject specific

IObject is simply a IExtension that can have child extensions. Its callback methods follows the general rules: they are called in the simulation thread.

There are two special methods IObject::add() and IObject::remove(). Those methods are used to add or remove child extensions and may be delayed. The operation cannot be performed immediately while the simulator modules are updated or if it is requested from another thread than the simulation thread. In that case, the operation will be performed at the next simulation update, in the simulation thread. Those method are thus safe to be called anytime.

Since IObject::add() is asynchronous, the callback IObject::isCompatible() might be called on any thread. It will be called to make sure that the actual adding operation is possible. Thus, isCompatible() will be called in the same thread that requests the addition. Only type checking should be performed in this callback.

ISimulatorModule specific

ISimulatorModule is also a IExtension, and its purpose is to power the simulation. Its callback methods follows the general rules: they are called in the simulation thread.

VxApplication thread safety guidelines

The application is a not an extension, but it follows the same general rules: accessors can be used in any thread, but mutators should only be called in the simulation thread. 

Obviously, the method VxApplication::update() and VxApplication::run() are special : the simulation thread is defined by being the thread where update() or run() is called. The only reason to call those methods is to manage your own main loop, instead of using SimApp.

Most mutators of VxApplication should not be called during the update process, onPreUpdate(), onUpdate() and onPostUpdate() of modules, unless when the documentation specifies that they are asynchronous and thread safe. In that case, they can be called anytime.

Application context

All extensions that need to access global feature should do it through the application context. All methods of the ApplicationContext are thread safe and can be called anytime. The managers available from the application context are also thread safe and can be used anytime.

In addition, the application context offers the possibility of telling the VxApplication to execute a functor in the simulation thread. The method VxSim::ApplicationContext::executeLater takes a functor of signature void(void) and will execute it when it is safe to do so: in the simulation thread, either just before or just after all the modules are updated. The given functor is garanteed to be executed in the simulation thread. The relative order between different executeLater functors is respected : they are executed in the order they are received by VxApplication, but keep in mind that several threads can add functors simultaneously.

Template Design Document Revision v1.2