1. 基于进程的并发编程

使用forkexecwaitpid等函数派生子进程处理不同任务。对于在父、子进程间的共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间。

2. I/O多路复用

可以通过select函数,要求内核挂起进程,只有在一个或多个I/O事件发生或经历一段指定的时间后,才将控制返回给该进程。

#include <sys/select.h>
#include <sys/time.h>

// 返回:若有就绪描述符则为其数目,若超时则为0;若出错则为-1
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,
           const struct timeval *timeout);

void FD_ZERO(fd_set *fdset);           /* Clear all bits in fdset */
void FD_SET(int fd, fd_set *fdset);    /* Turn on the bit for fd in fdset */
void FD_CLR(int fd, fd_set *fdset);    /* Turn off the bit for fd in fdset */
int  FD_ISSET(int fd, fd_set *fdset);  /* Is the bit for fd on in fdset? */

3. 基于线程的并发编程

线程(thread)就是运行在进程上下文中的逻辑流,线程由内核自动调度。一组并发线程运行在一个进程的上下文中。每个线程都有它自己独立的线程上下文(thread context),包括一个唯一的整数线程ID(Thread ID, TID)、栈、栈指针、程序计数器,通用目的寄存器和条件码。所有运行在一个进程里的线程共享该进程的整个虚拟地址空间,它是由只读文本(代码),读/写数据,堆以及所有的共享代码和数据区域组成的。线程也共享相同的打开文件的集合。

线程执行模型

每个进程开始生命周期都是单元线程,这个线程称为主线程(main thread),在某一时刻,主线程创建一个对等线程(peer thread),从这个时间点开始,两个线程就并发地运行。最后,因为主线程执行一个慢速系统调用,例如read或sleep,或者因为它被系统的间隔计时器中断,控制就会通过上下文切换传递到对等线程。对等线程会执行一段时间,然后传递会主线程,依次类推。

和一个进程相关的线程组成一个对等(线程)池(pool),独立于其他(进程/线程)创建的线程。主线程和其他线程的区别仅仅在于它总是进程中第一个运行的线程。对等(线程)池概念的主要影响是,一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止。另外,每个对等线程都能读写相同的共享数据。

Posix线程

Posix线程(Pthreads)是C程序中处理线程的一个标准接口。Pthreads定义了大约60个函数,允许程序创建、杀死和回收线程,与对等线程安全地共享数据,还可以通知对等线程系统状态的变化。

创建线程

线程通过调用pthread_create函数来创建其他线程:

#include <pthread.h>
typedef void *(func)(void *);

// 成功返回0,出错返回非零
int pthread_create(pthread_t *tid, pthread_attr_t *attr, func *f, void *arg);

pthread_create创建的新的线程运行线程例程f,带有输入参数arg。可以使用attr改变新创建线程的默认属性。当pthread_create返回时,参数tid包含新创建线程的ID。

新线程可以通过调用pthread_self函数来获得它自己的线程ID。

#include <pthread.h>

pthread_t pthread_self(void);
终止线程

一个线程是以下列方式之一来终止的:

  1. 当顶层线程例程返回时,线程会隐式地终止。
  2. 通过调用pthread_exit函数,线程会显式地终止。如果主线程调用pthread_exit,它会等待所有其他对等线程终止,然后在终止主线程和整个进程。

     #include <pthread.h>
    
     void pthread_exit(void *thread_return);
    
  3. 某个对等线程调用Unix的exit函数,该函数终止进程以及所有与该进程相关的线程。
  4. 另一个对等线程通过已当前线程ID作为参数调用pthread_cancle函数来终止当前线程。

     #include <pthread.h>
    
     // 若成功则返回0,若出错则为非零
     int pthread_cancle(pthread_t tid);
    
回收已终止线程的资源

线程通过调用pthread_join函数等待其他线程终止。

#include <pthread.h>

// 若成功则返回0,若出错则为非零
int pthread_join(pthread_t tid, void **thread_return);

pthread_join函数会阻塞,直到线程tid终止,将线程例程返回的(void*)指针赋值为thread_return指向的位置,然后回收已终止线程占用的所有存储器资源。

分离线程

在任何一个时间点上,线程是可结合的(joinable)或者是分离的(detached)。一个可结合的线程能够被其他线程回收其资源和杀死。在被其他线程回收之前,它的存储及资源(例如栈)是没有被释放的。相反,一个分离的线程是不能被其他线程回收或杀死的。它的存储及资源在它终止时由系统自动释放。

默认情况下,线程被创建为可结合的。为了避免存储器泄漏,每个可结合线程都应该要么被其他线程显示地回收,要么通过调用pthread_detach函数被分离。

pthread_detach函数分离可结合线程tid。线程能够通过以pthread_self()为参数的pthread_detach分离它们自己。

#include <pthread.h>

// 若成功则返回0,若出错则为非零
int pthread_detach(pthread_t tid);
初始化线程
#include <pthread.h>

pthread_once_t once_control = PTHREAD_ONCE_INIT;

// 总是返回0
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

信号量(semaphore)

同一个进程下的一组线程共享该进程的虚拟地址空间,但是每个线程有自己独立的栈和通用目的寄存器,当线程修改共享虚拟地址空间中的值时,一般会隐含 3 步:

  1. load:加载共享的虚拟地址空间中的值加载到线程独有的栈或者寄存器中
  2. update:线程在自己独有的栈或者寄存器中更新上一步加载的值
  3. store:将更新后的值重新存储回共享的虚拟地址空间

如果多个线程的 1, 2, 3 步交错执行,共享的值可能不会得到预期的正确结果。

信号量 s 是具有非负整数值的全局变量,只能由 2 中特殊的操作来处理,这 2 种操作称为 PV

  • P(s):如果 s 是非 0 的,那么 Ps 减 1,并且立即返回。如果 s 为 0,那么就挂起这个线程,直到 s 变为非 0(等待 V 操作增加 s 的值,重启这个线程)。在重启之后,P 操作将 s 减 1,并将控制返回给调用者。
  • V(s)
#include <semaphore.h>

int sem_init(sem_t *sem, 0, unsigned int value);
int sem_wait(sem_t *s);  /* P(s) */
int sem_post(sem_t *s);  /* V(s) */