Modern operating systems provide three basic approaches for building
concurrent programs:
1. Processes. With this approach, each logical control flow is a process that is
scheduled and maintained by the kernel. Since processes have separate virtual
address spaces, flows that want to communicate with each other must use some
kind of explicit interprocess communication (IPC) mechanism.
2. I/O multiplexing.This is a form of concurrent programming where applications
explicitly schedule their own logical flows in the context of a single process.
Logical flows are modeled as state machines that the main program explicitly
transitions from state to state as a result of data arriving on file descriptors.
Since the program is a single process, all flows share the same address space.
3. Threads. Threads are logical flows that run in the context of a single process
and are scheduled by the kernel. You can think of threads as a hybrid of the
other two approaches, scheduled by the kernel like process flows and sharing
the same virtual address space like I/O multiplexing flows.
approach
美[əˈproʊtʃ]
n. 相似的事物
maintain
美[meɪnˈteɪn]
v. 維持,保持
explicit
美[ɪkˈsplɪsɪt]
adj. 易於理解的
interprocess
美['ɪntəˌprɒses]
adj. 程式間的
mechanism
美[ˈmekənɪzəm]
n. [生]機制,機能
multiplexing
美['mʌltɪpleksɪŋ]
n. 複用
12.1 Concurrent Programming with Processes
To see how this might work, suppose we have two clients and a server that is
listening for connection requests on a listening descriptor (say, 3). Now suppose
that the server accepts a connection request from client 1 and returns a connected
descriptor (say, 4), as shown in Figure 12.1. After accepting the connection request,
the server forks a child, which gets a complete copy of the server’s descriptor table.
The child closes its copy of listening descriptor 3, and the parent closes its copy of
connected descriptor 4, since they are no longer needed. This gives us the situation
shown in Figure 12.2, where the child process is busy servicing the client.
12.1.1 A Concurrent Server Based on Processes
First, servers typically run for long periods of time, so we must include a
SIGCHLD handler that reaps zombie children (lines 4–9). Since SIGCHLD
signals are blocked while the SIGCHLD handler is executing, and since Linux
signals are not queued, the SIGCHLD handler must be prepared to reap
multiple zombie children.
Second, the parent and the child must close their respective copies of connfd
(lines 33 and 30, respectively). As we have mentioned, this is especially im-
portant for the parent, which must close its copy of the connected descriptor
to avoid a memory leak.
Finally, because of the reference count in the socket’s fifile table entry, the
connection to the client will not be terminated until both the parent’s and
child’s copies of connfd are closed.
reap
美[riːp]
vt. 收割
respective
美[rɪˈspektɪv]
adj. 各自的
leak
美[liːk]
n. 洩漏
12.1.2 Pros and Cons of Processes
Processes have a clean model for sharing state information between parents and
children: fifile tables are shared and user address spaces are not. Having separate
address spaces for processes is both an advantage and a disadvantage. It is im-
possible for one process to accidentally overwrite the virtual memory of another
process, which eliminates a lot of confusing failures—an obvious advantage.
accidentally
美[ˌæksəˈdɛntəli]
adv. 偶然地
eliminate
美[ɪˈlɪmɪneɪt]
v. 排除,清除
obvious
美[ˈɑːbviəs]
adj. 明顯的
12.2 Concurrent Programming with I/O Multiplexing
int select(int n, fd_set *fdset, NULL, NULL, NULL);
FD_ZERO(fd_set *fdset);
FD_CLR(int fd, fd_set *fdset);
FD_SET(int fd, fd_set *fdset);
FD_ISSET(int fd, fd_set *fdset);
typedef struct{
long int fds_bits[32];
} fd_set;
12.2.1 A Concurrent Event-Driven Server Based on I/O Multiplexing
I/O multiplexing can be used as the basis for concurrent event-driven programs,
where flflows make progress as a result of certain events. The general idea is to
model logical flflows as state machines. Informally, a state machine is a collection of
states, input events, and transitions that map states and input events to states. Each
transition maps an (input state, input event) pair to an output state. A self-loop is
a transition between the same input and output state. State machines are typically
drawn as directed graphs, where nodes represent states, directed arcs represent
transitions, and arc labels represent input events. A state machine begins execution
in some initial state. Each input event triggers a transition from the current state
to the next state.
progress
美[ˈprɑːɡres]
n. 進步;前進
logical
美[ˈlɑːdʒɪkl]
adj. 邏輯
informally
美[ɪnˈfɔmlɪ]
adv. 通俗地
12.2.2 Pros and Cons of I/O Multiplexing
A signifificant disadvantage of event-driven designs is coding complexity. Our
event-driven concurrent echo server requires three times more code than the
process-based server. Unfortunately, the complexity increases as the granularity
of the concurrency decreases. By granularity, we mean the number of instructions
that each logical flflow executes per time slice. For instance, in our example concur-
rent server, the granularity of concurrency is the number of instructions required
to read an entire text line. As long as some logical flflow is busy reading a text line,
no other logical flflow can make progress. This is fifine for our example, but it makes
our event-driven server vulnerable to a malicious client that sends only a partial
text line and then halts. Modifying an event-driven server to handle partial text
lines is a nontrivial task, but it is handled cleanly and automatically by a process-
based design. Another signifificant disadvantage of event-based designs is that they
cannot fully utilize multi-core processors.
significant
美[sɪɡˈnɪfɪkənt]
adj. 重要的;顯著的
complexity
美[kəmˈpleksəti]
n. 複雜性
vulnerable
美[ˈvʌlnərəbl]
adj. 易受攻擊的
malicious
美[məˈlɪʃəs]
adj. 惡意的
partial
美[ˈpɑːrʃl]
adj. 部分的
nontrivial
美[nɒn'trɪviəl]
adj. 非平凡的
utilize
美[ˈjuːtəlaɪz]
vt. 利用
12.3 Concurrent Programming with Threads
Logical flows based on threads combine qualities of flows based on processes
and I/O multiplexing. Like processes, threads are scheduled automatically by the
kernel and are known to the kernel by an integer ID. Like flows based on I/O
multiplexing, multiple threads run in the context of a single process, and thus they
share the entire contents of the process virtual address space, including its code,
data, heap, shared libraries, and open files.
12.3.1 Thread Execution Model
Thread execution differs from processes in some important ways. Because a
thread context is much smaller than a process context, a thread context switch is
faster than a process context switch. Another difference is that threads, unlike pro-
cesses, are not organized in a rigid parent-child hierarchy. The threads associated
with a process form a pool of peers, independent of which threads were created
by which other threads. The main thread is distinguished from other threads only
in the sense that it is always the fifirst thread to run in the process. The main impact
of this notion of a pool of peers is that a thread can kill any of its peers or wait
for any of its peers to terminate. Further, each peer can read and write the same
shared data.
differ
美[ˈdɪfər]
v. 有區別
rigid
美[ˈrɪdʒɪd]
adj. 嚴格的
hierarchy
美[ˈhaɪərɑːrki]
n. 分層,層次
associate
美[əˈsoʊsieɪt]
v. 使與...有關係
distinguish
美[dɪˈstɪŋɡwɪʃ]
v. 區分,使有別於
impact
美[ˈɪmpækt]
n. 影響
12.3.2 Posix Threads
Figure 12.13 shows a simple Pthreads program. The main thread creates a peer
thread and then waits for it to terminate. The peer thread prints Hello, world!\n
and terminates. When the main thread detects that the peer thread has terminated,
it terminates the process by calling exit. This is the fifirst threaded program we
have seen, so let us dissect it carefully. The code and local data for a thread are
encapsulated in a thread routine. As shown by the prototype in line 2, each thread
routine takes as input a single generic pointer and returns a generic pointer. If
you want to pass multiple arguments to a thread routine, then you should put the
arguments into a structure and pass a pointer to the structure. Similarly, if you
want the thread routine to return multiple arguments, you can return a pointer to
a structure.
detect
美[dɪˈtekt]
v. 發現;查明
dissect
美[dɪˈsekt]
vt. 解剖;仔細分析
encapsulate
美[ɪnˈkæpsjuleɪt]
vt. 封裝
routine
美[ruːˈtiːn]
n. 慣例,常規;例程
prototype
美[ˈproʊtətaɪp]
n. 原型
12.3.3 Creating Threads
int pthread_create(pthread_t *tid, NULL, func *f, void *arg);
pthread_t pthread_self(void);
12.3.4 Terminating Threads
1. The thread terminates implicitly when its top-level thread routine returns.
2. If the main thread calls pthread_exit, it waits for all other peer threads to
terminate and then terminates the main thread and the entire process with a return
value of thread_return.
3. Some peer thread calls the Linux exit function, which terminates the process
and all threads associated with the process.
4. Another peer thread terminates the current thread by calling the pthread_
cancel function with the ID of the current thread.
implicitly
美[ɪmˈplɪsɪtlɪ]
adv. 含蓄地
entire
美[ɪnˈtaɪər]
adj. 全部的
12.3.5 Reaping Terminated Threads
Notice that, unlike the Linux wait function, the pthread_join function can
only wait for a specifific thread to terminate. There is no way to instruct pthread_
join to wait for an arbitrary thread to terminate. This can complicate our code by
forcing us to use other, less intuitive mechanisms to detect process termination.
Indeed, Stevens argues convincingly that this is a bug in the specifification.
instruct
美[ɪnˈstrʌkt]
vt. 命令
arbitrary
美[ˈɑːrbɪtreri]
adj. 任意的
complicate
美[ˈkɑːmplɪkeɪt]
v. 使複雜化
12.3.6 Detaching Threads
At any point in time, a thread is joinable or detached. A joinable thread can be
reaped and killed by other threads. Its memory resources (such as the stack) are
not freed until it is reaped by another thread. In contrast, a detached thread cannot
be reaped or killed by other threads. Its memory resources are freed automatically
by the system when it terminates.
detach
美[dɪˈtætʃ]
v. 分離
contrast
美[kənˈtræst]
v. 對比;顯出明顯的差異
12.3.7 Initializing Threads
The pthread_once function allows you to initialize the state associated with a
thread routine.
initialize
美[ɪˈnɪʃəˌlaɪz]
vt. 初始化
associate
美[əˈsəʊʃieɪt]
v. 使與...有關係
12.3.8 A Concurrent Server Based on Threads
Another issue is avoiding memory leaks in the thread routine. Since we are
not explicitly reaping threads, we must detach each thread so that its memory
resources will be reclaimed when it terminates (line 31). Further, we must be
careful to free the memory block that was allocated by the main thread (line 32).
explicitly
美[ɪk'splɪsɪtlɪ]
adv. 明白地,明確地
reclaim
美[rɪˈkleɪm]
vt. 回收再利用
further
美[ˈfɜːrðər]
adv. 進一步地
allocate
美[ˈæləkeɪt]
v. 分配
12.4 Shared Variables in Threaded Programs
There are some basic questions to work through in order to understand
whether a variable in a C program is shared or not: (1) What is the underlying
memory model for threads? (2) Given this model, how are instances of the vari-
able mapped to memory? (3) Finally, how many threads reference each of these
instances? The variable is shared if and only if multiple threads reference some
instance of the variable.
variable
美[ˈveriəbl]
n. 可變因素,變數
underlying
美[ˌʌndərˈlaɪɪŋ]
adj. 表面下的,下層的
12.4.1 Threads Memory Model
The memory model for the separate thread stacks is not as clean. These
stacks are contained in the stack area of the virtual address space and are usually
accessed independently by their respective threads. We say usually rather than
always, because different thread stacks are not protected from other threads. So
if a thread somehow manages to acquire a pointer to another thread’s stack, then
it can read and write any part of that stack. Our example program shows this in
line 26, where the peer threads reference the contents of the main thread’s stack
indirectly through the global ptr variable.
respective
美[rɪˈspektɪv]
adj. 各自的,分別的
acquire
美[əˈkwaɪər]
v. 獲得,得到
12.4.2 Mapping Variables to Memory
1. Global variables. A global variable is any variable declared outside of a function.
2. Local automatic variables. A local automatic variable is one that is declared
inside a function without the static attribute.
3. Local static variables. A local static variable is one that is declared inside a function with the static attribute.
12.4.3 Shared Variables
We say that a variable v is shared if and only if one of its instances is referenced
by more than one thread. For example, variable cnt in our example program is
shared because it has only one run-time instance and this instance is referenced by
both peer threads. On the other hand, myid is not shared, because each of its two
instances is referenced by exactly one thread. However, it is important to realize
that local automatic variables such as msgs can also be shared.
reference
美[ˈrefrəns]
v. 引用
本作品採用《CC 協議》,轉載必須註明作者和本文連結