所有的Unix类系统都是多任务操作系统。unix允许一个用户同时运行多个进程,这是我们今天使用的最强大、最灵活的多任务编程模型之一。
在传统的unix模式,进程都是通过fork()系统调用创建的。
Fork()产生一个当前进程的拷贝。在子进程中fork()返回1,在父进程中fork返回子进程的pid。fork()通常使用方法:
[do parent stuff]
ppid = fork ();
if (ppid < 0) { fork_error_function (); } else if (ppid == 1) { child_function (); } else { parent_function (); }
注意fork在两个完全独立的进程返回。每个进程都有自己的地址空间,有它们自己的变量(除了一些特别的SysV IPC变量共用,但这是很特殊的情况)。
这种独立性,提供了内存保护也更稳定,但是这导致的问题是,当你想让多个进程处理同一个任务/问题的时候。是的,你可以使用pipes或者SysV IPC在进程间通信,但是还是有一些其它的问题。
由于这些原因,或者其它某些原因,线程(轻量的进程)就非常有用了。线程之间分享相同的地址空间,它们是在进程内部调度,因此避免了进程间的低效问题。
一个非常受欢迎的创建多线程的API是pthreads
,又以POSIX threads著称P1003.1c, or ISO/IEC 9945-1:1990c.这个API就是今天教程的主题。
在许多情况下多线程可以非常简单的写出优雅并且高效的代码。下面的这些种类的问题非常适合使用多线程。
阻塞IO:一个程序要处理一系列的IO操作有下面三种选择。串行的处理IO请求,一个完成了才能进行下一个。也可以使用异步IO,处理所有复杂的异步信号,polling或者selects。或者使用同步IO,每一个IO请求都单独创建一个线程来处理。在这种情况下,多线程可以明显的提高性能降低代码复杂度。
多处理器:如果你使用的线程库支持多处理器,你可以通过在每一个处理器上运行线程来显著的提高性能。这在你的程序需要大量计算的时候非常有用。
用户界面:将耗时的操作放在线程中来执行,这样UI界面可以继续和用户交互。
服务:服务于多个客户端的服务器可以通过适当使用并发作出反应更灵敏。这在传统上一直通过使用fork()系统调用来实现。然而,在某些情况下,尤其是对于大型高速缓存打交道时,线程可以帮助提高内存的利用率。甚至代替fork()处理并发操作,在某些fork()不适用的情况下。
但是这里也有一些问题,因为多个线程分享相同的地址空间。最大的问题就是数据竞争。考虑一下下面这个代码:
THREAD 1 THREAD 2
a = data; b = data;
a++; b--;
data = a; data = b;
如果代码是串行的执行(先执行THREAD 1,然后执行THREAD 2),那么这里没有问题。但是线程执行的顺序是完全随机的,所以看一下下面这种情况:
THREAD 1 THREAD 2
a = data;
b = data;
a++;
b--;
data = a;
data = b;
[data = data - 1!!!!!!!]
数据最后可能是+1,0,-1,这里没有办法知道是哪一个结果,因为这是完全不确定的。
解决办法是提供一种功能,当数据正在被一个线程访问时,其它线程则阻塞。pthreads使用mutex来实现这个功能。
使用pthread_create()
创建线程。
#include <pthread.h>
int
pthread_create (pthread_t *thread_id, const pthread_attr_t *attributes,
void *(*thread_function)(void *), void *arguments);
这个函数创建一个新的线程。pthread_t
用来标记这个新的线程。attributes
用来定义线程的一些属性,如果传NULL则表示使用默认的属性。thread_function
代表这个线程要执行的函数,如果这个函数结束了,那么线程也结束了。arguments
是传递给线程函数thread_function
的参数。
线程在thread_function
函数执行完毕时推出,或者明确的调用pthread_exit()
来退出线程。
int pthread_exit (void *status);
status
是线程的返回值。(注意线程函数返回void *
),所以调用return (void *)和这个函数功能一样。
一个线程可以使用pthread_join()
等待另一个线程退出
int pthread_join (pthread_t thread, void **status_ptr);
线程退出的状态会放在status_ptr
中。
一个线程可以调用pthread_self
获取自己的线程id。
pthread_t pthread_self ();
可以使用pthread_equal()
来比较线程id是否相同。
int pthread_equal(pthread_t t1, pthread_t t2);
返回0表示是不同的线程,其他情况返回非0;
锁有两个基本操作,lock和unlock。如果锁一个锁是unlock的此时线程调用lock,那么这个锁被锁住线程继续运行。如果这个锁是locked,那么这个线程将会阻塞直到拥有这个锁的人释放锁。
这里有5个基本的处理锁的方法。
初始化一个锁,第一个参数锁的指针,第二个参数传NULL使用默认的属性。
int pthread_mutex_init (pthread_mutex_t *mut, const pthread_mutexattr_t *attr);
对mutex加锁:
int pthread_mutex_lock (pthread_mutex_t *mut);
对mutex解锁:
int pthread_mutex_unlock (pthread_mutex_t *mut);
尝试加锁,尝试获得一个锁,如果成功则加锁,否则返回EBUSY。
int pthread_mutex_trylock (pthread_mutex_t *mut);
销毁一个锁,释放锁的内存和跟锁相关的一些其它资源。
int pthread_mutex_destroy (pthread_mutex_t *mut);
一个简单的例子:
考虑先前那个例子。
THREAD 1 THREAD 2
pthread_mutex_lock (&mut);
pthread_mutex_lock (&mut);
a = data; /* blocked */
a++; /* blocked */
data = a; /* blocked */
pthread_mutex_unlock (&mut); /* blocked */
b = data;
b--;
data = b;
pthread_mutex_unlock (&mut);
[数据的竞争被消除了]
锁可以让你避免数据竞争,在你操作共享数据的时候保护你,但是它不能让你在某个数据上等待。
条件变量可以解决这个问题。
你可以在条件变量上执行下面6中操作:
初始化,attr
传空使用默认的属性。
int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
等待某个条件成立,这个函数总是阻塞的。
int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
该函数内部的伪代码如下:
pthread_cond_wait (cond, mut)
begin
pthread_mutex_unlock (mut);
block_on_cond (cond);
pthread_mutex_lock (mut);
end
注意该函数在阻塞钱会是否锁,而在返回时会去获得锁。在重新获取锁时会被会有一段时间,所以这个函数返回时,需要重新检查条件变量的值是否成立。
发送一个信号,唤醒某个在这个条件变量上等待的线程。
int pthread_cond_signal (pthread_cond_t *cond);
广播唤醒,唤醒所有在等待这个条件的线程:
int pthread_cond_broadcast (pthread_cond_t *cond);
带超时的等待某个条件:
int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
超时时间是绝对时间,如果时间带了,这个条件还没有等到,将返回ETIMEDOUT
struct timespec to {
time_t tv_sec;
long tv_nsec;
};
销毁,销毁这个条件变量
int pthread_cond_destroy (pthread_cond_t *cond);