一、互斥体的概念
- 互斥体实现了“互相排斥”同步的简单形式,所以名为互斥体。互斥体禁止多个线程同时进入受保护的代码“临界区”。因此,在任意时刻,只有一个线程被允许进入这样的代码保护区。
- 任何线程在进入临界区之前,必须获取与此区域相关的互斥体的所有权。如果已有另一线程拥有了临界区的互斥体,其他线程就不能再进入其中。这些线程必须等待,直到当前的属主线程释放该互斥体。
- 什么时候需要使用互斥体呢?互斥体用于保护共享的易变代码,也就是全局或静态变量。这样的数据必须通过互斥体进行保护,以防止它们在多个线程同时访问被损坏。
二、互斥锁(mutex)
-
互斥锁的定义
为了保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。使用互斥锁可以使线程按照顺序执行。通常,互斥锁通过确保一次只有一个线程执行代码的临界段来同步多个线程。互斥锁还可以保护单线程代码。
如上图所示,一段需要互斥执行的代码称为临界区。线程在进入临界区之前,首先尝试加锁lock(),如果成功,则进入临界区,此时称这个线程持有锁;否则,就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁操作unlock()。
-
互斥锁的特点
- 原子性:把一个互斥锁定义为一个原子操作,这意味着操作系统保证了如果一个线程锁定了互斥锁,则没有其他线程可以在同一时间成功锁定这个互斥量。
- 唯一性:如果一个线程锁定一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量。
- 非繁忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将被挂起(不占用CPU资源),直到第一个线程解锁,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
- 相关API函数接口
- 互斥锁初始化和销毁的函数原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 互斥锁上锁和解锁的函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
三、互斥锁函数详解
-
互斥锁初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
- mutex:指向要初始化的互斥锁的指针
- attr:指向属性对象的指针,该属性对象定义要初始化的互斥锁的属性。如果该指针为NULL,则使用默认的属性,默认属性为快速互斥锁。
- PTHREAD_MUTEX_NORMAL:一种标准的互斥锁类型,不做任何的错误检查或死锁检测。如果 线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁;互斥锁处于未锁定状态,或者已由其它线程锁定,对其解锁会导致不确定结果。
- PTHREAD_MUTEX_ERRORCHECK:此类互斥锁会提供错误检查。譬如这三种情况都会导致返 回错误:线程试图对已经由自己锁定的互斥锁再次进行加锁(同一线程对同一互斥锁加锁两次), 返回错误;线程对由其它线程锁定的互斥锁进行解锁,返回错误;线程对处于未锁定状态的互斥锁进行解锁,返回错误。这类互斥锁运行起来比较慢,因为它需要做错误检查,不过可将其作为调试工具,以发现程序哪里违反了互斥锁使用的基本原则。
- PTHREAD_MUTEX_RECURSIVE:此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁,但是如果解锁次数不等于加锁次数,则是不会释放锁的;所以,如果对一个递归互斥锁加锁两次,然后解锁一次,那么这个互斥锁依然处于锁定状态,对它再次进行解锁之前不会释放该锁
- PTHREAD_MUTEX_DEFAULT : 此类互斥锁提供默认的行为和特性。使用宏 PTHREAD_ MUTEX_INITIALIZER 初始化的互斥锁 , 或者调用参数 arg 为 NULL 的 pthread_mutexattr _init()函数所创建的互斥锁,都属于此类型。此类锁意在为互斥锁的实现保留最大灵活性, Linux 上 , PTHREAD_MUTEX_DEFAULT 类型互斥锁的行为与 PTHREAD_MUTEX_NO RMAL 类型相仿。
- 返回值:成功则返回0,错误就返回错误编号。
初始化方式可以分为动态初始化和静态初始化。
- 动态初始化:
#include <pthread.h> pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); //按缺省的属性初始化互斥体变量mutex
- 静态初始化:这种方法等价于NULL指定的attr参数调用pthread_mutex_init()来完成动态初始化,不同之处在于PTHREAD_MUTEX_INITIALIZER宏不进行错误检查。
#include <pthread.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
可以使用pthread_mutexattr_gettype()函数得到互斥锁的类型属性,使用pthread_mutexattr_settype()修改/设置互斥锁类型属性,其函数原型如下所示:
#include <pthread.h> int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type); int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
参数attr指向pthread_mutexattr_t类型对象;对于pthread_mutexattr_gettype()函数,函数调用成功会将互斥锁类型属性保存在参数type所指向的内存中,通过它返回出来;而对于pthread_mutexattr_settype()函数,会将参数attr所指对象的类型属性设置为参数type指定的类型。使用方式如下:
pthread_mutex_t mutex; pthread_mutexattr_t attr; /* 初始化互斥锁属性对象 */ pthread_mutexattr_init(&attr); /* 将类型属性设置为 PTHREAD_MUTEX_NORMAL */ pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); /* 初始化互斥锁 */ pthread_mutex_init(&mutex, &attr); ...... /* 使用完之后 */ pthread_mutexattr_destroy(&attr); pthread_mutex_destroy(&mutex);
-
互斥锁加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
注意:对共享资源的访问,要对互斥量进行加锁,如果互斥量已经上了锁,调用线程会阻塞,直到互斥量被解锁。
返回值:成功返回0,出错则返回错误编号。int pthread_mutex_trylock(pthread_mutex_t *mutex);
当互斥锁已经被其他线程锁住时,调用pthread_mutex_ trylock()函数尝试对互斥锁进行加锁,如果互斥锁处于未锁住状态,那么将锁住互斥锁并立即返回;如果互斥锁已经被其他线程锁住,那么将加锁失败,但不阻塞线程,而是返回错误码EBUSY。
返回值:成功返回0,出错返回错误编号,如果目标互斥锁已经被其他线程锁住,则返回EBUSY。
四、使用示例
使用一个互斥锁对全局变量g_count进行保护。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
static pthread_mutex_t mutex;
static int g_count = 0;
static void *new_thread_start(void *arg){
int loops = *((int *)arg);
int l_count, j;
for (j = 0; j < loops; j++) {
pthread_mutex_lock(&mutex);//互斥锁上锁
l_count = g_count;
l_count++;
g_count = l_count;
pthread_mutex_unlock(&mutex);//互斥锁解锁
}
return (void *)0;
}
static int loops;
int main(int argc, char *argv[]){
pthread_t tid1, tid2;
int ret;
/* 获取用户传递的参数 */
if (2 > argc)
loops = 10000000; //没有传递参数默认为 1000 万次
else
loops = atoi(argv[1]);
/* 初始化互斥锁 */
pthread_mutex_init(&mutex, NULL);
/* 创建 2 个新线程 */
ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
if (ret) {
fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
/* 等待线程结束 */
ret = pthread_join(tid1, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
ret = pthread_join(tid2, NULL);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}
/* 打印结果 */
printf("g_count = %d\n", g_count);
exit(0);
}
五、使用互斥锁实现进程间的互斥操作
上面说介绍的都是在线程中使用互斥锁,其实进程间的操作也可以使用互斥锁。需要pthread_mutexattr_t的帮助,将原来的线程锁切换为进程锁。
假设有两个进程同时操作票数,一个进程是每次+1,另一个进程是每次+2。此时就可以借助进程间互斥锁保证票数这个全局变量能够被正确访问。
那么我们可以定义一个结构体mt,其中num标识总的票数,mutex是互斥锁,mutexattr是锁的属性。
typedef struct mt
{
int num; //多进程共享的票数
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
}mt;
使用mmap映射使得多进程之间可以共享变量。
int fd = open("mt_test", O_CREAT|O_RDWR, 077); //打开文件,获得一个文件描述符
if(-1 == fd)
{
printf("open file failed!\n");
return -1;
}
(void)ftruncate(fd, sizeof(mt)); //截取fd的长度
mm = (mt*)mmap(NULL, sizeof(mt), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
unlink("mt_test"); //删除文件
pthread_mutex_t默认是线程间互斥锁,要是想用在进程之间,需要pthread_mutexattr_t *restrictattr填充这个字段,在属性字段当中修改为PTHREAD_PROCESS_SHARED标识进程间共享。
pthread_mutexattr_init(&mm->mutexattr); //初始化mutex属性
pthread_mutexattr_setpshared(&mm->mutexattr, PTHREAD_PROCESS_SHARED); //修改属性为进程间共享
pthread_mutex_init(&mm->mutex, &mm->mutexattr); //初始化mutex锁
上述工作完成后,已经在共享区创建了一个结构体,并且父进程已经接受了指向共享区的指针,fork()创建子进程之后父子进程都可以挂接在共享区上面。
/*
互斥量 实现 多进程 之间的同步
*/
#include<unistd.h>
#include<sys/mman.h>
#include<pthread.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
typedef struct mt
{
int num;
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
}mt;
// 通过父子进程对同一个变量 mm->num 进行++ ,父进程一次+2 ,子进程一次 +1
int main(void)
{
int i;
struct mt* mm;
pid_t pid;
// 创建映射区文件
int fd = open("mt_test", O_CREAT|O_RDWR, 0777);
if( fd == -1 )
{
printf("open file failed!\n");
return -1;
}
(void)ftruncate(fd, sizeof(mt));
mm = (mt*)mmap(NULL, sizeof(mt), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
unlink("mt_test");
//就是这个mt结构体,当我们创建mt
// 建立映射区 MAP_ANON匿名映射
//mm = (mt*)mmap(NULL,sizeof(*mm),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
// printf("-------before memset------\n");
//memset(mm,0x00,sizeof(*mm));
// printf("-------after memset------\n");
pthread_mutexattr_init(&mm->mutexattr); // 初始化 mutex 属性
pthread_mutexattr_setpshared(&mm->mutexattr, PTHREAD_PROCESS_SHARED); // 修改属性为进程间共享
pthread_mutex_init(&mm->mutex, &mm->mutexattr); // 初始化一把 mutex 锁
pid = fork();
if( pid == 0 ) // 子进程
{
for( i=0; i<10;i++ )
{
pthread_mutex_lock(&mm->mutex);
(mm->num)++;
printf("-child--------------num++ %d\n", mm->num);
pthread_mutex_unlock(&mm->mutex);
sleep(1);
}
}
else
{
for( i=0;i<10;i++)
{
sleep(1);
pthread_mutex_lock(&mm->mutex);
mm->num += 2;
printf("--------parent------num+=2 %d\n", mm->num);
pthread_mutex_unlock(&mm->mutex);
}
wait(NULL);
}
pthread_mutexattr_destroy(&mm->mutexattr); // 销毁 mutex 属性对象
pthread_mutex_destroy(&mm->mutex); // 销毁 mutex 锁
return 0;
}
注意: pthread库不是Linux系统默认的库,链接时需要使用库libpthread.a,所以在编译中要加-lpthread参数:gcc 1.c -lpthread