1. 基于进程的并发编程
使用fork
、exec
、waitpid
等函数派生子进程处理不同任务。对于在父、子进程间的共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间。
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);
终止线程
一个线程是以下列方式之一来终止的:
- 当顶层线程例程返回时,线程会隐式地终止。
-
通过调用pthread_exit函数,线程会显式地终止。如果主线程调用pthread_exit,它会等待所有其他对等线程终止,然后在终止主线程和整个进程。
#include <pthread.h> void pthread_exit(void *thread_return);
- 某个对等线程调用Unix的exit函数,该函数终止进程以及所有与该进程相关的线程。
-
另一个对等线程通过已当前线程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 步:
- load:加载共享的虚拟地址空间中的值加载到线程独有的栈或者寄存器中
- update:线程在自己独有的栈或者寄存器中更新上一步加载的值
- store:将更新后的值重新存储回共享的虚拟地址空间
如果多个线程的 1, 2, 3 步交错执行,共享的值可能不会得到预期的正确结果。
信号量 s
是具有非负整数值的全局变量,只能由 2 中特殊的操作来处理,这 2 种操作称为 P
和 V
:
P(s)
:如果s
是非 0 的,那么P
将s
减 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) */
- older
- Newer