Linux系统编程(28)——线程间同步
多个线程同时访问共享数据时可能会冲突,这跟前面讲信号时所说的可重入性是同样的问题。比如两个线程都要把某个全局变量增加1,这个操作在某平台需要三条指令完成:
从内存读变量值到寄存器
寄存器的值加1
将寄存器的值写回内存
假设两个线程在多处理器平台上同时执行这三条指令,则可能导致下图所示的结果,最后变量只加了一次而非两次
我们通过一个简单的程序观察这一现象。上图所描述的现象从理论上是存在这种可能的,但实际运行程序时很难观察到,为了使现象更容易观察到,我们把上述三条指令做的事情用更多条指令来做:
- val= counter;
- printf("%x:%d\n", (unsigned int)pthread_self(), val + 1);
- counter= val + 1;
我们在“读取变量的值”和“把变量的新值保存回去”这两步操作之间插入一个printf调用,它会执行write系统调用进内核,为内核调度别的线程执行提供了一个很好的时机。我们在一个循环中重复上述操作几千次,就会观察到访问冲突的现象。
- #include <stdio.h>
- #include <stdlib.h>
- #include <pthread.h>
- #define NLOOP 5000
- int counter; /* incremented by threads */
- void *doit(void *);
- int main(int argc, char **argv)
- {
- pthread_ttidA, tidB;
- pthread_create(&tidA,NULL, &doit, NULL);
- pthread_create(&tidB,NULL, &doit, NULL);
- /* wait for both threads to terminate */
- pthread_join(tidA,NULL);
- pthread_join(tidB,NULL);
- return0;
- }
- void *doit(void *vptr)
- {
- int i, val;
- /*
- * Each thread fetches, prints, and incrementsthe counter NLOOP times.
- * The value of the counter should increasemonotonically.
- */
- for(i = 0; i < NLOOP; i++) {
- val= counter;
- printf("%x:%d\n", (unsigned int)pthread_self(), val + 1);
- counter= val + 1;
- }
- returnNULL;
- }
我们创建两个线程,各自把counter增加5000次,正常情况下最后counter应该等于10000,但事实上每次运行该程序的结果都不一样,有时候数到5000多,有时候数到6000多。
对于多线程的程序,访问冲突的问题是很普遍的,解决的办法是引入互斥锁(Mutex,Mutual Exclusive Lock),获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。
Mutex用pthread_mutex_t类型的变量表示,可以这样初始化和销毁:
- #include <pthread.h>
- int pthread_mutex_destroy(pthread_mutex_t*mutex);
- int pthread_mutex_init(pthread_mutex_t*restrict mutex,
- const pthread_mutexattr_t *restrict attr);
- pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
返回值:成功返回0,失败返回错误号。
pthread_mutex_init函数对Mutex做初始化,参数attr设定Mutex的属性,如果attr为NULL则表示缺省属性,本章不详细介绍Mutex属性,感兴趣的读者可以参考[APUE2e]。用pthread_mutex_init函数初始化的Mutex可以用pthread_mutex_destroy销毁。如果Mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。Mutex的加锁和解锁操作可以用下列函数:
- #include <pthread.h>
- 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_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。
如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。
现在我们用Mutex解决先前的问题:
- #include <stdio.h>
- #include <stdlib.h>
- #include <pthread.h>
- #define NLOOP 5000
- int counter; /* incremented by threads */
- pthread_mutex_t counter_mutex =PTHREAD_MUTEX_INITIALIZER;
- void *doit(void *);
- int main(int argc, char **argv)
- {
- pthread_ttidA, tidB;
- pthread_create(&tidA,NULL, doit, NULL);
- pthread_create(&tidB,NULL, doit, NULL);
- /* wait for both threads to terminate */
- pthread_join(tidA,NULL);
- pthread_join(tidB,NULL);
- return0;
- }
- void *doit(void *vptr)
- {
- int i, val;
- /*
- * Each thread fetches, prints, and incrementsthe counter NLOOP times.
- * The value of the counter should increasemonotonically.
- */
- for(i = 0; i < NLOOP; i++) {
- pthread_mutex_lock(&counter_mutex);
- val= counter;
- printf("%x:%d\n", (unsigned int)pthread_self(), val + 1);
- counter= val + 1;
- pthread_mutex_unlock(&counter_mutex);
- }
- returnNULL;
- }
这样运行结果就正常了,每次运行都能数到10000。
那么挂起等待”和“唤醒等待线程”的操作如何实现?每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先在把自己加入等待队列中,然后置线程状态为睡眠,然后调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。
一般情况下,如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁(Deadlock)。另一种典型的死锁情形是这样:线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。不难想象,如果涉及到更多的线程和更多的锁,有没有可能死锁的问题将会变得复杂和难以判断。
写程序时应该尽量避免同时获得多个锁,如果一定有必要这么做,则有一个原则:如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。比如一个程序中用到锁1、锁2、锁3,它们所对应的Mutex变量的地址是锁1<锁2<锁3,那么所有线程在需要同时获得2个或3个锁时都应该按锁1、锁2、锁3的顺序获得。如果要为所有的锁确定一个先后顺序比较困难,则应该尽量使用pthread_mutex_trylock调用代替pthread_mutex_lock调用,以免死锁。
Linux系统编程(28)——线程间同步的更多相关文章
- linux应用编程之进程间同步
一.描述 在操作系统中,异步并发执行环境下的一组进程,因为相互制约关系,进而互相发送消息.互相合作.互相等待,使得各进程按一定的顺序和速度执行,称为进程间的同步.具有同步关系的一组并发进程,称为合作进 ...
- linux系统编程:线程原语
线程原语 线程概念 线程(thread),有时被称为轻量级进程(Lightweight Process,LWP).是程序运行流的最小单元.一个标准的线程由线程ID.当前指令指针(PC),寄存器集合和堆 ...
- Linux系统编程:线程控制
一.提出问题 问1.线程存在的意义是什么?什么时候适合使用多线程? 答1.在单进程环境中实现多任务,线程可访问其所在进程的资源,例如内存.描述符等.对于单进程,如果要完成多项任务,这些任务只能依次执行 ...
- linux系统编程:线程同步-相互排斥量(mutex)
线程同步-相互排斥量(mutex) 线程同步 多个线程同一时候訪问共享数据时可能会冲突,于是须要实现线程同步. 一个线程冲突的演示样例 #include <stdio.h> #includ ...
- linux系统编程:线程同步-信号量(semaphore)
线程同步-信号量(semaphore) 生产者与消费者问题再思考 在实际生活中,仅仅要有商品.消费者就能够消费,这没问题. 但生产者的生产并非无限的.比如,仓库是有限的,原材料是有限的,生产指标受消费 ...
- Linux系统编程(29)——线程间同步(续篇)
线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行.在pthread库中通过条件变 ...
- Linux系统编程 —线程同步概念
同步概念 同步,指对在一个系统中所发生的事件之间进行协调,在时间上出现一致性与统一化的现象. 但是,对于不同行业,对于同步的理解略有不同.比如:设备同步,是指在两个设备之间规定一个共同的时间参考:数据 ...
- Linux 系统编程 学习:11-线程:线程同步
Linux 系统编程 学习:11-线程:线程同步 背景 上一讲 我们介绍了线程的属性 有关设置.这一讲我们来看线程之间是如何同步的. 额外安装有关的man手册: sudo apt-get instal ...
- linux系统编程--线程同步
同步概念 所谓同步,即同时起步,协调一致.不同的对象,对“同步”的理解方式略有不同. 如,设备同步,是指在两个设备之间规定一个共同的时间参考: 数据库同步,是指让两个或多个数据库内容保持一致,或者按需 ...
- Linux系统编程—进程间同步
我们知道,线程间同步有多种方式,比如:信号量.互斥量.读写锁,等等.那进程间如何实现同步呢?本文介绍两种方式:互斥量和文件锁. 互斥量mutex 我们已经知道了互斥量可以用于在线程间同步,但实际上,互 ...
随机推荐
- js的深拷贝和浅拷贝
一.数组的深浅拷贝 在使用JavaScript对数组进行操作的时候,我们经常需要将数组进行备份,事实证明如果只是简单的将它赋予其他变量,那么我们只要更改其中的任何一个,然后其他的也会跟着改变,这就导致 ...
- 数据库日期类型转换–HSQL
最近遇到要用HSQL查询离某个时间的后十分钟的记录,不像Oracle和SqlServer中可以直接有函数转换,而是直接通过'+'来得到 Hsql Document -- standard forms ...
- 使用 Spring 2.5 基于注解驱动的 Spring MVC--转
概述 继 Spring 2.0 对 Spring MVC 进行重大升级后,Spring 2.5 又为 Spring MVC 引入了注解驱动功能.现在你无须让 Controller 继承任何接口,无需在 ...
- 大数据笔记06:大数据之Hadoop的HDFS(文件的读写操作)
1. 首先我们看一看文件读取: (1)客户端(java程序.命令行等等)向NameNode发送文件读取请求,请求中包含文件名和文件路径,让NameNode查询元数据. (2)接着,NameNode返回 ...
- [转] JS nodeType返回类型
将HTML DOM中几个容易常用的属性做下记录: nodeName.nodeValue 以及 nodeType 包含有关于节点的信息. nodeName 属性含有某个节点的名称. 元素节点的 node ...
- git 更换远程仓库地址
1. 更改软件仓库指向.在github改了用户名和仓库名称后,仓库地址也相应的发生的变化,这时候就需要更新本地仓库以指向新的远程仓库地址: $git remote set-url origin git ...
- 进程ps、kill 、grep
linux上进程有5种状态: 1. 运行(正在运行或在运行队列中等待) 2. 中断(休眠中, 受阻, 在等待某个条件的形成或接受到信号) 3. 不可中断(收到信号不唤醒和不可运行, 进程必须等待直到有 ...
- 富文本 Htmll类 html标签
HTML类可解析的标签 在手机上显示从网络端获取的数据有两种方式,一种是WebView,另一种是TextView,WebView大家都知道,功能强大但不灵活,下面主要说下TextView. 通过查看a ...
- css动画+滚动的+飞舞的小球
源代码如下: <!DOCTYPE html><html><head> <title>xi</title> <meta charset= ...
- TextView过长显示省略号, TextView文字中间加横线
1.TextView显示的内容过长时自动显示省略号: 省略号的位置:android:ellipsize="end" 省略号在结尾android:ellipsize=" ...