很多时候,我们做项目并不会创建那么多进程,而是创建一个进程,在该进程中创建多个线程进行工作。
一、进程与线程
1、什么是进程、线程,有什么区别?
进程是资源(CPU、内存等)分配的基本单位,线程是 CPU 调度和分配的基本单位(程序执行的最小单位)。如果 CPU 是单核,同一时间只有一个进程在执行,多核 CPU 可以同一时间点有多个进程在执行。
2、多进程、多线程的优缺点
一个进程由进程控制块、数据段、代码段组成,进程本身不可以运行程序,而是像一个容器一样,先创建出一个主线程,分配给主线程一定的系统资源,这时候就可以在主线程开始实现各种功能。
当我们需要实现更复杂的功能时,可以在主线程里创建多个子线程,多个线程在同一个进程里,利用这个进程所拥有的系统资源合作完成某些功能。
优缺点:
- 一个进程死了不影响其他进程,一个线程崩溃很可能影响到它本身所处的整个进程。
- 创建多进程的系统花销大于创建多线程。
- 多进程通讯因为需要跨越进程边界,不适合大量数据的传送。多线程无需跨越进程边界,适合大量数据的传送。
3、什么时候用进程,什么时候用线程
- 创建和销毁较频繁使用线程,因为创建进程花销大。
- 需要大量数据传送使用线程,因为多线程切换速度快,不需要跨越进程边界。
- 安全稳定选进程;快速频繁选线程;
二、线程间通信/同步
上一篇文章我们讲了进程间通信的六种方式:管道和 FIFO、信号、消息队列、信号量、共享内存、套接字(Socket),今天我们讲一下线程间通信/同步的方式。
线程同步的方法:互斥锁、条件变量、自旋锁、读写锁,除此之外,还有信号量、屏障等等,在 Linux 应用开发当中,用的最多的还是互斥锁和条件变量。
为什么需要线程同步?
线程同步是在多线程环境下可能需要注意的一个问题。线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享,不过这种便捷的共享是有代价的,那就是多个线程并发访问共享数据所导致的数据不一致的问题。
1、互斥锁
互斥锁(mutex),在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁。如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行加锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。
初始化互斥锁
代码语言:javascript复制#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
加锁、解锁
代码语言:javascript复制#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
当互斥锁已经被其它线程锁住时,调用 pthread_mutex_lock()函数会被阻塞,直到互斥锁解锁;如果线程不希望被阻塞,可以使用 pthread_mutex_trylock()函数;调用 pthread_mutex_trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么调用 pthread_mutex_trylock()将会锁住互斥锁并立马返回,如果互斥锁已经被其它线程锁住,调用 pthread_mutex_trylock()加锁失败,但不会阻塞,而是返回错误码 EBUSY。
代码语言:javascript复制#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
销毁互斥锁(不再需要互斥锁时,应该将其销毁)
代码语言:javascript复制#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥锁死锁
如果一个线程试图对同一个互斥锁加锁两次,该线程会陷入死锁状态,一直被阻塞永远出不来;这就是出现死锁的一种情况。
有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又由不同的互斥锁管理。当超过一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁;譬如,程序中使用一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,并且在试图锁住第二个互斥锁时处于阻塞状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞,于是就产生了死锁。
2、条件变量
条件变量用于自动阻塞线程,直到某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。使用条件变量主要包括两个动作:
- 一个线程等待某个条件满足而被阻塞;
- 另一个线程中,条件满足时发出“信号”。
条件变量通常搭配互斥锁来使用,是因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须首先锁住互斥锁,不然就可能引发线程不安全的问题。
初始化和销毁条件变量
代码语言:javascript复制#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);
条件变量的主要操作是发送信号(signal)和等待。发送信号操作即是通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。
函数 pthread_cond_signal()和 pthread_cond_broadcast()均可向指定的条件变量发送信号,通知一个或多个处于等待状态的线程。调用 pthread_cond_wait()函数是线程阻塞,直到收到条件变量的通知。
通知条件变量
代码语言:javascript复制#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_signal()函数至少能唤醒一个线程,而 pthread_cond_broadcast()函数则能唤醒所有线程。
等待条件变量
代码语言:javascript复制#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
3、自旋锁
自旋锁与互斥锁很相似,在访问共享资源之前对自旋锁进行上锁,在访问完成后释放自旋锁(解锁);事实上,从实现方式上来说,互斥锁是基于自旋锁来实现的,所以自旋锁相较于互斥锁更加底层。
自旋锁与互斥锁之间的区别:
- 实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
- 开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,故自旋锁不适用于等待时间比较长的情况。
- 使用场景的区别:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!
初始化和销毁自旋锁
代码语言:javascript复制#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
加锁和解锁
代码语言:javascript复制#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
4、读写锁
互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。
读写锁有3 种状态:读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和不加锁状态(见),一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知,读写锁比互斥锁具有更高的并行性!
读写锁有如下两个规则:
- 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
- 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
读写锁非常适合于对共享数据读的次数远大于写的次数的情况。
初始化和销毁读写锁
代码语言:javascript复制#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁和解锁
代码语言:javascript复制#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);