如果每个线程使用的变量都是其它线程不会读取或修改的,那么就不存在一致性问题。同样,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。否则,将会出现不一致性问题。为了解决数据不一致问题,必须引入某些机制使线程间同步。
当变量修改时间多于一个存储器访问周期,同时读、写操作又相互交替时,潜在的不一致性就会出现。如下图所示:
此时,线程B读取到的数据是错误的。使用一把锁能够解决上述问题:
下面介绍线程的三种同步机制:
1、互斥量pthread_mutex_t
互斥量就是锁,对某段临界区进行加锁,使得线程只能串行访问这段临界区。对互斥量进行加锁后,任何其它试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥量。
2、读写锁pthread_rwlock_t
与互斥量类似,但并行性更高。读写锁有三种状态:
- 读模式加锁状态
- 写模式加锁状态
- 不加锁状态
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。下面是读写锁的逻辑关系表:
锁状态\需要加的锁 | 读模式加锁 | 写模式加锁 |
读模式 | 允许 | 阻塞直到所有线程释放读锁 |
写模式 | 阻塞直到写锁被释放 | 阻塞直到写锁被释放 |
不加锁 | 允许 | 允许 |
3、条件变量pthread_cond_t
条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定条件的发生。条件变量(由用户*定义)本身受互斥量保护,对条件的修改都必须先加锁。
这里要注意条件和条件变量之间的区别。条件变量是由系统提供的结构体pthread_cond_t,它用来在多个线程间传递信号;而条件则由用户代码定义,用于多个线程间共享某些数据。
测试代码:
#include <stdio.h> #include <pthread.h> /* 自定义的条件 */ struct msg { struct msg *m_next; char *data; }; struct msg *workq; /* 定义一个消息队列 */ pthread_cond_t qready = PTHREAD_COND_INITIALIZER; /* 静态分配条件变量 */ pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER; /* 静态分配互斥量 */ void process_msg(void) { struct msg *mp; while (1) { pthread_mutex_lock(&qlock); while (workq == NULL) { /* 此函数把线程放到等待条件的线程列表上,然后对互斥量解锁 * 函数返回时,互斥量再次被锁住 */ pthread_cond_wait(&qready, &qlock); } /* 从消息队列头取出一个消息 */ mp = workq; workq = workq->m_next; pthread_mutex_unlock(&qlock); printf("%s\n", mp->data); /* 打印收到的消息 */ } } void enqueue_msg(struct msg *mp) { pthread_mutex_lock(&qlock); mp->m_next = workq; /* 新消息放队列头部 */ workq = mp; pthread_mutex_unlock(&qlock); pthread_cond_signal(&qready); /* 唤醒等待该条件的某个线程 */ } void *thr_fn1(void *arg) { process_msg(); } void *thr_fn2(void *arg) { struct msg msg1, msg2, msg3; msg1.data = "Hello world"; msg2.data = "I love you"; msg3.data = "This is a test!"; /* 将消息放入队列 */ enqueue_msg(&msg1); enqueue_msg(&msg2); enqueue_msg(&msg3); } int main(void) { pthread_t tid1, tid2; pthread_create(&tid1, NULL, thr_fn1, NULL); pthread_create(&tid2, NULL, thr_fn2, NULL); pthread_join(tid1, NULL); return 0; }
运行结果:
可以看到两个线程成功进行了数据传递。
这里要注意pthread_cond_wait函数的用法。
函数原型:pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
函数传入的参数mutex用于保护条件,因为我们在调用pthread_cond_wait时,如果条件不成立就进入阻塞,但是进入阻塞这个期间,如果条件变量改变了的话,那我们就漏掉了这个条件。因为这个线程还没有放到等待队列上,所以调用pthread_cond_wait前要先锁互斥量,即调用pthread_mutex_lock(),pthread_cond_wait在把线程放进阻塞队列后,自动对mutex进行解锁,使得其它线程可以获得加锁的权利。这样其它线程才能对临界资源进行访问并在适当的时候唤醒这个阻塞的进程。当该函数返回后又自动给mutex加锁。
下面说明如何避免死锁。一般在下列情况下会出现死锁:
- 线程试图对同一个互斥量加锁两次
- 已拥有各自互斥量的两个线程相互请求加锁对方的互斥量
以下方法可以避免死锁的发生:
- 假设需要对两个互斥量同时加锁,那么要求所有线程必须先加锁A(或B),再加锁B(或A),死锁就不会发生。只有在一个线程试图以另一个线程相反的顺序锁住互斥量时,才可能出现死锁。
- 使用pthread_mutex_trylock函数,如果不能获得锁,先释放已经拥有的锁,过段时间重新尝试。
参考:
《unix环境高级编程》 P297-P311.