[转]一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程
一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程
希望此文能给初学多线程编程的朋友带来帮助,也希望牛人多多指出错误。
另外感谢以下链接的作者给予,给我的学习带来了很大帮助
http://blog.csdn.net/locape/article/details/6040383
http://www.cnblogs.com/liuweijian/archive/2009/12/30/1635888.html
一、什么是多线程?
当我自己提出这个问题的时候,我还是很老实的拿着操作系统的书,按着上面的话敲下“为了减少进程切换和创建开销,提高执行效率和节省资源,我们引入了线程的概念,与进程相比较,线程是CPU调度的一个基本单位。”
形象点的举个例子说:一个WEB服务器可以同时接收来自不同用户的网页访问请求,显然服务器处理这些网页请求都是通过并发进行的否则将会造成用户等待时间 长或者响应效率低等问题。如果在服务器中使用进程的办法来处理来自不同网页访问请求的话,我们可以用创建父进程以及多个子进程的方法,然而这样会花费很大 的系统开销和占用较多的资源,因此这样会较大的限制了访问服务器的用户数量。
使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的 进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线 程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间
使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的 方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快 捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改
二、互斥锁
正如上面所说的,如果两个线程同时对一块内存进行读写或者对向同一个文件写数据,那么结果是难以设想的。正因为如此,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
例如int *a int *b 分别指向两块内存,上面的值分别初始化为(200, 100) 线程A执行这样的一个操作:将*a的值减少50,*b的值增加50.
线程B执行:打印出(a 跟 b 指向的内存的值的和)。
如果串行运行:A: *a -= 50; *b += 50; B: printf("%d\n", *a + *b);
如果并发执行,则有可能会出现一下调度:*a -= 50; printf("%d\n", *a + *b); *b += 50;
因此我们可以引入互斥量,在对共享数据读写时进行锁操作,实现对内存的访问以互斥的形式进行。
#include <stdio.h> #include <unistd.h> #include <pthread.h> int a = 200; int b = 100; pthread_mutex_t lock; void * ThreadA( void *) { pthread_mutex_lock(&lock); //锁 a -= 50; sleep(5); //执行到一半 使用sleep 放弃cpu调度 b += 50; pthread_mutex_unlock(&lock); } void * ThreadB( void *) { sleep(1); //放弃CPU调度 目的先让A线程运行。 pthread_mutex_lock(&lock); printf ( "%d\n" , a + b); pthread_mutex_unlock(&lock); } int main() { pthread_t tida, tidb; pthread_mutex_init(&lock, NULL); pthread_create(&tida, NULL, ThreadA, NULL); pthread_create(&tidb, NULL, ThreadB, NULL); pthread_join(tida, NULL); pthread_join(tidb, NULL); return 1; } |
以上输出为300 去掉锁操作 输出为250。
三、信号量
作用域 | 上锁时 | |
信号量 | 进程间或线程间(linux仅线程间) | 只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait阻塞,直到sem_post释放后value值加一 |
互斥锁 | 线程间 | 只要被锁住,其他任何线程都不可以访问被保护的资源 成功后否则就阻塞 |
信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。
信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
可能这样说大家对信号量的概念还是很模糊,举个例子,现在有个图书馆,其能容纳100人,现在有两个线程A、B,A线程执行:往图书管理进入一个人,B
线程:从图书馆出来一个人。那么为了使得线程A在图书馆满人的时候进入等待,而不是继续往图书馆里进人,使得B线程在图书馆没人的时候等待人进入,我们可
以引入信号量:IN OUT 分别初始化为100和0。
那么A则可以被表示为A:P(IN) //剩余容量减少一 如果容量为0 则等待 B:P(OUT) //人数减少1 如果人数为0则阻塞等待
......... //登记进入图书馆的人信息
............. //记录 离开图书馆的人信息 (随便一系列操作。)
V(OUT)//增加信号量OUT 表示人数+1 V(IN) //增加图书馆剩余容量+1
通过这样我们就实现了线程的同步。
sem_init 用语初始化一个信号量其原型:int sem_init(sem_t *sem, int pshared, unsigned int value);
sem_init() 初始化一个定位在 sem 的匿名信号量。value 参数指定信号量的初始值。 pshared 参数指明信号量是由进程内线程共享,还是由进程之间共享。如果 pshared 的值为 0,那么信号量将被进程内的线程共享,并且应该放置在所有线程都可见的地址上
上面的P V操作相当于sem_wait () sempost()
sem_wait函数也是一个原子操作,它的作用是从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值才开始做减法。也就是说,如果你对一个值为2的信号量调用sem_wait(),线程将
会继续执行,这信号量的值将减到1。如果对一个值为0的信号量调用sem_wait(),这个函数就
会地等待直到有其它线程增加了这个值使它不再是0为止。如果有两个线程都在sem_wait()中等待同一个信号量变成非零值,那么当它被第三个线程增加
一个“1”时,等待线程中只有一个能够对信号量做减法并继续执行,另一个还将处于等待状态。
sem_post的作用很简单,就是使信号量增加1
三、条件变量
条件变量通过允许线程阻塞和
等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥
锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并
重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步。
假设有共享的资源sum,与之相关联的mutex
是lock_s.假设每个线程对sum的操作很简单的,与sum的状态无关,比如只是sum++.那么只用mutex足够了.程序员只要确保每个线程操作
前,取得lock,然后sum++,再unlock即可.每个线程的代码将像这样
add() { pthread_mutex_lock(lock_s); sum++; pthread_mutex_unlock(lock_s); } |
如果操作比较复杂,假设线程t0,t1,t2的操作是sum++,而线程
t3则是在sum到达100的时候,打印出一条信息,并对sum清零. 这种情况下,如果只用mutex,
则t3需要一个循环,每个循环里先取得lock_s,然后检查sum的状态,如果sum>=100,则打印并清零,然后unlock.如果sum&
amp; amp; amp; amp; lt;100,则unlock,并sleep()本线程合适的一段时间.
这个时候,t0,t1,t2的代码不变,t3的代码如下:
print() { while { pthread_mutex_lock(lock_s); if (sum>=100) { printf (“sum reach 100!”); pthread_mutex_unlock(lock_s); } else { pthread_mutex_unlock(lock_s); my_thread_sleep(100); return ; } } } |
这种办法有两个问题
1) sum在大多数情况下不会到达100,那么对t3的代码来说,大多数情况下,走的是else分支,只是lock和unlock,然后sleep().这浪费了CPU处理时间.
2) 为了节省CPU处理时间,t3会在探测到sum没到达100的时候sleep()一段时间.这样却又带来另外一个问题,亦即t3响应速度下降.可能在sum到达200的时候,t3才醒过来.
3) 这样,程序员在设置sleep()时间的时候陷入两难境地,设置得太短了节省不了资源,太长了又降低响应速度.真是难办啊!
这个时候,condition variable内裤外穿,从天而降,拯救了焦头烂额的你. (抄袭的哈哈~)
你首先定义一个condition variable.
pthread_cond_t cond_sum_ready=PTHREAD_COND_INITIALIZER;
t0,t1,t2的代码只要后面加两行,像这样
add() { pthread_mutex_lock(lock_s); sum++; pthread_mutex_unlock(lock_s); if (sum>=100) pthread_cond_signal(&cond_sum_ready); } <br><br> |
T3线程的print<br><br>print { pthread_mutex_lock(lock_s); while (sum<100) pthread_cond_wait(&cond_sum_ready, &lock_s); printf (“sum is over 100!”); sum=0; pthread_mutex_unlock(lock_s); return ; } <br><br> |
注意两点:
1) 在thread_cond_wait()之前,必须先lock相关联的mutex,
因为假如目标条件未满足,pthread_cond_wait()实际上会unlock该mutex,
然后block,在目标条件满足后再重新lock该mutex, 然后返回.
2) 为什么是while(sum<100),而不是if(sum<100)
?这是因为在pthread_cond_signal()和pthread_cond_wait()返回之间,有时间差,假设在这个时间差内,还有另外一
个线程t4又把sum减少到100以下了,那么t3在pthread_cond_wait()返回之后,显然应该再检查一遍sum的大小.
这就是用 while的用意
四、实战:
下面是一个例子:
//一个多线程例子:三个线程 0 1 往水池中加水。 线程2放水。 如果水池满 则阻塞进水线程 等待 #include <unistd.h> #include <pthread.h> #include <stdio.h> #include <errno.h> #include<semaphore.h> pthread_mutex_t Poll_Work; //互斥锁 sem_t Poll_IN; //水池容量信号量 sem_t Poll_OUT; //当前水量信号量 void * thread0( void *param) //0 1线程往水池中加水。 { while (1){ int rv = 0; for ( int i = 0; i < 5; ++ i) while ((rv = sem_wait(&Poll_IN)) != 0 && ( errno == EINTR)) //水量增加5 循环使得容量信号量减少5 ; pthread_mutex_lock(&Poll_Work); //加锁 线程互斥 (*( int *)param) += 5; printf ( "Thread0: %d\n" , *( int *)param); for ( int i = 0; i < 5; ++ i) sem_post( &Poll_OUT); //增加当前水量信号量 pthread_mutex_unlock(&Poll_Work); //解锁。 sleep(1); } return NULL; } void * thread1( void *param) { while (1){ int rv = 0; for ( int i = 0; i < 4; ++ i) while ((rv = sem_wait(&Poll_IN)) != 0 && ( errno == EINTR) ) ; pthread_mutex_lock(&Poll_Work); (*( int *)param) += 4; printf ( "Thread1: %d\n" , *( int *)param); for ( int i = 0; i < 4; ++ i) sem_post( &Poll_OUT); pthread_mutex_unlock(&Poll_Work); sleep(1); } return NULL; } void * thread2( void *param) //此线程用于将水从池中倒出。。 { while (1){ int rv = 0; for ( int i = 0; i < 3; ++ i) while ((rv = sem_wait(&Poll_OUT)) != 0 && ( errno == EINTR)) //减少当前水量 printf ( "xx" ); pthread_mutex_lock(&Poll_Work); (*( int *)param) -= 3; printf ( "Thread2: %d\n" , *( int *)param); for ( int i = 0; i < 3; ++ i) sem_post(&Poll_IN); //水池容量信号量增加 pthread_mutex_unlock(&Poll_Work); sleep(1); } return NULL; } int main() { int sum = 0; //水深 满为100米 初始化 池里没有水 int i; pthread_mutex_init(&Poll_Work, NULL); sem_init(&Poll_IN, 0, 100); sem_init(&Poll_OUT, 0, sum); pthread_t ths[4]; pthread_create(&ths[0], NULL, thread0, ( void *)&sum); pthread_create(&ths[1], NULL, thread1, ( void *)&sum); pthread_create(&ths[2], NULL, thread2, ( void *)&sum); for (i = 0; i < 3; ++ i){ pthread_join(ths[i], NULL); } } |
[转]一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程的更多相关文章
- linux 线程的同步 二 (互斥锁和条件变量)
互斥锁和条件变量 为了允许在线程或进程之间共享数据,同步时必须的,互斥锁和条件变量是同步的基本组成部分. 1.互斥锁 互斥锁是用来保护临界区资源,实际上保护的是临界区中被操纵的数据,互斥锁通常用于保护 ...
- Linux内核分析第三周学习总结:构造一个简单的Linux系统MenuOS
韩玉琪 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.Linux内 ...
- 第三周:构造一个简单的LINUX系统MENUOS
吕松鸿 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.Linux内 ...
- Linux内核设计第三周——构造一个简单的Linux系统
Linux内核设计第三周 ——构造一个简单的Linux系统 一.知识点总结 计算机三个法宝: 存储程序计算机 函数调用堆栈 中断 操作系统两把宝剑: 中断上下文的切换 进程上下文的切换 linux内核 ...
- 《Linux内核分析》第三周 构建一个简单的Linux系统MenuOS
[刘蔚然 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000] WEEK THREE ...
- 第三节 构造一个简单的Linux系统MenuOS——20135203齐岳
第三节 构造一个简单的Linux系统MenuOS By 20135203齐岳 Linux内核源代码 arch/ 支持不同cpu的源代码 Documentations/ 文档存储 init/ 内核启动相 ...
- 一个简单的iBatis入门例子
一个简单的iBatis入门例子,用ORACLE和Java测试 目录结构: 1.导入iBatis和oracle驱动. 2.创建类Person.java package com.ibeats;import ...
- Linux内核分析— —构造一个简单的Linux系统MenuOS(20135213林涵锦)
Linux内核分析— —构造一个简单的Linux系统MenuOS 实验内容 Linux内核的启动过程,从start_kernel到init进程启动 使用实验楼的虚拟机打开shell cd LinuxK ...
- 《Linux内核分析》第三周笔记 构造一个简单的Linux系统MenuOS
构造一个简单的Linux系统MenuOS 一.linux内核源代码简介 三大法宝(存储程序计算机.函数调用堆栈.中断)和两把宝剑(中断上下文的切换:保存现场和恢复现场.进程上下文的切换) 1.在lin ...
随机推荐
- java并发编程参考资料
1.java编程思想 2.java并发编程实战 3.java并发编程的艺术 4.http://www.infoq.com/cn/author/%E7%A8%8B%E6%99%93%E6%98%8E#文 ...
- Android-Universal-Image-Loader的缓存处理机制
讲到缓存,平时流水线上的码农一定觉得这是一个高大上的东西.看过网上各种讲缓存原理的文章,总感觉那些文章讲的就是玩具,能用吗?这次我将带你一起看过UIL这个国内外大牛都追捧的图片缓存类库的缓存处理机制. ...
- 计算字符串中al_num,spance_num,digit_num,other_num的个数
def jisuan(x) : al_num = 0 spance_num = 0 digit_num = 0 other_num = 0 for i in x : if i.isdigit() : ...
- Web服务器amp搭建
- pycharm 注册
pycharm在线注册:设置激活服务器 Help->Register->License server 在License server address中填写:http://idea.lany ...
- MFC编程入门之十五(对话框:一般属性页对话框的创建及显示)
属性页对话框包括向导对话框和一般属性页对话框两类,上一节讲了如何创建并显示向导对话框,本节将继续介绍一般属性页对话框的创建和显示. 实际上,一般属性页对话框的创建和显示过程和向导对话框是很类似的.将上 ...
- 转帖-[教程] Win7精简教程(简易中度)2016年8月-0day
[教程] Win7精简教程(简易中度)2016年8月 0day 发表于 2016-8-19 16:08:41 https://www.itsk.com/thread-370260-1-1.html ...
- oracle表空间不足相关问题解决办法
欢迎和大家交流技术相关问题: 邮箱: jiangxinnju@163.com 博客园地址: http://www.cnblogs.com/jiangxinnju GitHub地址: https://g ...
- Windows下pip安装包报错:Microsoft Visual C++ 9.0 is required Unable to find vcvarsall.bat
刚在机器上windows环境下装上pip方便以后安装包的时候使用,谁知道第一次使用pip安装asyncio的时候就报错. 在Windows7x64下使用pip安装包的时候提示报错:Microsoft ...
- 【html5】常见标签使用说明(持续更新)
说明: 所谓常见,是指我在优秀网页源码中见到的. 1.viewport 我见到的时候是这样: <meta name="viewport" content="widt ...