HISE Docs

Threads

The Threads API class provides information about various threads and some helper functions regarding multithreaded actions. This is an extremely advanced topic but it allows you to control and synchronize the different threads in a complex HISE project.

Basically you have 4 main thread types running simultaneously in HISE:

  1. the Audio thread which renders the audio buffers coming from the DAW. This is the thread with the highest priority and making sure that this isn't interrupted or stalled should be your top priority. The utilisation of this thread will show up as CPU usage in your DAW meter.
  2. the Scripting thread , which executes all non-synchronous scripting callbacks
  3. the Message thread which renders the interface using either OpenGL or the software renderer. If you're using OpenGL, the rendering will be done on a separate thread than the rest of the UI stuff (handling mouse callbacks, etc), however this thread will hold the Message Thread lock so from our point of view, it's the same thread.
  4. the Loading thread which performs various tasks. In normal operation mode this is used to fetch the samples from the disk, but if you initialise the plugin or load user presets / swap samplemaps, it will be executed on this thread

These threads are available as constant of this class and it's HIGHLY recommended to never use magic numbers but these constants.

Threads.Audio; 		// Audio Thread
Threads.UI;    		// Message Thread
Threads.Scripting; 	// Scripting Thread
Threads.Loading;	// Loading Thread
Threads.Unknown;	// Any other thread (eg. a custom background task)
Threads.Free; 		// Idle Thread (mostly used when querying lock states)

Now you might ask yourself: if every script callback is executed on the Scripting Thread , why should I need this class at all? Well, there are a few exceptions to that rule:

With the exception of the latter, all these multithreaded use cases are not synchronised by default (with the rationale of preferring data race conditions over deadlocks and priority inversions). The exception is the user preset load, which locks the scripting thread by default during the operation. However if you start doing complex operations or even using a BackgroundTask object to perform a heavyweight task on a dedicated background thread, you might want to start thinking about proper synchronisation options and this is where this class comes in handy.

How to synchronize threads

Be aware that there are no methods for locking any thread in this API class, it only offers constants for thread identification as well as querying methods for checking the lock state of a given thread or getting information about the current thread.

If you want to lock the threads, you will have to use the scoped statement .lock(Threads.xxx) , which ensures that the lock is guaranteed to be released after the scope even in a case of a script error (or if you simply forget to release it). This is consistent with the RAII concept that is used for locking threads in JUCE (and subsequently HISE).

This code example spawns off a timer and a background thread and uses the .lock() scoped statement in order to avoid simultaneuos execution:

// set this to false in order to deactivate the locking
const var LOCK = true;
reg isTimerRunning = false;

const var timer = Engine.createTimerObject();
timer.setTimerCallback(function()
{
	.trace("TIMER CALLBACK")
	.set(isTimerRunning, true);
	
	for(i = 0; i < 4000; i++)
		Math.sin(i);
});

timer.startTimer(15);

const var backgroundTask = Engine.createBackgroundTask("big task");

backgroundTask.callOnBackgroundThread(function(t)
{
	.print("background task")
	.trace("BACKGROUND TASK");
	
	for(i = 0; i < 1000; i++)
	{
		.if(LOCK):lock(Threads.Scripting);
		
		for(j = 0; j < 1000; j++);
		{
			Math.sin(j);

			if(isTimerRunning)
			{
				Console.print("ERROR: RACE CONDITION");
			}
		}
	}
});

The perfetto profiling timeline looks like this:

And if we zoom into one of the script events, we can see that the locking is working as expected. The "Waiting for ScriptLock" phase means that either one of the threads is waiting for the other to complete and there is no simultaneos execution: while the TIMER CALLBACK is being executed, the BACKGROUND TASK is stalling and vice versa.

You might notice how the TIMER CALLBACK is waiting much longer than the BACKGROUND TASK . This is because there is almost no "idle" time between lock operations in the background task where the timer callback could grab the lock, so it must wait extremely long until it hits the lucky spot where the lock is released.

Inspector Perfetto

Multithreading is maybe one of the most complex topics in programming, so let's take a look at an example that shows how the threads are interacting with each other. We're using the Perfetto Viewer to get a timeline of all events and investigate the details.


Class methods

getCurrentThread

Returns the thread ID of the thread that is calling this method.

Threads.getCurrentThread()


The return value is one of the constants of this class, so if you can compare it against those. If you just want to dump the thread info to the console, you should use getCurrentThreadName() as this returns a string.

getCurrentThreadName

Returns the name of the current thread (for debugging purposes only!). Edit on GitHub

Threads.getCurrentThreadName()



getLockerThread

Returns the thread ID of the thread the locks the given thread ID. Edit on GitHub

Threads.getLockerThread(int threadThatIsLocked)



isAudioRunning

Returns true if the audio callback is running or false if it's suspended during a load operation.

Threads.isAudioRunning()


During some operations (eg. sample map loading, user preset switch etc), the audio thread is suspended and the loading thread is performing the operation. During that time, this method will return true so you can check if the current function is part of a heavyweight task.


isCurrentlyExporting

Returns true if the audio exporter is currently rendering the audio on a background thread. Edit on GitHub

Threads.isCurrentlyExporting()



isLocked

Returns true if the given thread is currently locked. Edit on GitHub

Threads.isLocked(int thread)



isLockedByCurrentThread

Returns true if the given thread is currently locked by the current thread. Edit on GitHub

Threads.isLockedByCurrentThread(int thread)



killVoicesAndCall

Kills all voices, suspends the audio processing and calls the given function on the loading thread. Returns true if the function was executed synchronously. Edit on GitHub

Threads.killVoicesAndCall( var functionToExecute)



startProfiling

Starts a profiling session and calls the finishCallback when ready.

Threads.startProfiling(var options, var finishCallback)


This function can be used to programatically start the profiling session. It expects two arguments:

  1. either a number (a duration in milliseconds) that will directly start the profiling session for the given amount of time or a JSON object that sets up the profiler with the given settings.
  2. A function with a single parameter that will contain the base64 encoded profiling session as argument and can be used to write this to a file (or copy it to the clipboard).

The recommended way to get this JSON object is to use the profiling options popup in the profiler toolkit and then click Export as JSON to create the current state as a JSON object. Note that in order to keep the amount of data small it's recommended to limit the profiler settings to the threads & event types you actually want to inspect.

Note that if you're passing in a JSON object it will respect the trigger type, so if you eg. have selected Mouse click, it will not start the recording right away but only at the next time you move a control.

Here's an example with some random settings exctracted from the profiler popup and a function that dumps the profile file on the user's desktop.

// Grab your current settings from the profiling options popup
// Just click Export as JSON as paste it in your script. */
const var PROFILE_OPTIONS = {
  "threadFilter": [ "UI Thread" ],
  "eventFilter": [ "Lock", "Script", "Scriptnode", "Callback",
    			   "Broadcaster", "Paint", "DSP", "Trace", "Server", 
    			   "Background Task", "Undefined", "Threads" ],
  "recordingLength": "300 ms",
  "recordingTrigger": 0
};

// Pick whatever file you like
const var PROFILE_TARGET = FileSystem.getFolder(FileSystem.Desktop).getChildFile("profile.dat");

// dumps the profile to the desktop
Threads.startProfiling(PROFILE_OPTIONS, x => PROFILE_TARGET.writeString(x));

Important: This function can also be used in the compiled plugin, however you will have to explicitely include the profiling toolkit in your compile process by adding HISE_INCLUDE_PROFILING_TOOLKIT=1 to your ExtraDefinitions field. In HISE it will print a warning if that flag isn't added to your ExtraDefinitions and try to call this method.

toString

Returns the name of the given string (for debugging purposes only!). Edit on GitHub

Threads.toString(int thread)