-
Notifications
You must be signed in to change notification settings - Fork 56
Threading Models
Threading in Intercept allows addon makers to move processing loads off of the main game thread and onto multiple CPU cores in a way that can drastically increase FPS in your modifications. By moving math, book keeping, etc, out of the main game thread, the game can execute continuously while waiting on your modification to update the game.
Arma, and the RV Engine itself, is mostly single threaded. All major game logic executes in a single main thread, with some synchronization for file loading and some AI routines (as far as we know). This includes the SQF scripting engine as well. To safely execute code in Intercept we have to get into this thread. Luckily the SQF engine, as mentioned, is innately blocking due to the fact that all SQF execution takes place in this thread (remember spawn in SQF is not real threads, it feeds into the scripting scheduler). The RV Engine executes 3 milliseconds of scheduled SQF code each frame, and it executes all events in a totally blocking fashion (the entire world stops, and SQF can do its thing). Intercept works in the same exact way.
When an SQF function executes in Intercept it will wait (one way or the other, as it will be seen below) for the game engine to be blocked. This can happen innately when an event is executed like fired or killed, on_frame and post_init, or, it can happen for a period of 3ms each frame (this is added on top of the 3ms SQF has, so there is a total of 6ms of blocking execution each frame with Intercept running at full capacity). How Intercept knows the world is blocked is explained below.
In Intercept, each call has no implicit thread safety, and will call directly into the engine unless you specifically lock the RV engine or execute code from one of the already defined events that Intercept can raise. This is achieved using the intercept::client::invoker_lock class. It operates in a similar way to the std::lock_guard in the STD C++ library and it is incredibly simple to use as shown in the example below.
using namespace intercept;
void thread_func() {
while (true) {
// initializes and obtains a lock during the next available invocation period
client::invoker_lock thread_lock;
// call an SQF function
sqf::side_chat(sqf::player(), "Hello Threaded World!");
// thread_lock now goes out of scope, releasing the lock.
}
}As seen, thread_lock is a current scope only lock, as soon as the thread_lock object leaves the scope it was declared in, the lock is released. It is impossible to copy the lock, or move the lock, and as such it is incredibly safe in releasing the lock and preventing hanging execution from client plugins. Extending the example above to show a loop that sleeps and doesn't eat the process shows how you can use unnamed scopes to control locking.
using namespace intercept;
void thread_func() {
while (true) {
{ // unnamed scope
// initializes and obtains a lock during the next available invocation period
client::invoker_lock thread_lock;
// call an SQF function
sqf::side_chat(sqf::player(), "Hello Threaded World!");
} // thread_lock now goes out of scope, releasing the lock.
sleep(250);
}
}The invoker_lock class can also be passed a boolean in it's constructor to delay locking until locking is explicitly called. An example is given below.
using namespace intercept;
void thread_func() {
while (true) {
{
// initializes a lock object, but delays locking until we call the lock method
client::invoker_lock thread_lock(true);
if(some_condition) {
thread_lock.lock(); // now obtain a lock
sqf::side_chat(sqf::player(), "Conditionally Hello Threaded World!");
}
// thread_lock now goes out of scope, releasing the lock (if it was obtained).
}
sleep(250);
}
}In this mode, game execution is blocked (as well as any other Intercept plugins trying to access the game) until the invoker_lock object goes out of scope.
A more full example can be found in the example plugins, specifically the one titled solely "example".