多线程程序能够充分利用多核CPU,提升程序性能。在编写多线程程序时,我们首先需要考虑的问题是如何协调好各个线程之间的工作,让他们有条不紊的共同高效完成工作,即:线程同步。
Linux中,可以通过互斥锁、条件变量、信号量、读写锁等来解决线程的资源同步问题。
此外,在多线程编程实现线程同步过程中,尤其需要注意避免发生死锁现象。关于死锁,详细说明如下:
计算机系统中,如果系统的资源分配策略不当,更常见的可能是程序员写的程序有错误等,则会导致进程(线程)因竞争资源不当而产生死锁的现象。
产生死锁的原因主要是:
1) 因为系统资源不足。
2) 进程(线程)运行推进的顺序不合适。
3) 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
产生死锁的四个必要条件:
1) 互斥条件:一个资源每次只能被一个进程使用。
2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
下面依次来了解线程同步的几种方式。
1.互斥锁
互斥锁用来保证一段时间内只有一个线程在执行一段代码。只有获得了互斥锁的线程才能执行用互斥锁包含的代码片段。例如,假设各个线程向同一个文件顺序写入数据,为保证结果的准确性,可以通过互斥锁来实现。
互斥锁代码示例如下:
...
pthread_mutex_t mutex;
....
while(1){
//锁定互斥锁
pthread_mutex_lock(&mutext);
WriteSomethingToFile(fd);
//释放互斥锁
pthread_mutex_unlock(&mutex);
}
这样一来,就可以保证在多个线程向同一个文件写如数据时数据的准确性。
2.条件变量
通常条件变量都是和互斥量一起使用来完成线程同步。条件变量本身是由互斥量保护的。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。
条件变量特别适用于多个线程等待某个条件的发生。如果不使用条件变量,那么每个线程就需要不断尝试获得互斥锁并检查条件是否发生,这样大大浪费了系统的资源。
一个使用条件变量实现同步的示例程序如下:
这样一来,就可以保证在多个线程向同一个文件写如数据时数据的准确性。
2.条件变量
通常条件变量都是和互斥量一起使用来完成线程同步。条件变量本身是由互斥量保护的。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。
条件变量特别适用于多个线程等待某个条件的发生。如果不使用条件变量,那么每个线程就需要不断尝试获得互斥锁并检查条件是否发生,这样大大浪费了系统的资源。
一个使用条件变量实现同步的示例程序如下:
struct msg {
struct msg *m_next;
/* ... more stuff here ... */
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void
process_msg(void)
{
struct msg *mp;
for (;;) {
pthread_mutex_lock(&qlock);
while (workq == NULL)
pthread_cond_wait(&qready, &qlock);
mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&qlock);
/* now process the message mp */
}
}
void
enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}
3.信号量
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。
4.读写锁
读写锁与互斥量类似,但读写锁允许更高的并行性,主要原因在于读写锁有三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。虽然一次只有一个线程可以占有写模式的读写锁,但可以有多个线程同时占有读模式的读写锁。
一个unlock的RW lock可以被某个线程获取R锁或者W锁。
如果被一个线程获得R锁,RW lock可以被其它线程继续获得R锁,而不必等待该线程释放R锁。但是,如果此时有其它线程想要获得W锁,它必须等到所有持有共享读取锁的线程释放掉各自的R锁。
如果一个锁被一个线程获得W锁,那么其它线程,无论是想要获取R锁还是W锁,都必须等待该线程释放W锁。
这样,多个线程就可以同时读取共享资源。而具有危险性的写入操作则得到了互斥锁的保护。
一个读写锁使用示例如下:
#include <stdlib.h>
#include <pthread.h>
struct job {
struct job *j_next;
struct job *j_prev;
pthread_t j_id; /* tells which thread handles this job */
/* ... more stuff here ... */
};
struct queue {
struct job *q_head;
struct job *q_tail;
pthread_rwlock_t q_lock;
};
/*
* Initialize a queue.
*/
int
queue_init(struct queue *qp)
{
int err;
qp->q_head = NULL;
qp->q_tail = NULL;
err = pthread_rwlock_init(&qp->q_lock, NULL);
if (err != 0)
return(err);
/* ... continue initialization ... */
return(0);
}
/*
* Insert a job at the head of the queue.
*/
void
job_insert(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->q_lock);
jp->j_next = qp->q_head;
jp->j_prev = NULL;
if (qp->q_head != NULL)
qp->q_head->j_prev = jp;
else
qp->q_tail = jp; /* list was empty */
qp->q_head = jp;
pthread_rwlock_unlock(&qp->q_lock);
}
/*
* Append a job on the tail of the queue.
*/
void
job_append(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->q_lock);
jp->j_next = NULL;
jp->j_prev = qp->q_tail;
if (qp->q_tail != NULL)
qp->q_tail->j_next = jp;
else
qp->q_head = jp; /* list was empty */
qp->q_tail = jp;
pthread_rwlock_unlock(&qp->q_lock);
}
/*
* Remove the given job from a queue.
*/
void
job_remove(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->q_lock);
if (jp == qp->q_head) {
qp->q_head = jp->j_next;
if (qp->q_tail == jp)
qp->q_tail = NULL;
} else if (jp == qp->q_tail) {
qp->q_tail = jp->j_prev;
if (qp->q_head == jp)
qp->q_head = NULL;
} else {
jp->j_prev->j_next = jp->j_next;
jp->j_next->j_prev = jp->j_prev;
}
pthread_rwlock_unlock(&qp->q_lock);
}
/*
* Find a job for the given thread ID.
*/
struct job *
job_find(struct queue *qp, pthread_t id)
{
struct job *jp;
if (pthread_rwlock_rdlock(&qp->q_lock) != 0)
return(NULL);
for (jp = qp->q_head; jp != NULL; jp = jp->j_next)
if (pthread_equal(jp->j_id, id))
break;
pthread_rwlock_unlock(&qp->q_lock);
return(jp);
}