1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
The UQM Threading and Synchronization library
This document describes the function and API for the many threading and
synchronization routines that UQM uses. People attempting to port this
to thread systems that aren't covered by SDL (such as OS 9, for
instance) will find the necessary API descriptions here.
#defines
NAMED_SYNCHRO: When #defined, all synchronization objects are named.
This should be kept on all the time, at least until 1.0.
TRACK_CONTENTION: implies NAMED_SYNCHRO. Spits out status messages
whenever a thread goes to sleep because of an object matching
TRACK_CONTENTION_CLASSES.
TRACK_CONTENTION_CLASSES: This is an ORring of enums defined in
threadlib.h.
Constructs
----------
The operation of each construct is given; these operations map to the
API in fairly straightforward ways.
The locking constructs have an argument called "sync_class"; this
indicates which value must be defined in TRACK_CONTENTION_CLASSES to
see reports from this object. Valid values are SYNC_CLASS_TOPLEVEL
for game logic synchronizers, SYNC_CLASS_AUDIO and SYNC_CLASS_VIDEO
for audio and video systems, and SYNC_CLASS_RESOURCE for low-level
resource allocators like memory systems.
- Task
This is the construct that the UQM game logic sees. In our
multithreaded code tasks map directly to threads. A task is
"assigned" to be started. One of its arguments demands that a stack
size be specified; UQM ignores this.
Tasks can voluntarily yield their timeslice by calling TaskSwitch().
This is mandatory inside busywait loops; otherwise priority inversion
problems surface under BSD and even on other systems it causes undue
CPU utilization.
Tasks have "states" that can be read or modified by various other
threads. The only state that is ever checked in the UQM code is
TASK_EXIT, which is set when the task needs to be shut down.
Task functions themselves return an integer and take a void pointer
that can be cast to "Task". This is a reference to the Task itself,
and is used to check its own state and properly de-initialize itself.
The last thing a Task function must do before exiting is call
FinishTask.
If another thread wishes to terminate a Task, it can call
"ConcludeTask" upon it. ConcludeTask will wait for the Task to
recognize the TASK_EXIT state and exit; this can produce deadlocks if
the thread requestion the Task's termination is holding a resource
that the concluding Task needs. Program with care.
API:
Task AssignTask (ThreadFunction task_func, SDWORD Stacksize,
const char *name);
DWORD Task_SetState (Task task, DWORD state_mask);
DWORD Task_ClearState (Task task, DWORD state_mask);
DWORD Task_ToggleState (Task task, DWORD state_mask);
DWORD Task_ReadState (Task task, DWORD state_mask);
void TaskSwitch (void);
void FinishTask (Task task);
void ConcludeTask (Task task);
- Thread
A somewhat more powerful and low-level construct than the Task.
Threads carry a "ThreadLocal" data object with them - at present, this
is a single Semaphore used for signaling the thread when it wants to
sleep until some graphics have been rendered. Threads can be created
with arbitrary data passed as an argument (in the form of the void *),
and can request their own ThreadLocal object. Threads can also be put
to sleep for various amounts of time. Waiting on a thread permits one
to block a thread until another completes.
API:
Thread CreateThread (ThreadFunction func, void *data,
SDWORD stackSize, const char *name);
void SleepThread (TimePeriod timePeriod);
void SleepThreadUntil (TimeCount wakeTime);
void TaskSwitch (void);
void WaitThread (Thread thread, int *status);
- Mutex
The simplest form of lock. If a thread tries to lock a mutex, it will
sleep if the mutex is already locked, and awaken once the mutex
becomes available. A Mutex must be unlocked by the same thread that
locked it, and a thread must never lock a mutex it has already locked
(without unlocking it first).
API:
Mutex CreateMutex (const char *name, DWORD syncClass);
void DestroyMutex (Mutex sem);
void LockMutex (Mutex sem);
void UnlockMutex (Mutex sem);
- RecursiveMutex
A somewhat more powerful version of the Mutex; this mutex may be
locked multiple times by the same thread, but then to truly release it
it must unlock it an equal number of times. The Draw Command Queue
(DCQ) is protected by a recursive mutex. (This construct is
occasionally called a "reentrant mutex.")
API:
RecursiveMutex CreateRecursiveMutex (const char *name,
DWORD syncClass);
void DestroyRecursiveMutex (RecursiveMutex m);
void LockRecursiveMutex (RecursiveMutex m);
void UnlockRecursiveMutex (RecursiveMutex m);
int GetRecursiveMutexDepth (RecursiveMutex m);
- Semaphore
Semaphores superficially resemble Mutexes; they are "set" and
"cleared". An integer is associated with each semaphore. Clearing
the semaphore raises the value by one; setting it decreases it by one.
Attempting to set a semaphore that is at value 0 will sleep the thread
until the semaphore is cleared.
Semaphores are used in the code to sleep until another thread is
finished doing something. They are used heavily by the FlushGraphics
code and the thread handling routines. A Task in the game is
dedicated to advancing the calendar when necessary; this task is put
to sleep by the clock semaphore when the game is paused, the player
goes into orbit or to the menu, or any of several other events that
act to suspend the game clock.
API:
Semaphore CreateSemaphore (DWORD initial, const char *name,
DWORD syncClass);
void DestroySemaphore (Semaphore sem);
void SetSemaphore (Semaphore sem);
void ClearSemaphore (Semaphore sem);
- CondVar
If a thread waits on a condition variable, it will go to sleep until
some other thread tells that condition variable to signal a thread (or
broadcast to all threads) that are waiting upon that variable. Our
condition variables are weaker than the "standard" variables.
Condition variables are generally used in conjunction with mutexes,
but ours do not permit this synchronization and are only suitable for
use if the condition variable will broadcast periodically. UQM has a
condition variable associated with the DCQ that broadcasts whenever
the queue is empty. Threads that attempt to write to the DCQ when the
DCQ is full wait on this condition variable.
API:
CondVar CreateCondVar (const char *name, DWORD syncClass);
void DestroyCondVar (CondVar);
void WaitCondVar (CondVar);
void SignalCondVar (CondVar);
void BroadcastCondVar (CondVar);
|