This tech note describes the issues related to threading when writing plugins.
First the basics: the X-Plane SDK is __single-threaded__ and __non-reentrant.__ Here's what that means:
- All SDK operations happen on a single thread. This thread is guaranteed to not conflict with stuff going on inside X-Plane. (In practice X-Plane does have threads, but by this rule they are unimportant to plugin authors.) So you know that when your plugin is called, the sim is in a consistent state and no other plugins are running.
- Therefore the sim and all other plugins wait while you handle a callback. Plugin performance can be a huge bottleneck and reduce framerate.
- All SDK notifications and callbacks happen synchronously. When the sim sends you a message, it waits for you to finish your message handling function before continuing. When you send another plugin a message, your plugin waits until that plugin's mesage handling function is finished. When you send another plugin a message, that other plugin runs immediately, before XPLMSendPluginMessage returns.
- While you are free to create threads inside your plugin, you may not call the SDK while it is running.
- If you have any kind of interrupt-based handler or other reentrant code, the above rule applies - only one plugin call at a time between all plugins.
You may call the SDK on any thread you create, but to call the SDK on a worker thread (one you create), you must guarantee that the SDK thread is blocked. This essentially means you blocking the SDK thread and all other plugins in order to guarantee that it is safe for your worker thread to call the SDK. Because of the complexity and performance bottlenecks involved with this, we do not recommend that you use thread synchronizers (mutexes, semaphores, etc.) to call the SDK from multiple threads.
! Locking Shared Datarefs
If you do not create threads, there are no synchronization issues between plugins because all operations occur synchronously, serially. There is no risk of another plugin doing something while your plugin is doing something.
__Question:__ Is there protection of shared datarefs against simultaneous access?
There is no such mechanism because plugins do not (normally) run simultaneously. Basically...the SDK requires plugin authors to not call XPLM calls simultaneously. Since there can be multiple plugins that generally means that reading a dataref from a thread is bad. If plugins obey this, there is no problem with shared datarefs.
You _could_ attempt to lock your shared dataref but you'd still have to worry about the plugin mgr being in an inconsistent state due to reentrancy - Sandy and I do not protect against this. So if your plugin requires reading data from a thread, you may have to consider some design changes.
! Using Threads in a Plugin Safely
Having said all that, there may be cases where you need multiple threads:
- To perform blocking I/O.
- To utilize a second CPU.
- To perform computation-heavy tasks that can't easily be done in small increments without killing framerate.
Typical problems might include reading data from the sim in the second thread, writing data to the sim in the second thread, or making XPLM calls. All of these must be done through the main thread.
Probably the simplest way to handle communication with a thread in a plugin is to use atomic operations on Windows (the interlockedXXXX routines). Create a buffer of data for the values you have to store (fortunately a float fits in the same space as a DWORD). The reader thread can read these whenever using an interlocked op to pull the data out, and the writer thread can overwrite them using an interlocked op. This will give you consistent data (no risk that for some freak reason any one dataref is half-overwritten). And it'll be a bunch faster than using a mutex or other more complicated synchronizer. (I'm told critical sections are pretty fast, but interlocked ops should beat them all hands down.)
You __do__ have to be careful that at any one given read cycle from the high-speed thread, only half your data items could be updated by the sim. If this is not ok and you need a 'clean' snapshot of the sim for some reason (seems unlikely if you send the data that fast), then you will need to use a critical section/mutex.
If you do go for a critical section/mutex/synchronizer, consider an event or semaphore or some other synchronizer where you don't have to wait for the worker thread from the main thread. For example, a reader thread could wait on an event that would be set by the main thread in response to an XPLMProcessing callback. The main thread would never sleep and would always be able to immediately signal the worker thread.
! Future Development
The single-threaded non-reentrant nature of the SDK is very unlikely to change in future SDK versions.
One enhancement we are considering for a future SDK is: the ability to start an already-registered processing callback from another thread. This would allow a worker thread to 'wake up' a flight loop callback (that was not repeating) at any time. The flight loop callback would called once the sim went around a full cycle.
This would allow a worker thread to queue some data, wake up the flight loop callback, and then wait. The flight loop callback would, upon running, find the data, do something with it, and then signal the blocked worker thread.