ringbuffer java例子_使用Ring Buffer构建高性能的文件写入程序
最近常收到SOD框架的朋友报告的SOD的SQL日志功能报错:文件句柄丢失。经过分析得知,这些朋友使用SOD框架开发了访问量比较大的系统,由于忘记关闭SQL日志功能所以出现了很高频率的日志写入操作,从而偶然引起错误。后来我建议只记录出错的或者执行时间较长的SQL信息,暂时解决了此问题。但是作为一个热心造轮子的人,一定要看看能不能造一个更好的轮子出来。
前面说的错误原因已经很直白了,就是频繁的日志写入导致的,那么解决方案就是将多次写入操作合并成一次写入操作,并且采用异步写入方式。要保存多次操作的内容就要有一个类似“队列”的东西来保存,而一般的线程安全的队列,都是“有锁队列”,在性能要求很高的系统中,不希望在日志记录这个地方耗费多一点计算资源,所以最好有一个“无锁队列”,因此最佳方案就是Ring Buffer(环形缓冲区)了。
什么是Ring Buffer?顾名思义,就是一个内存环,每一次读写操作都循环利用这个内存环,从而避免频繁分配和回收内存,减轻GC压力,同时由于Ring Buffer可以实现为无锁的队列,从而整体上大幅提高系统性能。Ring Buffer的示意图如下,有关具体原理,请参考此文《Ring Buffer 有什么特别?》。
上文并没有详细说明如何具体读写Ring Buffer,但是原理介绍已经足够我们怎么写一个Ring Buffer程序了,接下来看看我在 .NET上的实现。
首先,定一个存放数据的数组,记住一定要用数组,它是实现Ring Buffer的关键并且CPU友好。
const int C_BUFFER_SIZE = 10;//写入次数缓冲区大小,每次的实际内容大小不固定
string[] RingBuffer = new string[C_BUFFER_SIZE];
int writedTimes = 0;
变量writedTimes 记录写入次数,它会一直递增,不过为了线程安全的递增且不使用托管锁,需要使用原子锁Interlocked。之后,根据每次 writedTimes 跟环形缓冲区的大小求余数,得到当前要写入的数组位置:
void SaveFile(string fileName, stringtext)
{int currP= Interlocked.Increment(refwritedTimes);int writeP= currP %C_BUFFER_SIZE ;int index = writeP == 0 ? C_BUFFER_SIZE - 1 : writeP - 1;
RingBuffer[index]= "Arr[" + index + "]:" +text;
}
Ring Buffer的核心代码就这么点,调用此方法,会一直往缓冲区写入数据而不会“溢出”,所以写入Ring Buffer效率很高。
一个队列如果只生产不消费肯定不行的,那么如何及时消费Ring Buffer的数据呢?简单的方案就是当Ring Buffer“写满”的时候一次性将数据“消费”掉。注意这里的“写满”仅仅是指写入位置 index达到了数组最大索引位置,而“消费”也不同于常见的堆栈,队列等数据结构,只是读取缓冲区的数据而不会移除它。
所以前面的代码只需要稍加改造:
void SaveFile(string fileName, stringtext)
{int currP= Interlocked.Increment(refwritedTimes);int writeP= currP %C_BUFFER_SIZE ;int index = writeP == 0 ? C_BUFFER_SIZE - 1 : writeP - 1;
RingBuffer[index]= "Arr[" + index + "]:" +text;if (writeP == 0)
{string result = string.Concat( RingBuffer);
FlushFile(fileName, result);
}
}
writeP == 0 表示当前一轮的缓冲区已经写满,然后调用函数 FlushFile 将Ring Buffer的数据连接起来,整体写入文件。
void FlushFile(string fileName, stringtext)
{using (FileStream fs = new FileStream(fileName, FileMode.Append, FileAccess.Write, FileShare.Write, 2048, FileOptions.Asynchronous))
{byte[] buffer =System.Text.Encoding.UTF8.GetBytes(text);
IAsyncResult writeResult= fs.BeginWrite(buffer, 0, buffer.Length,
(asyncResult)=>{
fs.EndWrite(asyncResult);
},
fs);//fs.EndWrite(writeResult);//这种方法异步起不到效果
fs.Flush();
}
}
在函数 FlushFile 中我们使用了异步写入文件的技术,注意 FileOptions.Asynchronous ,使用它才可以真正利用Windows的完成端口IOCP,将文件异步写入。
当然这段代码也可以使用.NET最新版本支持的 async/await ,不过我要让SOD框架继续支持.NET 2.0,所以只好这样写了。
现在,我们可以开多线程来测试这个循环队列效果怎么样:
Task[] arrTask = new Task[20];for (int i = 0; i < arrTask.Length; i++)
{
arrTask[i]= new Task(obj => SaveFile( (int)obj) ,i);
}for (int i = 0; i < arrTask.Length; i++)
{
arrTask[i].Start();
}
Task.WaitAll(arrTask);
MessageBox.Show(arrTask.Length+"Task All OK.");
这里开启20个Task任务线程来写入文件,运行此程序,发现20个线程才写入了10条数据,分析很久才发现,文件异步IO太快的话,会有缓冲区丢失,第一次写入的10条数据无法写入文件,多运行几次就没有问题了。所以还是得想法解决此问题。
通常情况下我们都是使用托管锁来解决这种并发问题,但本文的目的就是要实现一个“无锁环形缓冲区”,不能在此“功亏一篑”,所以此时“信号量”上场了。
同步可以分为锁定和信号同步,信号同步机制中涉及的类型都继承自抽象类WaitHandle,这些类型有EventWaitHandle(类型化为AutoResetEvent、ManualResetEvent)、Semaphore以及Mutex。见下图:
首先声明一个 ManualResetEvent对象:
ManualResetEvent ChangeEvent = new ManualResetEvent(true);
这里我们将 ManualResetEvent 对象设置成 “终止状态”,意味着程序一开始是允许所有线程不等待的,当我们需要消费Ring Buffer的时候再将 ManualResetEvent 设置成“非终止状态”,阻塞其它线程。简单说就是当要写文件的时候将环形缓冲区阻塞,直到文件写完才允许继续写入环形缓冲区。
对应的新的代码调整如下:
void SaveFile(string fileName, stringtext)
{
ChangeEvent.WaitOne();int currP= Interlocked.Increment(refwritedTimes);int writeP= currP %C_BUFFER_SIZE ;int index = writeP == 0 ? C_BUFFER_SIZE - 1 : writeP - 1;
RingBuffer[index]= "Arr[" + index + "]:" +text;if (writeP == 0)
{
ChangeEvent.Reset();string result = string.Concat( RingBuffer);
FlushFile(fileName, result);
}
}
然后,再FlushFile 方法的 回掉方法中,加入设置终止状态的代码,部分代码如下:
(asyncResult) =>{
fs.EndWrite(asyncResult);
ChangeEvent.Set();
}
OK,现在我们的程序具备高性能的安全的写入日志文件的功能了,我们来看看演示程序测试的日志结果实例:
Arr[0]:Thread index:0--FFFFFFF
Arr[1]:Thread index:1--FFFFFFF
Arr[2]:Thread index:8--FFFFFFF
Arr[3]:Thread index:9--FFFFFFF
Arr[4]:Thread index:3--FFFFFFF
Arr[5]:Thread index:2--FFFFFFF
Arr[6]:Thread index:4--FFFFFFF
Arr[7]:Thread index:10--FFFFFFF
Arr[8]:Thread index:5--FFFFFFF
Arr[9]:Thread index:6--FFFFFFF
Arr[0]:Thread index:7--FFFFFFF
Arr[1]:Thread index:11--FFFFFFF
Arr[2]:Thread index:12--FFFFFFF
Arr[3]:Thread index:13--FFFFFFF
Arr[4]:Thread index:14--FFFFFFF
Arr[5]:Thread index:15--FFFFFFF
Arr[6]:Thread index:16--FFFFFFF
Arr[7]:Thread index:17--FFFFFFF
Arr[8]:Thread index:18--FFFFFFF
Arr[9]:Thread index:19--FFFFFFF
测试结果符合预期!
到此,我们今天的主题就全部介绍完成了,不过要让本文的代码能够符合实际的运行,还要解决每次只写入少量数据并且将它定期写入日志文件的问题,这里贴出真正的局部代码:
PS:有朋友说采用信号量并不能完全保证程序安全,查阅了MSDN也说如果信号量状态改变还没有来得及应用,那么是起不到作用的,所以还需要检查业务状态标记,也就是在设置非终止状态后,马上设置一个操作标记,在其它线程中,需要检查此标记,以避免“漏网之鱼”引起不期望的结果。
再具体实现上,我们可以实现一个“自旋锁”,循环检查此状态标记,为了防止发生死锁,还需要有锁超时机制,代码如下:
void SaveFile(string fileName, stringtext)
{
ChangeEvent.WaitOne(10000);int currP= Interlocked.Increment(refWritedTimes);int writeP= currP %C_BUFFER_SIZE ;int index = writeP == 0 ? C_BUFFER_SIZE - 1 : writeP - 1;if (writeP == 0)
{
ChangeEvent.Reset();
IsReading= true;
RingBuffer[index]= "Arr[" + index + "]:" +text;
LastWriteTime=DateTime.Now;
WritingIndex= 0;
SaveFile(fileName,RingBuffer);
}else if (DateTime.Now.Subtract(LastWriteTime).TotalSeconds >C_WRITE_TIMESPAN)
{
ChangeEvent.Reset();
IsReading= true;
RingBuffer[index]= "Arr[" + index + "]:" +text;int length = index - WritingIndex + 1;if (length <= 0)
length= 1;string[] newArr = new string[length];
Array.Copy(RingBuffer, WritingIndex, newArr,0, length);
LastWriteTime=DateTime.Now;
WritingIndex= index + 1;
SaveFile(fileName, newArr);
}else{//防止漏网之鱼的线程在信号量产生作用之前修改数据//采用“自旋锁”等待
int count = 0;while(IsReading)
{if (count++ > 10000000)
{
Thread.Sleep(50);break;
}
}
RingBuffer[index]= "Arr[" + index + "]:" +text;
}
}
完整的Ring Buffer代码会在最新版本的SOD框架源码中,有关本篇文章测试程序的完整源码,请加QQ群讨论获取,
群号码:SOD框架高级群 18215717 ,加群请注明 PDF.NET技术交流 ,否则可能被拒绝。
ringbuffer java例子_使用Ring Buffer构建高性能的文件写入程序相关推荐
- Java 环形缓冲器(Ring Buffer)
环形缓冲器(Ring Buffer):环形队列,这里使用数组实现,但并未用上环形功能,因为设置了队满直接出队清空队列,如果只读取部分数据,又或者想要覆盖冲写,则可以用上环形功能 package cha ...
- oxygen 生成java对象_利用oxygen编辑并生成xml文件,并使用JAVA的JAXB技术完成xml的解析...
首先下载oxygen软件(Oxygen XML Editor),目前使用的是试用版(可以安装好软件以后get trial licence,获得免费使用30天的权限,当然这里鼓励大家用正版软件!!!) ...
- jdk12源代码文件_在JDK 11中启动单文件源代码程序
jdk12源代码文件 JEP 330 –启动单文件源代码程序是即将发布的JDK 11(18.9)发行版中令人兴奋的功能之一. 此功能允许直接使用java解释器执行Java源代码. 源代码在内存中编译, ...
- 【java】 linux下利用nohup后台运行jar文件包程序
Linux 运行jar包命令如下: 方式一: java -jar XXX.jar 特点:当前ssh窗口被锁定,可按CTRL + C打断程序运行,或直接关闭窗口,程序退出 那如何让窗口不锁定? 方式二 ...
- ddd java 例子_【Java】DDD思维导图
首页 专栏 java 文章详情 0 DDD思维导图 洪永佳发布于 今天 10:07 常见相关问题 DDD概念 DDD,全称Domain-Driven Design, 是一种处理复杂领域的设计思想,它试 ...
- idea maven创建java项目_新版本IntelliJ IDEA 构建maven,并用Maven创建一个web项目(图文教程)...
之前都没试过用maven来管理过项目,但是手动找包导包确实不方便,于是今天用2016版的IDEA进行了maven的初尝试. 打开IDEA,创建新项目: 然后选择Maven,以及选择自己电脑的jdk: ...
- 内存泄漏java例子_一次线上Java应用内存泄漏分析实例
由于JVM的内存管理采用GC垃圾自动回收机制,这使得Java程序员在编程的时候确实可以从内存管理中释放出来,但这也引发了另外一个大问题,一旦Java应用出现内存泄漏的时候,常常让人措手不及,陷入无从下 ...
- beetl java例子_初识Java模板引擎Beetl之简单示例
以前没用过模板引擎,没去关注过,今天正好看到,简单看了一下,写个简单的示例,记录一下: 首先,Beetl 是 Bee Template language的缩写,是新一代的模板引擎,它功能强大,拥有主流 ...
- 容器云java开发_使用码云构建 Docker 容器镜像并部署到华为云
华为公有云平台的容器镜像服务开放了对码云代码库的支持. 华为云平台的容器镜像服务(SWR),能够支持从源码到镜像.从镜像到应用的容器镜像全生命周期的管理服务,为用户提供简单易用.安全可靠的镜像管理功能 ...
最新文章
- 判断二叉树中两个节点的最低共同父节点
- JavaScript获取样式值的几种方法学习总结
- cocos2d-x 连帧动画实现
- 深入浅出SQL(1)
- 一个漫长的程序人生路程,如何突破5000元大关?几年前的2-3线城市的参考,现在未必...
- Flutter中ListView加载图片数据的优化
- ZED2 ROS下bag包的录制与回放数据
- RocketMQ学习-概览
- android点击不同次切换不同图片,Android实现简单的上一张、下一张图片切换显示...
- 知乎python小项目_python进阶知乎
- 如何制作一份高大上的学术PPT?
- 代理ip填写格式有什么要求?
- java设计模式(1)
- Java校验身份证号码合法性
- 折线(Polyline)、多边形(Polygon)
- telnet协议的Wireshark抓包分析
- Delphi 2010的好消息
- kettle案例四使用java脚本进行数据处理
- 人是什么?从生物学的角度来说,人就是直立行走无毛动物而已...这是一个多么可怕的世界啊
- 快慢指针求环入口问题:
热门文章
- VSCode配置C/C++教程
- 中文在线抢滩文学+ 借IP一体化打响圈地战
- 华硕gl553vw是飞行堡垒几代_信仰也可以亲民:ASUS 华硕 推出 ROG Strix GL553VW 主流级游戏笔记本电脑...
- CL210云应用程序自动化(理论)
- java接口的关键字_Java 接口 interface关键字
- 用草料二维码搭建“出租车服务监督平台”,提升出行服务质量
- linux 安装tgz,python tgz包安装
- 计算机主板电池没电什么情况,事实:如果计算机主板电池没电了怎么办?解决计算机主板电池没电的问题...
- mac python3.* ModuleNotFoundError: No module named 'MySQLdb'
- Linux下可以替代windows的软件汇总:(不断完善中)