It's instructive to see how processes, threads, communication and synchronization are handled in the low-level operating system API. These notes cover the Windows API.
Micorosoft's reference documentation is extensive. Some required reading:
The Windows API facilities for concurrent and distributed programming allow you to:
A process is an instance of a running program. A process contains
Create a process with an API call that takes a log of arguments
BOOL result = CreateProcess( applicationName, commandLine, processSecurityAttributes, threadSecurityAttributes, shouldInheritHandles, creationFlags, environmentPointer, currentDirectoryPathname, startupInfo, &processInfo);
This returns whether or not the process was created. The actual handle of the new process is returned in the process information structure.
typedef struct { HANDLE hProcess, HANDLE hThread, DWORD dwProcessId, DWORD dwThreadId }
When a process and its main thread are created with CreateProcess() they get an initial usage count of 2. So the creator should always call CloseHandle() on these things.
The processId and threadId are system-wide unique ids that provide alternate ways of manipulating processes and threads.
Creation flags: DEBUG_PROCESS, CREATE_SUSPENDED, DETACHED_PROCESS, CREATE_NEW_CONSOLE, CREATE_NO_WINDOW, ...
Startup info contains: desktop, window title, x, y, width, height, flags, showFlags, standard input handle, standard output handle, standard error handle, ...
Remember some objects are owned by the kernel (processes, threads, modules, files, mailslots, pipes, semaphores, mutexes, events, timers, ...), and some are owned by the process (brush, pen, bitmap, font, cursor, caret, window, ...).
Objects are manipulated via their handle. The Windows API exposes low-level functions for this. Use like this:
HANDLE h = CreateWindow(...); MoveWindow(h, ...);
Objects are chunks of memory and handles are (more or less) pointers to the memory (better: "opaque references"). Objects owned by the process are stored inside the process memory, so they are automatically destroyed when the process dies, but you should destroy them yourself. Kernel objects are stored in kernel memory:
TODO Picture goes here
All kernel objects have a
Most have additional attributes, depending on what kind of object it is.
You never destroy kernel objects because you do not own them. They are destroyed by the O.S. when the usage count goes to 0.
Process A Process B --------------------- ----------------------- HANDLE h1 = CreateMutex(0, FALSE, "dog"); (* Mutex "dog" created with usage count = 1 *) HANDLE h2 = OpenMutex(0, FALSE, "dog"); (* Now usage count of dog is 2 *) CloseHandle(h1); (* Usage count of dog is 1 *) CloseHandle(h2) (* Usage count is now 0, the kernel will destroy the mutex *)
If you forget to call CloseHandle() the system will close the handle when the process terminates. Not before, of course.
Objects can be shared between processes:
Three ways to terminate a process
Each thread in a process has access to the address space, global variables, and all the resources of that process. A thread does have its very own context, and (sometimes) its very own message queue.
Windows schedules threads, not processes.
Use multiple threads within a process to do several concurrent tasks, like repagination, or spell checking, or hyphenation in a word processor.
Threads are incredibly cheap — way cheaper than Windows processes. (Traditionally Unix processes have been somewhere in the middle.) Threads are fast to start up and shut down and don't impact system resources like processes do. Use multiple threads for:
A thread has its own context which includes
The O.S. does the thread scheduling and context switching so you write threads that appear to be independent of each other. In old versions of Windows (up to 3.1), you had to shove in a bunch of PeekMessage() calls to satisfy the limitations of cooperative multitasking. (Old timers will remember the whole system mercilessly hanging when connecting to a busy server with the File Manager.)
The API function is
HANDLE h = CreateThread( securityAttributes, stackSize, function, parameter, creationFlags, &threadId);
(There's also a CreateRemoteThread function to create a thread in another process.)
Almost all API calls use the handle; the only functions I know that use the thread id are AttachThreadInput() and PostThreadMessage() because these functions can manipulate the message queues of other threads in remote processes.
Three ways that I know of:
try { int exitCode = yourFunction(yourParameter); ExitThread(exitCode); } catch (...) { do some clean up stuff ExitProcess(...); }
When a thread terminates, regardless of how:
Modern Windows uses preemptive multitasking. The actual algorithm is implementation dependent and subject to tweaking at Microsoft's whim. It sort of works like this:
A thread's priority level is computed by combining its process's priority class with its own relative priority and a possible boost.
The computation is:
Priority Level = base priority + boost where base priority = if thread relative priority == idle then if process priority = realtime then 16 else 1 else if thread relative priority == time critical then if process priority == realtime then 31 else 15 else process priority + thread relative priority
Relative Priority |
Process Priority Class | |||
---|---|---|---|---|
Idle | Normal | High | Real Time | |
Idle | 1 | 1 | 1 | 16 |
Lowest | 2 | 6 | 11 | 22 |
Below Normal | 3 | 7 | 12 | 23 |
Normal | 4 | 8 | 13 | 24 |
Above Normal | 5 | 9 | 14 | 25 |
Highest | 6 | 10 | 15 | 26 |
Time Critical | 15 | 15 | 15 | 31 |
To get/set the priority of a process
To get/set the relative priority of a thread
The system like to tweak priorities whenever it thinks everyone will be happier.
To turn boosting on and off
To determine whether boosting is enabled or not
Processes with NORMAL_PRIORITY_CLASS get helped (its threads either get boosts or extended quanta) when a window is brought to the foreground.
If a thread makes a call to a function in user32.dll, the thread gets its very own message queue.
TODOThe two approaches to synchronization are to use kernel objects or some other mechanism. Non-kernel object mechanisms include the interlocked functions and critical sections, which are super high-performance (WAY better than kernel objects) but can only be used in limited contexts).
One way to do syncronization is to wait for a kernel object to be signaled. There are ten things you can wait for:
Kernel Object | Signaled When... |
---|---|
Process | It terminates |
Thread | It terminates |
Job | |
Console Input | There is unread input in the console's input buffer |
Change Notification | The specified change occurs within a specified directory or directory tree |
Memory Resource Notification | |
Event | You call SetEvent() or PulseEvent(). It is also possible to create an event in the signaled state. |
Mutex | It is not owned by any thread. If a wait is successful then the waiting thread gets ownership and the mutex goes back to being unsignaled. |
Semaphore | Its count is greater than 0. If a wait is successful then the count is decremented by 1. |
Waitable Timer | The timer reaches the due time. |
Mechanisms for IPC