线程间同步
- 锁机制:互斥锁、条件变量、信号量、读写锁
- 互斥锁:提供了以排他方式数据结构被并发修改的方法
- 读写锁:写锁优先抢占资源,读锁允许多个线程共同读共享数据,而写锁操作是互斥的
- 条件变量:以原子方式阻塞进程,直到某个特定条件为真为止
一般情况下:互斥锁起保护作用,条件变量和互斥锁一起使用
互斥锁(mutex)
锁机制是同一时刻只允许一个线程执行一个关键部分的代码。
C++
// 初始化锁
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
其中参数 mutexattr 用于指定锁的属性(见下),如果为NULL则使用缺省属性。
互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前有四个值可供选择:
-
(1)PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
-
(2)PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
-
(3)PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
-
(4)PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
// 阻塞加锁
int pthread_mutex_lock(pthread_mutex *mutex);
// 非阻塞加锁
// 该函数语义与 pthread_mutex_lock() 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待。
int pthread_mutex_trylock( pthread_mutex_t *mutex);
// 销毁锁
int pthread_mutex_destroy(pthread_mutex *mutex);
互斥量的死锁:
一个线程需要访问两个或者更多不同的共享资源,而每个资源又有不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就可能发生死锁。
死锁就是指多个线程/进程因竞争资源而造成的一种僵局(相互等待),若无外力作用,这些进程都将无法向前推进。
死锁的处理策略:
- 预防死锁:破坏死锁产生的四个条件:互斥条件、不剥夺条件、请求和保持条件以及循环等待条件。
- 避免死锁:在每次进行资源分配前,应该计算此次分配资源的安全性,如果此次资源分配不会导致系统进入不安全状态,那么将资源分配给进程,否则等待。算法:银行家算法。
- 检测死锁:检测到死锁后通过资源剥夺、撤销进程、进程回退等方法解除死锁。
读写锁
读写锁与互斥量类似,不过读写锁拥有更高的并行性。互斥量要么是锁住状态,要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁有3种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
当读写锁是写加锁状态时,在这个锁被解锁之前,所有视图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止。
C++
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
/* 初始化读写锁 */
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;/* 全局资源 */
int global_num = 10;
void err_exit(const char *err_msg)
{
printf("error:%s\n", err_msg);
exit(1);
}
/* 读锁线程函数 */
void *thread_read_lock(void *arg)
{
char *pthr_name = (char *)arg;
while (1)
{
/* 读加锁 */
pthread_rwlock_rdlock(&rwlock);
printf("线程%s进入临界区,global_num = %d\n", pthr_name, global_num);
sleep(1);
printf("线程%s离开临界区...\n", pthr_name);
/* 读解锁 */
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
return NULL;
}
/* 写锁线程函数 */
void *thread_write_lock(void *arg)
{
char *pthr_name = (char *)arg;
while (1)
{
/* 写加锁 */
pthread_rwlock_wrlock(&rwlock);
/* 写操作 */
global_num++;
printf("线程%s进入临界区,global_num = %d\n", pthr_name, global_num);
sleep(1);
printf("线程%s离开临界区...\n", pthr_name);
/* 写解锁 */
pthread_rwlock_unlock(&rwlock);
sleep(2);
}
return NULL;
}
int main(void)
{
pthread_t tid_read_1, tid_read_2, tid_write_1, tid_write_2;
/* 创建4个线程,2个读,2个写 */
if (pthread_create(&tid_read_1, NULL, thread_read_lock, "read_1") != 0)
err_exit("create tid_read_1");
if (pthread_create(&tid_read_2, NULL, thread_read_lock, "read_2") != 0)
err_exit("create tid_read_2");
if (pthread_create(&tid_write_1, NULL, thread_write_lock, "write_1") != 0)
err_exit("create tid_write_1");
if (pthread_create(&tid_write_2, NULL, thread_write_lock, "write_2") != 0)
err_exit("create tid_write_2");
/* 随便等待一个线程,防止main结束 */
if (pthread_join(tid_read_1, NULL) != 0)
err_exit("pthread_join()");
return 0;
}
条件变量(cond)
条件变量是利用线程间共享全局变量进行同步的一种机制。
条件变量上的基本操作有:触发条件(当条件变为 true 时);等待条件,挂起线程直到其他线程触发条件。
条件变量是线程可用的另一种同步机制。互斥量用于上锁,条件变量则用于等待,并且条件变量总是需要与互斥量一起使用,运行线程以无竞争的方式等待特定的条件发生。
条件变量本身是由互斥量保护的,线程在改变条件变量之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种变化,因为互斥量必须在锁定之后才能计算条件。
C++
// 初始话条件变量
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
// 无条件等待
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
// 计时等待
// 如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求(用 pthread_cond_wait() 或 pthread_cond_timedwait() 请求)竞争条件(Race Condition)。
mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。
C++
// 激活一个等待该条件的线程(存在多个等待线程时按入队顺序激活其中一个)
int pthread_cond_signal(pthread_cond_t *cond);
// 激活所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 销毁条件变量
// 只有在没有线程在该条件变量上等待的时候才能销毁这个条件变量,否则返回EBUSY
int pthread_cond_destroy(pthread_cond_t *cond);
信号量
如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。
线程的信号和进程的信号量类似,使用线程的信号量可以高效地完成基于线程的资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量就增加;公共资源减少的时候,信号量就减少;只有当信号量的值大于0的时候,才能访问信号量所代表的公共资源。
线程使用的基本信号量函数有四个:
C++
// 初始化信号量
/*//
sem - 指定要初始化的信号量;
pshared - 信号量 sem 的共享选项,linux只支持0,表示它是当前进程的局部信号量;
value - 信号量 sem 的初始值。
*/
int sem_init (sem_t *sem , int pshared, unsigned int value);
// 信号量值加1
int sem_post(sem_t *sem);
// 信号量值减1
// 如果sem所指的信号量的数值为0,函数将会等待直到有其它线程使它不再是0为止。
int sem_wait(sem_t *sem);
// 销毁信号量
int sem_destroy(sem_t *sem);
示例代码
// C++
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <errno.h>
#define return_if_fail(p) if((p) == 0){printf ("[%s]:func error!\n", __func__);return;}
typedef struct _PrivInfo
{
sem_t s1;
sem_t s2;
time_t end_time;
}PrivInfo;
static void info_init (PrivInfo* prifo);
static void info_destroy (PrivInfo* prifo);
static void* pthread_func_1 (PrivInfo* prifo);
static void* pthread_func_2 (PrivInfo* prifo);
int main (int argc, char** argv)
{
pthread_t pt_1 = 0;
pthread_t pt_2 = 0;
int ret = 0;
PrivInfo* prifo = NULL;
prifo = (PrivInfo* )malloc (sizeof (PrivInfo));
if (prifo == NULL) {
printf ("[%s]: Failed to malloc priv.\n");
return -1;
}
info_init (prifo);
ret = pthread_create (&pt_1, NULL, (void*)pthread_func_1, prifo);
if (ret != 0) {
perror ("pthread_1_create:");
}
ret = pthread_create (&pt_2, NULL, (void*)pthread_func_2, prifo);
if (ret != 0) {
perror ("pthread_2_create:");
}
pthread_join (pt_1, NULL);
pthread_join (pt_2, NULL);
info_destroy (prifo);
return 0;
}
static void info_init (PrivInfo* prifo)
{
return_if_fail (prifo != NULL);
prifo->end_time = time(NULL) + 10;
sem_init (&prifo->s1, 0, 1);
sem_init (&prifo->s2, 0, 0);
return;
}
static void info_destroy (PrivInfo* prifo)
{
return_if_fail (prifo != NULL);
sem_destroy (&prifo->s1);
sem_destroy (&prifo->s2);
free (prifo);
prifo = NULL;
return;
}
static void* pthread_func_1 (PrivInfo* prifo)
{
return_if_fail (prifo != NULL);
while (time(NULL) < prifo->end_time)
{
sem_wait (&prifo->s2);
printf ("pthread1: pthread1 get the lock.\n");
sem_post (&prifo->s1);
printf ("pthread1: pthread1 unlock\n");
sleep (1);
}
return;
}
static void* pthread_func_2 (PrivInfo* prifo)
{
return_if_fail (prifo != NULL);
while (time (NULL) < prifo->end_time)
{
sem_wait (&prifo->s1);
printf ("pthread2: pthread2 get the unlock.\n");
sem_post (&prifo->s2);
printf ("pthread2: pthread2 unlock.\n");
sleep (1);
}
return;
}
互斥与同步的区别
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:主要是流程上的概念,是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
互斥锁、条件变量和信号量的区别
互斥锁:互斥,一个线程占用了某个资源,那么其它的线程就无法访问,直到这个线程解锁,其它线程才可以访问。
条件变量:同步,一个线程完成了某一个动作就通过条件变量发送信号告诉别的线程,别的线程再进行某些动作。条件变量必须和互斥锁配合使用。
信号量:同步,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。而且信号量有一个更加强大的功能,信号量可以用作为资源计数器,把信号量的值初始化为某个资源当前可用的数量,使用一个之后递减,归还一个之后递增。
另外还有以下几点需要注意:
-
信号量可以模拟条件变量,因为条件变量和互斥量配合使用,相当于信号量模拟条件变量和互斥量的组合。在生产者消费者线程池中,生产者生产数据后就会发送一个信号 pthread_cond_signal通知消费者线程,消费者线程通过pthread_cond_wait等待到了信号就可以继续执行。这是用条件变量和互斥锁实现生产者消费者线程的同步,用信号量一样可以实现!
-
信号量可以模拟互斥量,因为互斥量只能为加锁或解锁(0 or 1),信号量值可以为非负整数,也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量时,就完成一个资源的互斥访问。前面说了,信号量主要用做多线程多任务之间的同步,而同步能够控制线程访问的流程,当信号量为单值时,必须有线程释放,其他线程才能获得,同一个时刻只有一个线程在运行(注意,这个运行不一定是访问资源,可能是计算)。如果线程是在访问资源,就相当于实现了对这个资源的互斥访问。
-
互斥锁是为上锁而优化的;条件变量是为等待而优化的; 信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性。
-
互斥锁,条件变量都只用于同一个进程的各线程间,而信号量(有名信号量)可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。
-
互斥量必须由同一线程获取以及释放,信号量和条件变量则可以由一个线程释放,另一个线程得到。
-
信号量的递增和减少会被系统自动记住,系统内部的计数器实现信号量,不必担心丢失,而唤醒一个条件变量时,如果没有相应的线程在等待该条件变量,此次唤醒会被丢失。
多线程和多进程比较
多进程
- 优点:
-
进程内存空间独立;
-
操作系统在进程间提供的附加保护和更高级别的通信机制,更安全;
-
多进程,可基于网络实现多机独立进程
- 缺点:
-
每个进程内存空间独立,固有开销大,管理复杂;
-
进程间通信设置复杂;
多线程
- 优点:
- 所有线程共享相同的地址空间,系统开销小;
- 缺点:
- 共享内存空间带来的缺点就是安全性降低;
死锁
死锁指的是由于两个或多个执行单元之间相互等待对方结束而引起阻塞的情况。例如:
一个线程T1获得了对资源R1的访问权。
一个线程T2获得了对资源R2的访问权。
T1请求对R2的访问权但是由于此权力被T2所占而不得不等待。
T2请求对R1的访问权但是由于此权力被T1所占而不得不等待。
T1和T2将永远维持等待状态,此时我们陷入了死锁的处境!这种问题比你所遇到的大多数的bug都要隐秘,针对此问题主要有三种解决方案:
- 在同一时刻不允许一个线程访问多个资源。
- 为资源访问权的获取定义一个关系顺序。换句话说,当一个线程已经获得了R1的访问权后,将无法获得R2的访问权。当然,访问权的释放必须遵循相反的顺序。
- 为所有访问资源的请求系统地定义一个最大等待时间(超时时间),并妥善处理请求失败的情况。几乎所有的.NET的同步机制都提供了这个功能。
声明
本文是个人学习和总结的笔记和感想,内容涉及网络资料、相关书籍摘录、个人总结和感悟。在这之中也必有疏漏未加标注者,如有侵权请联系删除。