众所周知,互斥量(mutex)是同步线程对共享资源访问的技术,用来防止下面这种情况:线程A试图访问某个共享资源时,线程B正在对其进行修改,从而造成资源状态不一致。与之相关的一个术语临界区(critical section)是指访问某一共享资源的代码片段,并且这段代码的执行为原子(atomic)操作,即同时访问同一共享资源的其他线程不应中断该片段的执行。

我们先来看看不使用临界区技术保护共享资源的例子,该例子使用2个线程来同时递增同一个全局变量。

代码示例1:不使用临界区技术访问共享资源

 #include <pthread.h>
#include <stdio.h>
#include <stdlib.h> static int g_n = ; static void *
thread_routine(void *arg)
{
int n_loops = (int)(arg);
int loc;
int j; for (j = ; j < n_loops; j++)
{
loc = g_n;
loc++;
g_n = loc;
} return ;
} int
main(int argc, char *argv[])
{
int n_loops, s;
pthread_t t1, t2;
void *args[]; n_loops = (argc > ) ? atoi(argv[]) : ; args[] = (void *)n_loops;
s = pthread_create(&t1, , thread_routine, &args);
if (s != )
{
perror("error pthread_create.\n");
exit(EXIT_FAILURE);
} s = pthread_create(&t2, , thread_routine, &args);
if (s != )
{
perror("error pthread_create.\n");
exit(EXIT_FAILURE);
} s = pthread_join(t1, );
if (s != )
{
perror("error pthread_join.\n");
exit(EXIT_FAILURE);
} s = pthread_join(t2, );
if (s != )
{
perror("error pthread_join.\n");
exit(EXIT_FAILURE);
} printf("Loops [%d] times by 2 threads without critical section.\n", n_loops);
printf("Var g_n is [%d].\n", g_n);
exit(EXIT_SUCCESS);
}

运行以上代码生成的程序,若循环次数较少,比如每个线程都对全局变量g_n递增1000次,结果看起来很正常:

$ ./thdincr_nosync 1000
Loops [1000] times by 2 threads without critical section.
Var g_n is [2000].

如果加大每个线程的循环次数,结果将大不相同:

$ ./thdincr_nosync 10000000
Loops [10000000] times by 2 threads without critical section.
Var g_n is [18655665].

造成以上问题的原因在于下面的执行序列:

1. 线程1将g_n的值赋给局部变量loc。假设g_n的当前值为1000。

2. 线程1的时间片用尽,线程2开始执行。

3. 线程2执行多次循环:将g_n的值改为其他的值,例如3000,线程2的时间片用尽。

4. 线程1重新获得时间片,并从上次停止处恢复执行。线程1在上次运行时,已将g_n的值(1000)赋给loc,现在递增loc,再将loc的值1001赋给g_n。此时线程2之前递增操作的结果遭到覆盖。

如果使用上面同样的命令行参数运行该程序多次,g_n的值会出现很大波动:

$ ./thdincr_nosync 10000000
Loops [10000000] times by 2 threads without critical section.
Var g_n is [14085995].

$ ./thdincr_nosync 10000000
Loops [10000000] times by 2 threads without critical section.
Var g_n is [13590133].

$ ./thdincr_nosync 10000000
Loops [10000000] times by 2 threads without critical section.
Var g_n is [20000000].

$ ./thdincr_nosync 10000000
Loops [10000000] times by 2 threads without critical section.
Var g_n is [16550684].

这一行为结果的不确定性,原因在于内核CPU调度顺序的不可预测性。若在复杂的程序中发生这种不确定结果的行为,意味着此类错误将偶尔发作,难以复现,因此也很难发现。如果使用如下语句:

g_n++;        /* 或者: ++g_n */

来替换thread_routine内for循环中的3条语句,似乎可以解决这一问题,不过在很多硬件架构上,编译器在将这条语句转换成机器码时,其效果仍等同于原先thread_routine内for循环中的3条语句。即换成一条语句并非意味着该操作就是原子操作。

为了避免上述同一行为的结果不确定性,必须使用某种技术来确保同一时刻只有一个线程可以访问共享资源,在Linux/Unix系统中,互斥量mutex(mutual exclusion的缩写)就是为这种情况设计的一种线程间同步技术,可以使用互斥量来保证对任意共享资源的原子访问。

互斥量有两种状态:已锁定和未锁定。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的互斥量再次加锁,将可能阻塞线程或者报错,具体取决于加锁时使用的方法。

静态分配的互斥量:

互斥量既可以像静态变量那样分配,也可以在运行时动态创建,例如,通过malloc在堆中分配,或者在栈上的自动变量,下面的语句展示了如何初始化静态分配的互斥量:

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

互斥量的加锁和解锁操作:

初始化之后,互斥量处于未锁定状态。函数pthread_mutex_lock()可以锁定某一互斥量,而函数pthread_mutex_unlock()可以将一个已经锁定的互斥量解锁。

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex); /* 两个函数在成功时返回值为0,失败时返回一个正值代表错误号。 */

代码示例2:使用静态分配的互斥量保护对全局变量的访问

 #include <pthread.h>
#include <stdio.h>
#include <stdlib.h> static int g_n = ;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; static void *
thread_routine(void *arg)
{
int n_loops = *((int *)arg);
int loc;
int j;
int s; for (j = ; j < n_loops; j++)
{
s = pthread_mutex_lock(&mtx);
if (s != )
{
perror("error pthread_mutex_lock.\n");
exit(EXIT_FAILURE);
} loc = g_n;
loc++;
g_n = loc; s = pthread_mutex_unlock(&mtx);
if (s != )
{
perror("error pthread_mutex_unlock.\n");
exit(EXIT_FAILURE);
}
} return ;
} int
main(int argc, char *argv[])
{
pthread_t t1, t2;
int n_loops, s; n_loops = (argc > ) ? atoi(argv[]) : ; s = pthread_create(&t1, , thread_routine, &n_loops);
if (s != )
{
perror("error pthread_create.\n");
exit(EXIT_FAILURE);
} s = pthread_create(&t2, , thread_routine, &n_loops);
if (s != )
{
perror("error pthread_create.\n");
exit(EXIT_FAILURE);
} s = pthread_join(t1, );
if (s != )
{
perror("error pthread_join.\n");
exit(EXIT_FAILURE);
} s = pthread_join(t2, );
if (s != )
{
perror("error pthread_join.\n");
exit(EXIT_FAILURE);
} printf("Var g_n is [%d].\n", g_n);
exit(EXIT_SUCCESS);
}

运行此示例代码生成的程序,从结果中可以看出对g_n的递增操作总能保持正确:

$ ./thdincr_mutex 10000000
Var g_n is [20000000].
$ ./thdincr_mutex 10000000
Var g_n is [20000000].
$ ./thdincr_mutex 10000000
Var g_n is [20000000].
$ ./thdincr_mutex 10000000
Var g_n is [20000000].
$ ./thdincr_mutex 10000000
Var g_n is [20000000].

代码示例3:使用动态分配的互斥量保护对全局变量的访问

 #include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h> static int g_n = ; static void *
thread_routine(void *arg)
{
void **args = (void **)arg;
int n_loops = (int)(args[]);
int loc;
int j;
int s;
pthread_mutex_t *mtx = (pthread_mutex_t *)(args[]); for (j = ; j < n_loops; j++)
{
s = pthread_mutex_lock(mtx);
if (s != )
{
printf("error pthread_mutex_lock. return:[%d] errno:[%d]\n", s, errno);
exit(EXIT_FAILURE);
} loc = g_n;
loc++;
g_n = loc; s = pthread_mutex_unlock(mtx);
if (s != )
{
perror("error pthread_mutex_unlock.\n");
exit(EXIT_FAILURE);
}
} return ;
} int
main(int argc, char *argv[])
{
int n_loops, s;
pthread_t t1, t2;
pthread_mutex_t mtx;
pthread_mutexattr_t mtx_attr;
void *args[]; s = pthread_mutexattr_init(&mtx_attr);
if (s != )
{
perror("error pthread_mutexattr_init.\n");
exit(EXIT_FAILURE);
} s = pthread_mutexattr_settype(&mtx_attr, PTHREAD_MUTEX_ERRORCHECK);
if (s != )
{
perror("error pthread_mutexattr_settype.\n");
exit(EXIT_FAILURE);
} s = pthread_mutex_init(&mtx, &mtx_attr);
if (s != )
{
perror("error pthread_mutex_init.\n");
exit(EXIT_FAILURE);
} s = pthread_mutexattr_destroy(&mtx_attr);
if (s != )
{
perror("error pthread_mutexattr_destroy.\n");
exit(EXIT_FAILURE);
} n_loops = (argc > ) ? atoi(argv[]) : ; args[] = (void *)n_loops;
args[] = (void *)&mtx;
s = pthread_create(&t1, , thread_routine, &args);
if (s != )
{
perror("error pthread_create.\n");
exit(EXIT_FAILURE);
} s = pthread_create(&t2, , thread_routine, &args);
if (s != )
{
perror("error pthread_create.\n");
exit(EXIT_FAILURE);
} s = pthread_join(t1, );
if (s != )
{
perror("error pthread_join.\n");
exit(EXIT_FAILURE);
} s = pthread_join(t2, );
if (s != )
{
perror("error pthread_join.\n");
exit(EXIT_FAILURE);
} s = pthread_mutex_destroy(&mtx);
if (s != )
{
perror("error pthread_mutex_destroy.\n");
exit(EXIT_FAILURE);
} printf("Var g_n is [%d].\n", g_n);
exit(EXIT_SUCCESS);
}

多次运行示例3代码生成的程序会看到与示例2代码的程序同样的结果。

本文展示了Linux/Unix线程间同步技术---互斥量的基本功能和基础使用方法,在后面的文章中将会讨论互斥量的其他内容,如锁定互斥量的另外2个API: pthread_mutex_trylock()和pthread_mutex_timedlock() ,互斥量的性能,互斥量的死锁等。欢迎大家参与讨论。

本文参考了Michael Kerrisk的著作《The Linux Programming Interface》(中文版名为:Linux/Unix系统编程手册)第30章的内容,版权相关的问题请联系作者或者相应的出版社。

Linux/Unix 线程同步技术之互斥量(1)的更多相关文章

  1. Linux的线程同步对象:互斥量Mutex,读写锁,条件变量

        进程是Linux资源分配的对象,Linux会为进程分配虚拟内存(4G)和文件句柄等 资源,是一个静态的概念.线程是CPU调度的对象,是一个动态的概念.一个进程之中至少包含有一个或者多个线程.这 ...

  2. 线程同步方式之互斥量Mutex

    互斥量和临界区非常相似,只有拥有了互斥对象的线程才可以访问共享资源,而互斥对象只有一个,因此可以保证同一时刻有且仅有一个线程可以访问共享资源,达到线程同步的目的. 互斥量相对于临界区更为高级,可以对互 ...

  3. Linux下线程同步的几种方法

    Linux下提供了多种方式来处理线程同步,最常用的是互斥锁.条件变量和信号量. 一.互斥锁(mutex) 锁机制是同一时刻只允许一个线程执行一个关键部分的代码.  1. 初始化锁 int pthrea ...

  4. [转]一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程

    一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程 希望此文能给初学多线程编程的朋友带来帮助,也希望牛人多多指出错误. 另外感谢以下链接的作者给予,给我的学习带来了很大帮助 http ...

  5. 【WIN32进阶之路】:线程同步技术纲要

    前面博客讲了互斥量(MUTEX)和关键段(CRITICAL SECTION)的使用,想来总觉不妥,就如盲人摸象一般,窥其一脚而言象,难免以偏概全,追加一篇博客查遗补漏. win32下的线程同步技术分为 ...

  6. iOS开发系列-线程同步技术

    概述 多线程的本质就是CPU轮流随机分配给每条线程时间片资源执行任务,看起来多条线程同时执行任务. 多条线程同时访问同一块资源,比如操作同一个对象.统一变量.同一个文件,就会引发数据错乱和数据安全的问 ...

  7. C#线程同步技术(二) Interlocked 类

    接昨天谈及的线程同步问题,今天介绍一个比较简单的类,Interlocked.它提供了以线程安全的方式递增.递减.交换和读取值的方法. 它的特点是: 1.相对于其他线程同步技术,速度会快很多. 2.只能 ...

  8. C#线程学习笔记六:线程同步--信号量和互斥体

    本笔记摘抄自:https://www.cnblogs.com/zhili/archive/2012/07/23/Mutex_And_Semaphore.html,记录一下学习过程以备后续查用.     ...

  9. linux 线程的同步 一 (互斥量和信号量)

    互斥量(Mutex) 互斥量表现互斥现象的数据结构,也被当作二元信号灯.一个互斥基本上是一个多任务敏感的二元信号,它能用作同步多任务的行为,它常用作保护从中断来的临界段代码并且在共享同步使用的资源. ...

随机推荐

  1. jQuery 移动端ajax请求列表数据,实现点击翻页效果(还有手势往下滑动翻页)。

    1 首先是html部分 <div class="content"> <div class="list"></div>  // ...

  2. offsetwidth/clientwidth的区别

    clientWidth是对象看到的宽度(不含边线,即border)scrollWidth是对象实际内容的宽度(若无padding,那就是边框之间距离,如有padding,就是左padding和右pad ...

  3. mybatis中 ${}和#取值小记(Parameter index out of range)

    mybatis mapperxml文件中有两种取值法.${}和#{} $的是原样,#的是取值并转成指定?#{ele1,jdbcType=VARCHAR} 有个坑, 错误的写法 <if test= ...

  4. Sql Server R2还有备份数据库错误

    错误信息描述  该数据库是运行版本10.50.1600的服务器上备份的.该版本与此服务器(运行版本10.00.1600)不兼容.请在支持该被份的服务器上还原该数据,  或者使用与此服务器兼容的备份(M ...

  5. swift_初始化器的使用

    //: Playground - noun: a place where people can play import Cocoa ***************************结构体与Cla ...

  6. javascript进阶系列专题:闭包(Closure)

    在javascript中,函数可看作是一种数据,可以赋值给变量,可以嵌套在另一个函数中. var fun = function(){ console.log("平底斜"); } f ...

  7. C# WebForm内置对象2+Repeater的Command

    内置对象:用于页面之间的数据交互 为什么要使用这么内置对象?因为HTTP的无状态性. Session:在计算机中,尤其是在网络应用中,称为“会话控制”.Session 对象存储特定用户会话所需的属性及 ...

  8. [转]用CSS给SVG <use>的内容添加样式

    来源:http://www.w3cplus.com/svg/styling-svg-use-content-css.html?utm_source=tuicool&utm_medium=ref ...

  9. Orchard中codegen相关命令

    Orchard开放了命令行功能,用于在快速创建代码. 由于该功能默认没有开启.系统中提供两种开启方式: 1.进入管理后台->Modules->找到[Code Generation]-> ...

  10. Mysql Innodb 引擎优化-内存、日志、IO、其他相关参数

    介绍: InnoDB给MySQL提供了具有提交,回滚和崩溃恢复能力的事务安全(ACID兼容)存储引擎.InnoDB锁定在行级并且也在SELECT语句提供一个Oracle风格一致的非锁定读.这些特色增加 ...