Linux互斥和同步应用程序(一):posix线程和线程之间的相互排斥
【版权声明:尊重原创,转载请保留出处:blog.csdn.net/shallnet 或 .../gentleliu,文章仅供学习交流。请勿用于商业用途】
有了进程的概念,为何还要使用线程呢?
首先,回顾一下上一个系列我们讲到的IPC。各个进程之间具有独立的内存空间,要进行数据的传递仅仅能通过通信的方式进行,这样的方式不仅费时,并且非常不方便。
而同一个进程下的线程是共享全局内存的,所以一个线程的数据能够在还有一个线程中直接使用,及快捷又方便。
其次,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而执行于一个进程中的多个线程,它们彼此之间使用同样的地址空间,共享大部分数据。启动一个线程所花费的空间远远小于启动一个进程所花费的空间。并且,线程间彼此切换所需的时间也远远小于进程间切换所须要的时间。
可是,伴随着这些长处,线程却带来了同步与相互排斥的问题。
以下先讲讲线程基本函数:
- 线程的创建pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
一个线程由一个线程ID(參数thread)标识,新的线程创建成功,其值通过指针thread返回。
參数attr为线程属性(比方:优先级、初始栈大小等),通常我们使用默认设置,设为NULL。
參数start_routine为一个函数指针,指向线程运行的函数,最后參数arg为函数start_routine唯一參数,假设须要传递多个參数。须要打包为结构。然后将其地址传给该函数。
pthread_create成功时返回0,失败为非0值,这和其它linux系统调用的习惯不一样。
- pthread_join函数
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
通过调用该函数等待一个给定线程终止,类似于线程的waitpid函数。
该函数等待參数thread指定的线程终止,该函数会堵塞,直到线程thread终止,将线程返回的(void
*)指针赋值为retval指向的位置。然后回收已经终止线程占用的全部存储器资源。
- pthread_self函数
#include <pthread.h>
pthread_t pthread_self(void);
该函数用于获取线程自身线程ID。
类似于进程的getpid函数。
- pthread_detach函数
#include <pthread.h>
int pthread_detach(pthread_t thread);
该函数可分离可结合线程,线程能够通过以pthread_self()为參数的pthread_detach调用来分离他们自己。
一个分离线程是不能被其它线程回收或杀死的。他的存储器资源在他终止时由系统自己主动释放。一个可结合线程可以被其它线程收回其资源和杀死。在被其它线程收回之前,他的存储器资源是没有被释放的。在不论什么一个时间点上,线程是可结合的或者是可分离的。默认情况下,线程是被创建成可结合的。
为了避免存储器泄露。每一个可结合线程都应该要么被其它线程现实的收回,要么通过调用pthread_detach函数被分离。
在现实的程序中,我们一般都使用分离的线程。
- pthread_exit函数
#include <pthread.h>
void pthread_exit(void *retval);
该函数作用就是终止线程。假设该线程未曾分离。他的线程ID和退出状态将一直保留到调用进程内某个其它线程对他调用pthread_join。
另外,当线程函数(pthread_create第三个參数)返回时,该线程将终止;当创建该线程的进程main函数返回时,该线程也将终止。
以下给一个简单的演示样例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h> void *func_th(void *arg)
{
unsigned int *val = (unsigned int *)arg; printf("=======%s->%d==thread%d: %u====\n", __func__, __LINE__,
*val, (unsigned int)pthread_self());
return NULL;
} int main(int argc, const char *argv[])
{
pthread_t tid1, tid2;
int a, b; a = 1;
if (0 != pthread_create(&tid1, NULL, func_th, &a)) {
printf("pthread_create failed!\n");
return -1;
} b = 2;
if (0 != pthread_create(&tid2, NULL, func_th, &b)) {
printf("pthread_create failed!\n");
return -1;
} pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
运行2次输出为:
# ./target_bin
=======func_th->9==thread1: 3077856064====
=======func_th->9==thread2: 3069463360====
# ./target_bin
=======func_th->9==thread2: 3069315904====
=======func_th->9==thread1: 3077708608====
类似于进程,线程的调度随机的。
在前面開始我们说到同一个进程内的线程是共享全局内存的。那么当多个线程同一时候去改动一个全局变量的时候就会出问题,假设一个线程在改动某个变量时中途被挂起,操作系统去调度另外一个线程运行,那就可能导致错误。
我们无法保证操作系统对这些操作都是原子的。
在我们在如今的样例中这样去复现这样的问题:一个线程对一个全局变量(100)进行读-加1-读操作,另个变量对该全局进行减1操作,我们通过sleep来实现加线程先运行。减线程在加线程的加和读之间进行,最后来查看加操作是否是我们期望的结果(101)。样例例如以下:
(该演示样例仅仅是为了强化执行时出错,并对这样的错误有一个宏观的了解而写)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h> int gval = 100; void *func_add_th(void *arg)
{
int *val = (int *)arg; printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self()); printf("before add 1, gval=%d\n", gval);
gval += 1; sleep(4);//此时add线程挂起。sub线程运行键操作 printf("after add 1, gval=%d\n", gval);
return NULL;
} void *func_sub_th(void *arg)
{
int *val = (int *)arg; printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self()); gval -= 1; return NULL;
} int main(int argc, const char *argv[])
{
pthread_t tid1, tid2;
int a, b; a = 1;
if (0 != pthread_create(&tid1, NULL, func_add_th, &a)) {
printf("pthread_create failed!\n");
return -1;
} sleep(1); //保证add线程先被调度
b = 2;
if (0 != pthread_create(&tid2, NULL, func_sub_th, &b)) {
printf("pthread_create failed!\n");
return -1;
} pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
运行结果例如以下:
# ./target_bin
==do func_add_th==thread1: 3078355776====
before add 1, gval=100
==do func_sub_th==thread2: 3069963072====
after add 1, gval=100
通过输出我们能够看到sub操作在加1和读之间操作,终于读取出来的值仍然是100,不是我们期望的101。
这就是两个线程不是相互排斥带来的结果,所以我们希望在某某一线程一段代码执行期间,仅仅有一个线程在执行。当执行完毕之后,下一个线程执行该部分代码。所以我们须要将该部分代码加锁。这就是线程编程,也是并发编程须要考虑的问题。
解决多线程共享的问题就是使用相互排斥锁(mutex,即mutual
exliusion)来保护共享数据。在运行某一段代码是首先要持有该相互排斥锁。运行完毕之后再释放该锁。相互排斥锁是类型为pthread_mutex_t的变量。
使用例如以下方法来加锁和解锁操作。
#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);
首先我们须要初始化锁,初始化方法有两种,一种是静态初始化,给锁变量赋值PTHREAD_MUTEX_INITIALIZER,一种动态初始化,使用函数pthread_mutex_init。
我们使用静态方法初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
当试图使用pthread_mutex_lock()获得一个已经被另外线程加锁的锁时。本线程将堵塞,直到相互排斥锁被解锁为止。
函数pthread_mutex_trylock为获取锁的非堵塞版本号,当获取失败时会马上返回。
我们改动add和sub线程函数分别例如以下:
void *func_add_th(void *arg)
{
int *val = (int *)arg; pthread_mutex_lock(&mutex);//此处加锁
printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self()); printf("before add 1, gval=%d\n", gval);
gval += 1; sleep(4); printf("after add 1, gval=%d\n", gval);
pthread_mutex_unlock(&mutex);//此处释放锁
return NULL;
}
void *func_sub_th(void *arg)
{
int *val = (int *)arg; pthread_mutex_lock(&mutex);
printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self()); gval -= 1;
pthread_mutex_unlock(&mutex); return NULL;
}
运行结果为:
# ./target_bin
==do func_add_th==thread1: 3077614400====
before add 1, gval=100
after add 1, gval=101
==do func_sub_th==thread2: 3069221696====
通过结果输出能够看到,sub操作是在add操作运行完毕之后才运行的。而add线程输出结果也是我们预期的。所以我们的加锁是成功的。
可是假设add线程要运行非常久的话。sub线程就要堵塞非常久。我们能够将sub线程加锁函数改为非堵塞版本号,当加锁失败时。马上返回。
改动后的sub线程函数:
void *func_sub_th(void *arg)
{
int *val = (int *)arg; if (0 != pthread_mutex_trylock(&mutex)) {
printf("failed to lock!\n");
return NULL;
}
printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self()); gval -= 1;
pthread_mutex_unlock(&mutex); return NULL;
}
执行输出为:
# ./target_bin
==do func_add_th==thread1: 3077638976====
before add 1, gval=100
failed to lock!
after add 1, gval=101
当多个线程同一时候须要多个同样锁时,可能会出现死锁的情况。比方两个线程同一时候须要相互排斥锁1和相互排斥锁2。线程a先获得锁1,线程b获得锁2,这是线程a、b分别还须要锁2和锁1。但此时两个锁都被加锁了,都堵塞在那里等待对方释放锁。这样死锁就出现了。我们来实现一下死锁的情况,将之前两个样例的线程函数改动例如以下:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_sec = PTHREAD_MUTEX_INITIALIZER; void *func_add_th(void *arg)
{
int *val = (int *)arg; pthread_mutex_lock(&mutex);//1. add线程先加第一个锁mutex
printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self());
sleep(2);//等待2秒。让sub线程加第二个锁mutex_sec
pthread_mutex_lock(&mutex_sec);//4. add线程加锁mutex_sec失败 printf("before add 1, gval=%d\n", gval);
gval += 1; sleep(4); printf("after add 1, gval=%d\n", gval);
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&mutex_sec);
return NULL;
} void *func_sub_th(void *arg)
{
int *val = (int *)arg; pthread_mutex_lock(&mutex_sec);//2. Sub线程比add线程先加锁mutex_sec
pthread_mutex_lock(&mutex);//3. Sub线程加锁mutex失败 printf("==do %s==thread%d: %u====\n", __func__,
*val, (unsigned int)pthread_self()); gval -= 1;
pthread_mutex_unlock(&mutex);
pthread_mutex_unlock(&mutex_sec); return NULL;
}
上面两个线程依照函数凝视中1-2-3-4顺序执行。执行时程序就卡在那里出现了死锁。
能够使用非堵塞版本号的加锁函数来加锁,只是也要注意在第二个锁加锁不成功情况下,须要释放第一个锁再返回,不然其它线程仍然得不到第一个锁。有时在线程须要多个相互排斥锁时,让线程依照指定的相同顺序进行加锁也能够避免死锁。程序死锁出现时非常难定位,所以程序员在编程(尤其是在设计)时须要注意避免这个问题。
本节演示样例代码下载:
版权声明:本文博主原创文章,博客,未经同意不得转载。
假设你认为你的实际物品,请点击以下“最佳”。
Linux互斥和同步应用程序(一):posix线程和线程之间的相互排斥的更多相关文章
- Linux互斥和同步应用程序(四):posix互斥信号和同步
[版权声明:尊重原创.转载请保留源:blog.csdn.net/shallnet 要么 .../gentleliu,文章仅供学习交流,请勿用于商业用途] 在前面讲共享内 ...
- 【Linux】rsync同步文件 & 程序自启动
rsync使用 1. 为什么使用rsync? rsync解决linux系统下文件同步时, 增量同步问题. 使用场景: 线上需要定时备份数据文件(视频资源), 使用rsync完成每天的增量备份. 参见: ...
- POSIX 线程具体解释(3-相互排斥量:"固定加锁层次"/“试加锁-回退”)
有时一个相互排斥量是不够的: 比方: 当多个线程同一时候訪问一个队列结构时,你须要2个相互排斥量,一个用来保护队列头,一个用来保护队列元素内的数据. 当为多线程建立一个树结构时.你可能须要为每一个节点 ...
- Android多线程研究(3)——线程同步和相互排斥及死锁
为什么会有线程同步的概念呢?为什么要同步?什么是线程同步?先看一段代码: package com.maso.test; public class ThreadTest2 implements Runn ...
- linux下的同步与互斥
linux下的同步与互斥 谈到linux的并发,必然涉及到线程之间的同步和互斥,linux主要为我们提供了几种实现线程间同步互斥的 机制,本文主要介绍互斥锁,条件变量和信号量.互斥锁和条件变量包含在p ...
- java同步和互斥【用具体程序说明】
java同步和互斥[用具体程序说明] 所有对象都自动含有单一的锁,也就是所有对象都有且只有唯一的锁,所以当某个任务(线程)访问一个类A中含有sycnhronized的方法是,那么 ...
- Linux并发与同步专题 (4) Mutex互斥量
关键词:mutex.MCS.OSQ. <Linux并发与同步专题 (1)原子操作和内存屏障> <Linux并发与同步专题 (2)spinlock> <Linux并发与同步 ...
- Linux相互排斥与同步应用(三):posix线程实现单个生产者和单个消费者模型
[版权声明:尊重原创.转载请保留出处:blog.csdn.net/shallnet 或 .../gentleliu.文章仅供学习交流,请勿用于商业用途] 在第一节说到了 ...
- Linux多线程与同步
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 典型的UNIX系统都支持一个进程创建多个线程(thread).在Linux进程基础 ...
随机推荐
- POJ 2538 WERTYU水的问题
[题目简述]:题意非常easy,没有trick. [分析]:事实上这题还是挺有趣的,在 算法竞赛入门经典中也有这一题. 详见代码: // 120K 0Ms /* 边学边做 -- */ // 字符串:W ...
- Cocos2d-x实现简单的翻牌效果
触发器互联网影响找了很多.有自己的点重写一个复杂的sprite类来实现.简单的操作来对引擎的使用CCOrbitCamera实现,但是,也存在一些问题,后变反了. 我在用的仅仅是一个简单的翻牌效果,点击 ...
- js阻止冒泡
js阻止冒泡 (ev || event).cancelBubble = true; 标签切换 <script type="text/javascript"> windo ...
- Asp.NET MVC3 使用 SignalR 实现推(持续)
一,Persistent Connection 演示示例教程 1.实现server端代码 1),编写server PersistentConnection 代码 项目中 SignalR 文件夹下创建 ...
- mybatis至mysql插入一个逗号包含值误差
mybatis至mysql插入形如"11,22,33"当误差.我使用了错误的原因是美元符号镶嵌sql.正确的做法是使用# 有时间去看看mybatis的$和#差异. 版权声明:本文 ...
- 【Android基础】listview控件的使用(1)------最简单的listview的使用
listview控件是项目开发中最常用的空间之一,我将慢慢推出关于listview的一系列的文章,先从最简单的,系统自带的listview开始吧! 先上效果图: activity_one.xml &l ...
- 读书时间《JavaScript高级程序设计》三:函数,闭包,作用域
上一次看了第6章,面向对象.这里接着看第7章. 第7章:函数表达式 定义函数有两种方式:函数声明.函数表达式 //函数声明 function functionName(arg0,arg1,arg2){ ...
- Android决议具体解释
1.Android手机常见的分辨率 WVGA:800x480 FWVGA:854x480 QHD:960x540 720P:1280x720(SD.standard definition,SD) 10 ...
- PL/SQL Developer ORA-12154: TNS: 无法解析指定的连接标识符
底: 在这台机器(Win7 64位置 最后)设备Oracle 11g的client(已安装32位ORACLEclient.假设安装64位ORACLEclient的时候,在CMD命令中 ...
- 《web全栈工程师的自我修养》阅读笔记
在买之前以为这本书是教你怎么去做一个web全栈工程师,以及介绍需要掌握的哪些技术的书,然而看的过程中才发现,是一本方法论的书.读起来的感觉有点像红衣教主的<我的互联网方法论>,以一些自己的 ...