Interested in improving this site? Please check the To Do page.

Multi-Threading

Basics

Most of the platform-specific threading code is hidden away in threads.c. Frontier's scheduler is implemented in process.c. The builtins.thread kernel verbs are implemented in shellsysverbs.c.

Scheduler

The basic unit that Frontier's scheduler deals in is referred to in the source code as a process. When you hit the Run button in a script window, or when you call thread.evaluate or thread.callscript from a UserTalk script, a new one-shot process is created and added to processlist, a global linked list containing all processes. A process each is also created and added to the processlist for every entry in the system.agents table, though these are not called one-shot processes.

The central routine of Frontier's scheduler is processscheduler. First, it calls oneshotscheduler to handle the one-shot processes. This function calls scheduleprocess for every one-shot process in the processlist. The scheduleprocess function creates a system thread for all newly-added processes, using the platform-agnostic API implemented in threads.c.

After the processscheduler has dealt with the one-shot processes, it ensures that the agent thread exists. This thread is responsible for running all the processes that are not one-shot processes. The main routine of the agent thread is agentthreadmain which calls agentscheduler. This function walks the process list and runs all the non-oneshot processes.

If you want to find out how to put a thread to sleep, wake it, or kill it, a good starting point is the implementation of the builtins.thread kernel verbs in shellsysverbs.c.

Thread Globals

The Usertalk interpreter and some other parts of Frontier use a lot of global variables. Many of these global variables actually need to be maintained on a per-thread basis. For this purpose, every thread has a struct of type tythreadglobals [see processinternal.h] associated with it. A struct of type typrocessrecord is linked to the hprocess field of the tythreadglobals field.

When a thread yields control of the CPU, all the globals that actually need to be thread-specific are copied into its tythreadglobals struct by copythreadglobals [in process.c]. When the thread gets back in control, the values are copied back from the thread's tythreadglobals struct to the globals by swapinthreadglobals [in process.c].

The Thread Manager API on Mac OS uses cooperatively scheduled threads. When Frontier creates a new thread on Mac OS, it calls setthreadprocs [see threads.c], which registers copythreadcontext, swapinthreadcontext, and disposethreadcontext with the Thread Manager, so that the functions will be called when the thread is swapped out, swapped in, or terminated respectively. copythreadcontext calls copythreadglobals [see previous paragraph] and swapinthreadcontext calls swapinthreadglobals.

Under the Win32 API, threads are scheduled preemptively. However, since most of Frontier was written under the assumption that it would be running on an OS with cooperative multi-threading, a mechanism needed to be introduced to make sure that only one thread at a time was executing code that could have an effect on any global variables. For this purpose, Frontier/Win32 creates a global semaphore during app initialization in initmainthread [see threads.c]. Every thread has to call grabthreadglobals or grabthreadglobalsnopriority [in threads.c] to acquire the global semaphore, and releasethreadglobals or releasethreadglobalsnopriority to release it when it wants to yield control to another thread. These functions also take care of calling swapinthreadglobals and copythreadglobals to maintain thread context. (The functions are no-ops on Mac OS.)

This global semaphore is the reason why Frontier doesn't use multi-processor NT machines effectively. It's also the reason why Frontier goes unresponsive during come time-intensive tasks like sorting huge tables or saving a copy of a large database. Unfortunately, it's often not safe to release the global semaphore during those operations without risking a crash or data corruption.

Under the Win32 API, there is the concept of every window being owned by a thread. Only that thread will receive messages (events) about the window, i.e. when the user clicks into it, resizes it, etc. By default, the owner of a window is the thread that created it. In Frontier, the only thread that knows how to process these messages is the main thread. Therefore, whenever a new window is created by another thread, it immediately transfers ownership of the window to the main thread by calling attachtomainthread [in threads.c]. As a consequence, if a thread other than the main thread wants to change any window properties (e.g. the title), it has to release the global semaphore before calling the appropriate Win32 function. That function will block while it sends a message to the main thread which has to be able to grab the global semaphore before it can make the requested change. If the global semaphore were not available to the main thread, you would get a deadlock.


Personal Tools