1.互斥量
当多个控制线程共享相同的内存时,需要确保每一个线程看到一致数据视图。如果不存在读取数据或者所有数据只读时不会存在一致性问题。如果某个线程正在修改变量而其他线程也可以读取或者修改这个变量的时候就需要对这些线程进行同步。在变量修改多于一个存储器访问周期时可能出现不一致的错误。(例如需要写的十一个长类型,需要多次写入内存。而写之间如果有其他线程读取就会出现同时读取一些修改过和没修改过的数据)。
通过使用pthread的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥量mutex本质上是一把锁,在访问共享资源前对互斥量加锁,然后其他任何试图访问互斥量加锁的线程将会被阻塞。访问完成后释放互斥量上的锁,被阻塞的线程将会变成可运行状态。同理,第一个变为可运行状态的进程将会锁住信号量,这样后面要访问该互斥量线程会被继续阻塞,直到回去再次等待它重新变为可用。
互斥量用pthread_mutex_t数据类型表示,在使用互斥量以前,必须首先对它进行初始化,可以把它置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量)或者使用pthread_mutex_init函数进行初始化。如果动态分配(malloc)那么释放内存前需要调用(pthread_mutex_destroy)
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destory(pthread_mutex_t *mutex);
要使用默认的属性初始化互斥量,只需要将attr设置为NULL。非默认的互斥量属性
<<Chapter 12.4>>
对互斥量进行加锁,需要调用pthread_mutex_lock,如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁需要调用pthread_mutex_unlock。
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/*成功返回0,失败则返回错误编号*/
如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁,成功加锁(原来未被锁住)则返回0,失败则不会被阻塞,直接返回EBUSY。
下面是一个整形信号量的实现,当线程需要对这个对象时则对引用计数加1,当对象使用完毕时,需要对引用计数减1.当最后一个引用被释放时,对象所占空间就会被释放。
- #include <stdlib.h>
- #include <pthread.h>
- struct foo {
- int f_count;
- pthread_mutex_t f_lock;
- };
- struct foo *
- foo_alloc(void) /* allocate the object */
- {
- struct foo *fp;
- if ((fp = malloc(sizeof(struct foo)))!=NULL) {
- fp->count = 1;
- if (pthread_mutex_init(&fp->lock, NULL) != 0) {
- free(fp);
- return NULL;
- }
- /* continue initialization */
- }
- return fp;
- }
- void
- foo_add(struct foo *fp) { /* add an reference to this object */
- pthread_mutex_lock(&fp->f_lock);
- fp->count++;
- pthread_mutex_unlock(&fp->f_lock);
- }
- void
- foo_rele(struct foo *fp) { /* release a reference to the object */
- pthread_mutex_lock(&fp->f_lock);
- if (--fp->f_count == 0) /* last reference */
- pthread_mutex_unlock(&fp->f_lock);
- pthread_mutex_destroy(&fp->f_lock);
- free(fp);
- } else {
- pthread_mutex_unlock(&fp->f_lock);
- }
- }
2. 读写锁
读写锁与mutex类似,不过读写锁允许更高的并行性。mutex只有两种状态(lock & unlock),而读写锁有三种状态读模式下加锁(所有以读模式对它进行加锁的线程都可以得到访问权,写模式访问则会被阻塞),写模式加锁(所有试图访问的都会被阻塞)和无锁。
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
/* 如果希望读写锁有默认的属性,则分配一个NULL给attr */
int pthread_rwlock_destory(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
- /*作业请求作业*/
- #include <stdlib.h>
- #include <pthread.h>
- struct job {
- struct job *j_next;
- struct job *j_prev;
- pthread_t j_id;
- /* something more */
- }
- 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);
- 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->j_prev = jp;
- 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->q_next = jp;
- else
- qp->q_head = jp;
- qp->q_tail = jp;
- }
- /*
- * Remove the given job from a queue
- */
- void
- job_remove(struct queue *qp, struct job *jp)
- {
- pthread_rwlock_wrlock(qp->q_lock);
- if (qp->head == jp) {
- qp->head = jp->q_next;
- if (qp->tail == jp)
- qp->tail = NULL;
- }
- else if (qp->tail == jp)
- qp->tail = jp->q_prev;
- else {
- jp->q_prev->q->next = jp->q_next;
- jp->q_next->q->prev = jp->q_prev;
- }
- free(jp);
- pthread_rwlock_unlock(qp->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->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->lock)
- return jp;
- }
3. 条件变量
条件变量是线程可用的另一种同步机制。通常和互斥量一起使用,允许线程以无竞争的方式等待特定条件发生。条件本身由互斥信号量保护,线程在改变条件状态之前必须首先锁住信号量,在获得信号量之前不会察觉到这种改变。
条件变量在使用之前要初始化。动态分配的可以由pthread_cond_init函数来分配,静态变量可以直接将PTHREAD_COND_INITIALIZER赋给静态分配的条件。动态分配的变量需要使用pthread_mutex_destory来去初始化,然后再free。
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const strcut timespec *restrict timeout);
/*若成功则返回真,错误则返回错误编号*/
传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的信号量传给函数,函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁。这个是原子操作,当它返回时,互斥量会被再次锁住。
pthread_cond_timedwait函数的工作方式与pthread_cond_wait函数相似。只是多了一个timeout。timeout变量指定了等待的时间。时间的结构:
struct timespec{
time_t tv_sec;
/*seconds*/
long tv_nsec;
/*nanoseconds*/
}
int pthread_cond_signal(pthread_cond_t *cond);
/*唤醒等待该条件的某个进程*/
int pthread_cond_broadcast(pthread_cond_t *cond);
/*唤醒等待该条件的所有进程*/
要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。函数pthread_cond_broadcast(pthread_cond_t *cond)用来唤醒所有被阻塞在条件变量cond上的线程。这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。
- pthread_mutex_t count_lock;
- pthread_cond_t count_nonzero;
- unsigned count;
- decrement_count () {
- pthread_mutex_lock (&count_lock);
- while(count == 0)
- pthread_cond_wait( &count_nonzero, &count_lock);
- count=count -1;
- pthread_mutex_unlock (&count_lock);
- }
- increment_count(){
- pthread_mutex_lock(&count_lock);
- if(count == 0)
- pthread_cond_signal(&count_nonzero);
- count=count+1;
- pthread_mutex_unlock(&count_lock);
- }
4. 信号量
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。下面我们逐个介绍和信号量有关的一些函数,它们都在头文件/usr/include/semaphore.h中定义。
信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));
sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
函数sem_post(sem_t *sem)用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程唤醒,选择机制同样是由线程的调度策略决定的。
函数sem_wait( sem_t *sem)被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem)是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
函数sem_destroy(sem_t *sem)用来释放信号量sem。
下面我们来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。
- /* File sem.c */
- #include <stdlib.h>
- #include <pthread.h>
- #include <semaphore.h>
- #define MAXSTACK 100
- int stack[MAXSTACK][2];
- int size=0;
- sem_t sem;
- /*
- * 从文件1.dat读取数据,每读一次,信号量加1
- */
- void ReadData1(void) {
- FILE *fp=fopen("1.dat","r");
- while(!feof(fp)){
- fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
- sem_post(&sem);
- ++size;
- }
- fclose(fp);
- }
- /*
- * 从文件2.dat读取数据
- */
- void ReadData2(void){
- FILE *fp=fopen("2.dat","r");
- while(!feof(fp)){
- fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);
- sem_post(&sem);
- ++size;
- }
- fclose(fp);
- }
- /*
- *阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待
- */
- void HandleData1(void) {
- while(1){
- sem_wait(&sem);
- printf("Plus:%d+%d=%dn",stack[size][0],stack[size][1],
- stack[size][0]+stack[size][1]);
- --size;
- }
- }
- void
- HandleData2(void) {
- while(1) {
- sem_wait(&sem);
- printf("Multiply:%d*%d=%dn",stack[size][0],stack[size][1],
- stack[size][0]*stack[size][1]);
- --size;
- }
- }
- int
- main(void) {
- pthread_t t1,t2,t3,t4;
- sem_init(&sem,0,0);
- pthread_create(&t1,NULL,(void *)HandleData1,NULL);
- pthread_create(&t2,NULL,(void *)HandleData2,NULL);
- pthread_create(&t3,NULL,(void *)ReadData1,NULL);
- pthread_create(&t4,NULL,(void *)ReadData2,NULL);
- /* 防止程序过早退出,让它在此无限期等待*/
- pthread_join(t1,NULL);
- }
在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem。
我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10
,我们运行sem,得到如下的结果:
Multiply:-1*-2=2
Plus:-1+-2=-3
Multiply:9*10=90
Plus:-9+-10=-19
Multiply:-7*-8=56
Plus:-5+-6=-11
Multiply:-3*-4=12
Plus:9+10=19
Plus:7+8=15
Plus:5+6=11
从中我们可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来这是由于size这个数值被各个线程任意修改的缘故。这也往往是多线程编程要注意的问题
Reference
APUE
互斥量,条件变量和信号量 http://blog.163.com/embeded-life/blog/static/10593251920098245319305/