Csharp/C#教程:基于一个应用程序多线程误用的分析详解分享

一、需求和初步实现
很简单的一个windows服务:客户端连接邮件服务器,下载邮件(含附件)并保存为.eml格式,保存成功后删除服务器上的邮件。实现的伪代码大致如下:
代码如下:
     publicvoidProcess()
       {
           varrecordCount=1000;//每次取出邮件记录数
           while(true)
           {
               using(varclient=newPop3Client())
               {
                   //1、建立连接,并进行身份认证
                   client.Connect(server,port,useSSL);
                   client.Authenticate(userName,pwd);

                   varmessageCount=client.GetMessageCount();//邮箱中现有邮件数
                   if(messageCount>recordCount)
                   {
                       messageCount=recordCount;
                   }
                   if(messageCount<1)
                   {
                       break;
                   }
                   varlistAllMsg=newList<Message>(messageCount);//用于临时保存取出的邮件

                   //2、取出邮件后填充至列表,每次最多recordCount封邮件
                   for(inti=1;i<=messageCount;i++)//邮箱索引是基于1开始的,索引范围:[1,messageCount]
                   {
                       listAllMsg.Add(client.GetMessage(i));//取出邮件至列表
                   }

                   //3、遍历并保存至客户端,格式为.eml
                   foreach(varmessageinlistAllMsg)
                   {
                       varemlInfo=newSystem.IO.FileInfo(string.Format(“{0}.eml”,Guid.NewGuid().ToString(“n”)));
                       message.SaveToFile(emlInfo);//保存邮件为.eml格式文件
                   }

                   //4、遍历并删除
                   intmessageNumber=1;
                   foreach(varmessageinlistAllMsg)
                   {
                       client.DeleteMessage(messageNumber);//删除邮件(本质上,在关闭连接前只是打上DELETE标签,并没有真正删除)
                       messageNumber++;
                   }

                   //5、断开连接,真正完成删除
                   client.Disconnect();

                   if(messageCount<recordCount)
                   {
                       break;
                   }
               }
           }
       }

开发中接收邮件的时候使用了开源组件Mail.Net(实际上这是OpenSMTP.Net和OpenPop两个项目的并集),调用接口实现很简单。代码写完后发现基本功能是满足了,本着在稳定的基础上更快更有效率的原则,最终进行性能调优。

二、性能调优及产生BUG分析
暂时不管这里的耗时操作是属于计算密集型还是IO密集型,反正有人一看到有集合要一个一个遍历顺序处理,就忍不住有多线程异步并行操作的冲动。有条件异步尽量异步,没有条件异步,创造条件也要异步,真正发挥多线程优势,充分利用服务器的强大处理能力,而且也自信中规中矩写了很多多线程程序,这个业务逻辑比较简单而且异常处理也较容易控制(就算有问题也有补偿措施,可以在后期处理中完善它),理论上每天需要查收的邮件的数量也不会太多,不会长时间成为CPU和内存杀手,这样的多线程异步服务实现应该可以接受。而且根据分析,显而易见,这是一个典型的频繁访问网络IO密集型的应用程序,当然要从IO处理上下功夫。

1、收取邮件
从Mail.Net的示例代码中看到,取邮件需要一个从1开始的索引,而且必须有序。如果异步发起多个请求,这个索引怎么传入呢?必须有序这一条开始让我有点犹豫,如果通过Lock或者Interlocked等同步构造,很显然就失去了多线程的优势,我猜可能还不如顺序同步获取速度快。

分析归分析,我们还是写点代码试试看效率如何。

快速写个异步方法传递整型参数,同时通过Interlocked控制提取邮件总数的变化,每一个异步方法获取完了之后通过Lock将Message加入到listAllMsg列表中即可。

邮件服务器测试邮件不多,测试获取一两封邮件,嗯,很好,提取邮件成功,初步调整就有收获,可喜可贺。

2、保存邮件
调优过程是这样的:遍历并保存为.eml的实现代码改为使用多线程,将message.SaveToFile保存操作并行处理,经测试,保存一到两封邮件,CPU没看出高多少,保存的效率貌似稍有提升,又有点进步。

3、删除邮件
再次调优:仿照多线程保存操作,将遍历删除邮件的代码进行修改,也通过多线程并行处理删除的操作。好,很好,非常好,这时候我心里想着什么Thread啊,ThreadPool啊,CCR啊,TPL啊,EAP啊,APM啊,把自己知道的能用的全给它用一遍,挑最好用的最优效率的一个,显得很有技术含量,哇哈哈。

然后,快速写了个异步删除方法开始测试。在邮件不多的情况下,比如三两封信,能正常工作,看起来好像蛮快的。

到这里我心里已经开始准备庆祝大功告成了。

4、产生BUG原因分析
从上面的1、2、3独立效果看,似乎每一个线程都能够独立运行而不需要相互通信或者数据共享,而且使用了异步多线程技术,取的快存的快删的也快,看上去邮件处理将进入最佳状态。但是最后提取、保存、删除集成联调测试。运行了一段时间查看日志,悲剧发生了:

在测试邮件较多的时候,比如二三十封左右,日志里看到有PopServerException异常,好像还有点乱码,而且每次乱码好像还不一样;再测试三两封信,发现有时能正常工作,有时也抛出PopServerException异常,还是有乱码,分析出错堆栈,是在删除邮件的地方。

我kao,这是要闹哪样啊,和邮件服务器关系没搞好吗,怎么总是PopServerException异常?

难道,难道是异步删除方法有问题?异步删除,索引为1的序号,嗯,索引的问题?还是不太确定。

到这里你能发现多线程处理删除操作抛出异常的原因吗?你已经知道原因了?OK,下面的内容对你就毫无意义了,可以不用往下看了。

谈谈我的排查经过。

看日志我初步怀疑是删除邮件的方法有问题,但是看了一下目测还是可靠的。接着估计是删除时邮件编码不正确,后来又想不太可能,同样的邮件同步代码查收保存删除这三个操作就没有异常抛出。不太放心,又分几次分别测试了几封邮件,有附件的没附件的,html的纯文本的,同步代码处理的很好。

百思不得其解,打开Mail.NET源码,从DeleteMessage方法跟踪查看到Mail.Net的Pop3Client类中的SendCommand方法,一下子感觉有头绪了。DeleteMessage删除邮件的源码如下:
代码如下:
       publicvoidDeleteMessage(intmessageNumber)
       {
           AssertDisposed();

           ValidateMessageNumber(messageNumber);

           if(State!=ConnectionState.Transaction)
               thrownewInvalidUseException(“Youcannotdeleteanymessageswithoutauthenticatingyourselftowardstheserverfirst”);

           SendCommand(“DELE”+messageNumber);
       }

最后一行SendCommand需要提交一个DELE命令,跟进去看看它是怎么实现的:
代码如下:
       privatevoidSendCommand(stringcommand)
       {
           //ConvertthecommandwithCRLFafterwardsasperRFCtoabytearraywhichwecanwrite
           byte[]commandBytes=Encoding.ASCII.GetBytes(command+”rn”);

           //Writethecommandtotheserver
           OutputStream.Write(commandBytes,0,commandBytes.Length);
           OutputStream.Flush();//Flushthecontentaswenowwaitforaresponse

           //Readtheresponsefromtheserver.TheresponseshouldbeinASCII
           LastServerResponse=StreamUtility.ReadLineAsAscii(InputStream);

           IsOkResponse(LastServerResponse);
       }

注意InputStream和OutputStream属性,它们的定义如下(神奇的private修饰属性,这种写法少见哪):
代码如下:
  ///<summary>
       ///Thisisthestreamusedtoreadofftheserverresponsetoacommand
       ///</summary>
       privateStreamInputStream{get;set;}

       ///<summary>
       ///Thisisthestreamusedtowritecommandstotheserver
       ///</summary>
       privateStreamOutputStream{get;set;}

给它赋值的地方是调用Pop3Client类里的publicvoidConnect(StreaminputStream,StreamoutputStream)方法,而这个Connect方法最终调用的Connect方法如下:
代码如下:
     ///<summary>
       ///ConnectstoaremotePOP3server
       ///</summary>
       ///<paramname=”hostname”>The<paramrefname=”hostname”/>ofthePOP3server</param>
       ///<paramname=”port”>TheportofthePOP3server</param>
       ///<paramname=”useSsl”>TrueifSSLshouldbeused.FalseifplainTCPshouldbeused.</param>
       ///<paramname=”receiveTimeout”>Timeoutinmillisecondsbeforeasocketshouldtimeoutfromreading.Setto0or-1tospecifyinfinitetimeout.</param>
       ///<paramname=”sendTimeout”>Timeoutinmillisecondsbeforeasocketshouldtimeoutfromsending.Setto0or-1tospecifyinfinitetimeout.</param>
       ///<paramname=”certificateValidator”>IfyouwanttovalidatethecertificateinaSSLconnection,passareferencetoyourvalidator.Supply<seelangword=”null”/>ifdefaultshouldbeused.</param>
       ///<exceptioncref=”PopServerNotAvailableException”>IftheserverdidnotsendanOKmessagewhenaconnectionwasestablished</exception>
       ///<exceptioncref=”PopServerNotFoundException”>Ifitwasnotpossibletoconnecttotheserver</exception>
       ///<exceptioncref=”ArgumentNullException”>If<paramrefname=”hostname”/>is<seelangword=”null”/></exception>
       ///<exceptioncref=”ArgumentOutOfRangeException”>Ifportisnotintherange[<seecref=”IPEndPoint.MinPort”/>,<seecref=”IPEndPoint.MaxPort”/>orifanyofthetimeoutsislessthan-1.</exception>
       publicvoidConnect(stringhostname,intport,booluseSsl,intreceiveTimeout,intsendTimeout,RemoteCertificateValidationCallbackcertificateValidator)
       {
           AssertDisposed();

           if(hostname==null)
               thrownewArgumentNullException(“hostname”);

           if(hostname.Length==0)
               thrownewArgumentException(“hostnamecannotbeempty”,”hostname”);

           if(port>IPEndPoint.MaxPort||port<IPEndPoint.MinPort)
               thrownewArgumentOutOfRangeException(“port”);

           if(receiveTimeout<-1)
               thrownewArgumentOutOfRangeException(“receiveTimeout”);

           if(sendTimeout<-1)
               thrownewArgumentOutOfRangeException(“sendTimeout”);

           if(State!=ConnectionState.Disconnected)
               thrownewInvalidUseException(“YoucannotasktoconnecttoaPOP3server,whenwearealreadyconnectedtoone.Disconnectfirst.”);

           TcpClientclientSocket=newTcpClient();
           clientSocket.ReceiveTimeout=receiveTimeout;
           clientSocket.SendTimeout=sendTimeout;

           try
           {
               clientSocket.Connect(hostname,port);
           }
           catch(SocketExceptione)
           {
               //Closethesocket-wearenotconnected,sononeedtoclosestreamunderneath
               clientSocket.Close();

               DefaultLogger.Log.LogError(“Connect():”+e.Message);
               thrownewPopServerNotFoundException(“Servernotfound”,e);
           }

           Streamstream;
           if(useSsl)
           {
               //IfwewanttouseSSL,openanewSSLStreamontopoftheopenTCPstream.
               //WealsowanttoclosetheTCPstreamwhentheSSLstreamisclosed
               //Ifavalidatorwaspassedtous,useit.
               SslStreamsslStream;
               if(certificateValidator==null)
               {
                   sslStream=newSslStream(clientSocket.GetStream(),false);
               }
               else
               {
                   sslStream=newSslStream(clientSocket.GetStream(),false,certificateValidator);
               }
               sslStream.ReadTimeout=receiveTimeout;
               sslStream.WriteTimeout=sendTimeout;

               //Authenticatetheserver
               sslStream.AuthenticateAsClient(hostname);

               stream=sslStream;
           }
           else
           {
               //IfwedonotwanttouseSSL,useplainTCP
               stream=clientSocket.GetStream();
           }

           //Nowdotheconnectwiththesamestreambeingusedtoreadandwriteto
           Connect(stream,stream);//In/OutputStream属性初始化
       }

一下子看到了TcpClient对象,这个不就是基于Socket,通过Socket编程实现POP3协议操作指令吗?毫无疑问需要发起TCP连接,什么三次握手呀,发送命令操作服务器呀…一下子全想起来了。

我们知道一个TCP连接就是一个会话(Session),发送命令(比如获取和删除)需要通过TCP连接和邮件服务器通信。如果是多线程在一个会话上发送命令(比如获取(TOP或者RETR)、删除(DELE))操作服务器,这些命令的操作都不是线程安全的,这样很可能出现OutputStream和InputStream数据不匹配而相互打架的情况,这个很可能就是我们看到的日志里有乱码的原因。说到线程安全,突然恍然大悟,我觉得查收邮件应该也有问题。为了验证我的想法,我又查看了下GetMessage方法的源码:
代码如下:

       publicMessageGetMessage(intmessageNumber)
       {
           AssertDisposed();

           ValidateMessageNumber(messageNumber);

           if(State!=ConnectionState.Transaction)
               thrownewInvalidUseException(“Cannotfetchamessage,whentheuserhasnotbeenauthenticatedyet”);

           byte[]messageContent=GetMessageAsBytes(messageNumber);

           returnnewMessage(messageContent);
       }

内部的GetMessageAsBytes方法最终果然还是走SendCommand方法:
代码如下:
     if(askOnlyForHeaders)
           {
               //0isthenumberoflinesofthemessagebodytofetch,thereforeitissettozerotofetchonlyheaders
               SendCommand(“TOP”+messageNumber+”0”);
           }
           else
           {
               //Askforthefullmessage
               SendCommand(“RETR”+messageNumber);
           }

根据我的跟踪,在测试中抛出异常的乱码来自于LastServerResponse(Thisisthelastresponsetheserversentbackwhenacommandwasissuedtoit),在IsOKResponse方法中它不是以“+OK”开头就会抛出PopServerException异常:
代码如下:
   ///<summary>
       ///Testsastringtoseeifitisa”+OK”string.<br/>
       ///An”+OK”stringshouldbereturnedbyacompliantPOP3
       ///serveriftherequestcouldbeserved.<br/>
       ///<br/>
       ///Themethoddoesonlycheckifitstartswith”+OK”.
       ///</summary>
       ///<paramname=”response”>Thestringtoexamine</param>
       ///<exceptioncref=”PopServerException”>Thrownifserverdidnotrespondwith”+OK”message</exception>
       privatestaticvoidIsOkResponse(stringresponse)
       {
           if(response==null)
               thrownewPopServerException(“Thestreamusedtoretrieveresponsesfromwasclosed”);

           if(response.StartsWith(“+OK”,StringComparison.OrdinalIgnoreCase))
               return;

           thrownewPopServerException(“Theserverdidnotrespondwitha+OKresponse.Theresponsewas:””+response+”””);
       }

分析到这里,终于知道最大的陷阱是Pop3Client不是线程安全的。终于找到原因了,哈哈哈,此刻我犹如见到女神出现一样异常兴奋心花怒放,高兴的差点忘了错误的代码就是自己写的。

片刻后终于冷静下来,反省自己犯了很低级的失误,晕死,我怎么把TCP和线程安全这茬给忘了呢?啊啊啊啊啊啊,好累,感觉再也不会用类库了。

对了,保存为.eml的时候是通过Message对象的SaveToFile方法,并不需要和邮件服务器通信,所以异步保存没有出现异常(二进制数组RawMessage也不会数据不匹配),它的源码是下面这样的:
代码如下:
     ///<summary>
       ///Savethis<seecref=”Message”/>toafile.<br/>
       ///<br/>
       ///Canbeloadedatalatertimeusingthe<seecref=”LoadFromFile”/>method.
       ///</summary>
       ///<paramname=”file”>TheFilelocationtosavethe<seecref=”Message”/>to.Existentfileswillbeoverwritten.</param>
       ///<exceptioncref=”ArgumentNullException”>If<paramrefname=”file”/>is<seelangword=”null”/></exception>
       ///<exception>Otherexceptionsrelevanttofilesavingmightbethrownaswell</exception>
       publicvoidSaveToFile(FileInfofile)
       {
           if(file==null)
               thrownewArgumentNullException(“file”);

           File.WriteAllBytes(file.FullName,RawMessage);
       }

再来上述就是C#学习教程:基于一个应用程序多线程误用的分析详解分享的全部内容,如果对大家有所用处且需要了解更多关于C#学习教程,希望大家多多关注—计算机技术网(www.ctvol.com)!

本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。

ctvol管理联系方式QQ:251552304

本文章地址:https://www.ctvol.com/cdevelopment/904673.html

(0)
上一篇 2021年10月22日
下一篇 2021年10月22日

精彩推荐