并发编程
并行计算
并行计算是通过尝试使用多个执行并行算法的处理器来更快解决问题的一种计算方案。
并行性与并发性
在理想情况下,并行算法中的所有任务都应该同时实时执行。然而,真正的并行执行只能在有多个处理组件的系统中实现,比如多处理器或多核系统。
在单cpu系统中,一次只能执行一个任务。在这种情况下,不同的任务只能并发执行,即在逻辑上并发执行。在单cpu系统中,并发性是通过多任务处理来实现的。
线程
一个操作系统包含许多并发进程。在进程模型中,进程是独立的执行单元,所有进程均在内核模式或用户模式下执行。在内核模式下,各进程在唯一地址空间上执行,与其他进程是分开的。
线程是某进程同一地址空间上的独立执行单元,创建某个进程就是在一个唯一地址空间创建一个主线程,当某进程开始时,就会执行该进程的主线程。
线程的优点
- 线程的创建和切换速度更快
- 线程的响应速度更快
- 线程更适合并行计算
线程的缺点
- 由于地址空间共享,线程需要来自用户的明确同步
- 许多库函数可能对线程不安全
- 在单cpu系统上,使用线程解决问题实际上比使用顺序程序慢,这是由在运行时创建线程和切换上下文的系统开销造成的
线程操作
线程可在用户模式或内核模式下执行。在用户模式下,线程在进程的相同地址空间中执行,但每个线程都有自己的执行堆栈。线程是独立的执行单元,可根据操作系统内核的调度策略,对内核进行系统调用,变为挂起、激活以继续执行等。为了利用线程的共享地址空间,操作系统内核的调度策略可能会优先选择同一进程中的线程,而不是不同线程中的进程。
线程管理函数Pthread库
- 线程创建:使用pthread_create()函数创建线程
- 线程ID:使用pthread_equal()函数对线程ID这一不透明的数据类型进行比较
- 线程结束:线程函数结束后线程终止,或调用函数int pthread_exit(void *status)
- 线程链接:一个线程可以等待另一个线程的终止,通过int pthread_join(pthread_t pthread, void **status_ptr)
线程同步
互斥量
通过使用pthread的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。
在设计时需要规定所有的线程必须遵守相同的数据访问规则,只有这样,互斥机制才能正常工作。操作系统并不会做数据访问的串行化。如果允许其中的的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其他的线程在使用共享资源前都获取了锁,也还是会出现数据不一致的问题。
死锁预防
如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态,使用互斥量时,还有其他更不明显的方式也能产生死锁。例如,程序中使用多个互斥量时,如果允许一个线程一直占有第一个互斥量,并且在试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量,这时就会发生死锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生死锁。
可以通过小心地控制互斥量加锁的顺序来避免死锁的发生。例如,假设需要对两个互斥量A和B同时加锁,如果所有线程总是在对互斥量B加锁之前锁住互斥量A,那么使用这两个互斥量不会产生死锁(当然在其他资源上仍可能出现死锁);类似地,如果所有的线程总是在锁住互斥量A之前锁住互斥量B,那么也不会发生死锁。只有在一个线程试图以与另一个线程相反的顺序锁住互斥量时,才可能出现死锁。
有时候应用程序的结果使得对互斥量加锁进行排序是很困难的,如果涉及了太多的锁和数据结构,可用的函数并不能把它转换成简单的层次,那么就需要采用另外的方法。可以先释放占有的锁,然后过一段时间再试。这种情况可以使用pthread_mutex_trylock接口避免死锁。如果已经占有某些锁而且pthread_mutex_trylock接口返回成功,那么就可以前进;但是,如果不能获取锁,可以先释放已经占有的锁,做好清理工作,然后过一段时间重新尝试。
条件变量
条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
与互斥量一样,条件变量也可以通过静态方法或动态方法进行初始化。
生产者-消费者问题
生产者消费者是非常经典的同步模型,在编程中也有非常多的应用。生产者消费者问题描述如下:一个或者多个生产者往消息队列里面插入数据;单个消费者从消息队列里面取出数据处理;并且对消息队列的操作必须是互斥的。分析问题可知需要解决三个同步问题:
- 对消息队列的操作必须是互斥的,需要加锁(如果是lockfree的数据结构,就不用加锁,如boost::lockfree::queue)
- 消息队列中没有数据时,消费者需要等待生产者产生数据,这就是条件同步
- 消息队列满时,生产者需要等到消费者消费数据,这也是条件同步