Overview
Many applications consist of a main thread that is typically responsible for presenting the GUI and handling user-input. Events generated by the user in response to button clicks, text updates etc. must find their way into the CLIP circuitry in order to be useful and likewise events produced within the CLIP circuitry and their associated data must be visible to the main thread and GUI. CLIP provides two separate mechanisms to enable this communication.
Sending an Event to a Regular Thread
CLIP provides an object called a Call-back function for this purpose:
In many respects the call-back function is the same as a method:
- It has a single trigger connection and it is fired when an event arrives on that connection
- When it fires it executes a Process() function containing user code
- When the Process() function returns, it automatically closes the event that triggered it.
However, there is a key difference with a call-back function and that is that instead of executing in the worker thread pool, it is executed by the main application thread that hosts the GUI.
Why Execute in the Main Application Thread?
There is a fundamental problem with modifying the GUI's state data from a worker thread (i.e. inside a method). If from our worker thread we modify part of the state and at the same time the main thread overwrites the same area of memory then the data becomes inconsistent and inevitably results in failure. In order to freely modify the GUI state without having to protect it with synchronization objects like semaphores and mutexes, CLIP provides the call-back function object, which always executes code from within the main thread. You should always use call-back functions to modify GUI state and never do it from methods.
It is important to note that CLIP can't know if you choose to modify GUI state from a method instead of a call-back function and you should never do this as it is guaranteed to result in failure at some point in the future. Blocking on mutexes within a method to access state shared between CLIP and the main application is strongly discouraged because blocking will stall a worker thread until the mutex is acquired and this potentially has a significant negative impact on performance.
How Does it Execute in the Main Thread?
CLIP taps into the main thread's message pump and when a call-back function is triggered, it sends a message into the message queue containing essential information about the call-back function that was triggered. This message is then processed by the pump (in the GUI thread) and the GUI thread makes a synchronous call to the call-back function's processing code. As a result, the processing code is executed by the GUI thread. When the processing code returns, control is returned to the message pump and it continues to process application events.
In order for CLIP to access the message pump, the application programmer must make a couple of calls during application start-up and these are described in more detail in Interfacing a Blueprint Library to a GUI.
Requesting an Event From Within a Regular Thread
CLIP provides an object called an Interface for this purpose:
An interface creates and exposes a connection to a particular CLIP object. This allows the object to be accessed from anywhere, both within the CLIP part of the application and from regular threads such as the GUI thread.
The connection can be opened and closed in the same way as any other manual connection.
How to Choose Whether to Use an Interface or a Call-Back Function
Both interfaces and call-back functions allow a regular thread to consume events from a CLIP circuit. The key difference between them is that call-back functions request an event automatically whereas interfaces must manually request an event.
This subtle difference determines whether it is the circuit or the external thread that is in control. In the case of a call-back function, the circuit is control and will invoke behavior in the main thread as soon as possible following the event being sent. In the case of an interface, the external thread is in control and it will block and wait for the circuit to provide the event before continuing.
The choice of object is therefore typically selected based on whether the circuit is updating the external thread or vice-versa. For example, consider a simple application that takes a temperature in farenheit given by the user and converts it to a temperature in Celsius displaying that back to the user. The calculation is performed within the circuitry and the GUI is hosted by an external thread. The circuit is as follows:
In the above example, the farenheit store is accessed by an interface. The "new farenheit data" event arises in the GUI thread and it requests an event from the store in order to write in the farenheit data. The GUI thread must request the write event because only the GUI thread knows when the user input is generated.
The Celsius store, on the other hand, is accessed through a call-back function. In this case the "new Celsius data" event originates from within the circuit (generated by the completion of the method). The circuit must therefore wait for that event asynchronously and then invoke the external thread as soon as it arrives.