互斥锁指代相互排斥,它是最基本的同步形式。互斥锁用于保护临界区,以保证任何时刻只有一个线程在执行其中的代码(假设互斥锁由多个线程共享),或任何时刻只有一个进程在执行其中的代码(假设互斥锁由多个进程共享)。既然任何时刻只有线程能够锁住一个给定的互斥锁,于是这样的代码保证任何时刻只有一个线程在执行其临界区中的指令。
在Linux中,互斥锁被声明为具有pthread_mutex_t数据类型的变量。如果互斥锁变量是静态分配的,那么我们可以把它初始化为常值PTHREAD_MUTEX_INITIALIZER,例如:
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
如果互斥锁是动态分配的(例如通过malloc),或者分配在共享内存里,那我们必须在我们运行时调用pthread_mutex_init函数来初始化它。
下面三个函数给一个互斥锁上锁和解锁:
#include <pthread.h>
int pthread_mutex_lock( pthread_mutext_t *mptr);
int pthread_mutex_trylock( pthread_mutex_t *mptr);
int pthread_mutex_unlock( pthread_mutex_t *mptr );
如果尝试给一个已由另外某个线程锁住的互斥锁上锁,那么pthread_mutex_lock将阻塞到该互斥锁解锁为止。pthread_mutex_trylock是对应的非阻塞函数,如果该互斥锁已经锁住,它就返回一个EBUSY错误。
生产者消费者问题
同步中有一个生产者消费者问题的经典问题,一个或多个生产者创建着一个个的数据条目,然后这些条目由一个或者多个消费者处理。数据条目在生产者和消费者之间是使用某种类型的IPC传递的。
//unpipc.h
#ifndef UNPIPC_H
#define UNPIPC_H
#include <sys/types.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <time.h>
#include <error.h>
#include <fcntl.h>
#include <pthread.h>
#include <unistd.h>
#include <limits.h>
#include <string.h>
#include <signal.h>
#ifdef HAVE_MQUEUE_H
#include <mqueue.h>
#endif
#include <semaphore.h>
#define max(a,b) ( (a)>(b)?(a):(b))
#define min(a,b) ( (a)<(b)?(a):(b))
#endif // UNPIPC_H
//客户端代码
#include <stdio.h>
#include "unpipc.h"
#define MAXNITEMS 100000 //缓冲区条目数
#define MAXNTHREADS 100 //线程数
int nitems ; //实际的条目数
//这些变量是共享的,我们把它们以及相应的互斥锁收集到一个名为Shared
//的结构中,目的是为了强调这些变量只应该在拥有其互斥锁时访问。nput是
//buffer数组下一次存放的元素下标,nval是下次存放的值。
//定义该结构体边初始化初始化互斥锁
struct
{
pthread_mutex_t mutex;//互斥锁
int buffer[MAXNITEMS];//缓冲区
int nput ;//元素下标
int nval;//元素值
} Shared = { PTHREAD_MUTEX_INITIALIZER };
void *produce( void*);//生产者线程函数声明
void *comsume( void *);//消费者线程函数声明
int main(int argc, char** argv )
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if( argc != 3 )
{
printf( "usage:test <#items> <#threads>\n");
return -1;
}
//从命令行取items值,缓冲区最大容量
nitems = min( atoi(argv[1]), MAXNITEMS );
//从命令行取线程数
nthreads = min( atoi(argv[2]), MAXNTHREADS );
//设置线程并发级别,这里是所有的生产者加上一个消费者
pthread_setconcurrency( nthreads +1 );
//生产者线程
for( i = 0; i < nthreads ; i++ )
{
count[i] = 0;
pthread_create(&tid_produce[i], NULL,produce, &count[i] );
}
//创建消费者线程
pthread_create( &tid_consume, NULL, comsume, NULL );
for( i =0; i<nthreads; i++)
{
pthread_join( tid_produce[i], NULL );
}
pthread_join(tid_consume, NULL);
return 0;
}
void * produce( void* arg )
{
for( ; ; )
{
pthread_mutex_lock( &Shared.mutex );
if( Shared.nput > nitems )
{
pthread_mutex_unlock(&Shared.mutex );//array is full, return;
return NULL ;
}
Shared.buffer[Shared.nput] = Shared.nval;
Shared.nput++;
Shared.nval++;
pthread_mutex_unlock( &Shared.mutex );
* ( (int*)arg ) += 1;
}
}
//consumewait函数必须等到生产者产生了第i个条目。为检查这种条件,先给
//生产者的互斥锁上锁,再比较i和生产者的nput下标。我们必须在查看nput前
//获得互斥锁,因为某个生产者线程当时可能正处于更新该变量的过程。
void consumewait( int i )
{
for( ; ; )
{
pthread_mutex_lock( &Shared.mutex );
if ( i < Shared.nput )
{
pthread_mutex_unlock( &Shared.mutex );//
return ;
}
pthread_mutex_unlock(&Shared.mutex );
}
}
void *comsume( void* arg )
{
int i ;
for( i =0 ;i < nitems; i ++ )
{
consumewait( i );
printf("buffer[%d] = %d\n", i, Shared.buffer[i] );
}
return NULL ;
}
consumewait函数有一个问题就是:当期待的条目尚未准备好的时候,我们不能做什么?一次次的循环,每次给互斥锁上锁解锁,这可以称之为轮转或者轮询,是一种对CPU时间的严重浪费。
下面是程序的运行结果: