多个线程同时访问共享数据时可能会冲突,这跟前面讲信号时所说的可重入性是同样的问题。比如两个线程都要把某个全局变量增加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)——线程间同步的更多相关文章

  1. linux应用编程之进程间同步

    一.描述 在操作系统中,异步并发执行环境下的一组进程,因为相互制约关系,进而互相发送消息.互相合作.互相等待,使得各进程按一定的顺序和速度执行,称为进程间的同步.具有同步关系的一组并发进程,称为合作进 ...

  2. linux系统编程:线程原语

    线程原语 线程概念 线程(thread),有时被称为轻量级进程(Lightweight Process,LWP).是程序运行流的最小单元.一个标准的线程由线程ID.当前指令指针(PC),寄存器集合和堆 ...

  3. Linux系统编程:线程控制

    一.提出问题 问1.线程存在的意义是什么?什么时候适合使用多线程? 答1.在单进程环境中实现多任务,线程可访问其所在进程的资源,例如内存.描述符等.对于单进程,如果要完成多项任务,这些任务只能依次执行 ...

  4. linux系统编程:线程同步-相互排斥量(mutex)

    线程同步-相互排斥量(mutex) 线程同步 多个线程同一时候訪问共享数据时可能会冲突,于是须要实现线程同步. 一个线程冲突的演示样例 #include <stdio.h> #includ ...

  5. linux系统编程:线程同步-信号量(semaphore)

    线程同步-信号量(semaphore) 生产者与消费者问题再思考 在实际生活中,仅仅要有商品.消费者就能够消费,这没问题. 但生产者的生产并非无限的.比如,仓库是有限的,原材料是有限的,生产指标受消费 ...

  6. Linux系统编程(29)——线程间同步(续篇)

    线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行.在pthread库中通过条件变 ...

  7. Linux系统编程 —线程同步概念

    同步概念 同步,指对在一个系统中所发生的事件之间进行协调,在时间上出现一致性与统一化的现象. 但是,对于不同行业,对于同步的理解略有不同.比如:设备同步,是指在两个设备之间规定一个共同的时间参考:数据 ...

  8. Linux 系统编程 学习:11-线程:线程同步

    Linux 系统编程 学习:11-线程:线程同步 背景 上一讲 我们介绍了线程的属性 有关设置.这一讲我们来看线程之间是如何同步的. 额外安装有关的man手册: sudo apt-get instal ...

  9. linux系统编程--线程同步

    同步概念 所谓同步,即同时起步,协调一致.不同的对象,对“同步”的理解方式略有不同. 如,设备同步,是指在两个设备之间规定一个共同的时间参考: 数据库同步,是指让两个或多个数据库内容保持一致,或者按需 ...

  10. Linux系统编程—进程间同步

    我们知道,线程间同步有多种方式,比如:信号量.互斥量.读写锁,等等.那进程间如何实现同步呢?本文介绍两种方式:互斥量和文件锁. 互斥量mutex 我们已经知道了互斥量可以用于在线程间同步,但实际上,互 ...

随机推荐

  1. ceph主要数据结构解析3-Ceph_fs.h文件

    (1)集群内部子版本协议类型宏定义:与公共协议保持独立性,以便消息类型和协议升级受影响 #define CEPH_OSDC_PROTOCOL   24 /* server/client */OSD服务 ...

  2. Cocos2dx 多线程

    多-threaded负荷plist特征.获取知识的必要性: 1.多线程开启:pthread 2.怎样在线程中载入plist 一.多线程开启 当我们想在程序中开多线程中.第一想到的是cocos2d-x有 ...

  3. mongodb入门教程二

    title: mongodb入门教程二 date: 2016-04-07 10:33:02 tags: --- 上一篇文章说了mongodb最基本的东西,这边博文就在深入一点,说一下mongo的一些高 ...

  4. jdbc01

    1.创建对应的数据库以及表 /* SQLyog 企业版 - MySQL GUI v8.14 MySQL - 5.5.32-log : Database - news ***************** ...

  5. HTML学习(1)

    1.缩写和首字母缩写<abbr><acronym> <abbr title="etcetera">etc.</abbr> <a ...

  6. (转)关于c#中的事件

    原文链接http://blog.csdn.net/joyhen/article/details/8500211 如有不明白的地方欢迎加QQ群14670545 探讨 最近在看委托,然后看到事件,以前一直 ...

  7. javaScript 自定义事件、发布订阅设计模式

    现在很多应用都允许用户根据自己的喜好订阅一些自己较为关注的信息,当应用更新了这些信息后将针对不同的订阅类型推送此类信息.例如xx招聘网,当你订阅了互联网IT技术相关分类的招聘信息推送后,当企业在该网站 ...

  8. 回传值(代理、通知、block)

    回传值问题,一直都是困扰初学者的问题,今写者 代理.通知.block 三者的回传值做了一个小小的总结, Main.storyboard 视图: 通过代码分别创建三个代表 代理.通知.block 的按钮 ...

  9. xcode 删除 Provisioning Profile

    provisioning profile path: ~/Library/MobileDevice/Provisioning Profiles 打开并日期排序,删除老的 provisioning pr ...

  10. Android应用开发中关于this.context=context的理解

    在Android应用开发中,有的类里面需要声明一个Context的成员变量,然后还需要在该类的构造函数中加上this.context=context;这行代码.为什么要这么写呢?不写不行么? 先看下面 ...