亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > C# > 正文

基于一個應用程序多線程誤用的分析詳解

2020-01-24 03:21:44
字體:
來源:轉載
供稿:網友

一、需求和初步實現
很簡單的一個windows服務:客戶端連接郵件服務器,下載郵件(含附件)并保存為.eml格式,保存成功后刪除服務器上的郵件。實現的偽代碼大致如下:

復制代碼 代碼如下:

      public void Process()
        {
            var recordCount = 1000;//每次取出郵件記錄數
            while (true)
            {
                using (var client = new Pop3Client())
                {
                    //1、建立連接,并進行身份認證
                    client.Connect(server, port, useSSL);
                    client.Authenticate(userName, pwd);

                    var messageCount = client.GetMessageCount(); // 郵箱中現有郵件數
                    if (messageCount > recordCount)
                    {
                        messageCount = recordCount;
                    }
                    if (messageCount < 1)
                    {
                        break;
                    }
                    var listAllMsg = new List<Message>(messageCount); //用于臨時保存取出的郵件

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

                    //3、遍歷并保存至客戶端,格式為.eml
                    foreach (var message in listAllMsg)
                    {
                        var emlInfo = new System.IO.FileInfo(string.Format("{0}.eml", Guid.NewGuid().ToString("n")));
                        message.SaveToFile(emlInfo);//保存郵件為.eml格式文件
                    }

                    //4、遍歷并刪除
                    int messageNumber = 1;
                    foreach (var message in listAllMsg)
                    {
                        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刪除郵件的源碼如下:

復制代碼 代碼如下:

        public void DeleteMessage(int messageNumber)
        {
            AssertDisposed();

            ValidateMessageNumber(messageNumber);

            if (State != ConnectionState.Transaction)
                throw new InvalidUseException("You cannot delete any messages without authenticating yourself towards the server first");

            SendCommand("DELE " + messageNumber);
        }


最后一行SendCommand需要提交一個DELE命令,跟進去看看它是怎么實現的:
復制代碼 代碼如下:

        private void SendCommand(string command)
        {
            // Convert the command with CRLF afterwards as per RFC to a byte array which we can write
            byte[] commandBytes = Encoding.ASCII.GetBytes(command + "/r/n");

            // Write the command to the server
            OutputStream.Write(commandBytes, 0, commandBytes.Length);
            OutputStream.Flush(); // Flush the content as we now wait for a response

            // Read the response from the server. The response should be in ASCII
            LastServerResponse = StreamUtility.ReadLineAsAscii(InputStream);

            IsOkResponse(LastServerResponse);
        }


注意InputStream和OutputStream屬性,它們的定義如下(神奇的private修飾屬性,這種寫法少見哪):
復制代碼 代碼如下:

   /// <summary>
        /// This is the stream used to read off the server response to a command
        /// </summary>
        private Stream InputStream { get; set; }

        /// <summary>
        /// This is the stream used to write commands to the server
        /// </summary>
        private Stream OutputStream { get; set; }


給它賦值的地方是調用Pop3Client類里的 public void Connect(Stream inputStream, Stream outputStream)方法,而這個Connect方法最終調用的Connect方法如下:
復制代碼 代碼如下:

      /// <summary>
        /// Connects to a remote POP3 server
        /// </summary>
        /// <param name="hostname">The <paramref name="hostname"/> of the POP3 server</param>
        /// <param name="port">The port of the POP3 server</param>
        /// <param name="useSsl">True if SSL should be used. False if plain TCP should be used.</param>
        /// <param name="receiveTimeout">Timeout in milliseconds before a socket should time out from reading. Set to 0 or -1 to specify infinite timeout.</param>
        /// <param name="sendTimeout">Timeout in milliseconds before a socket should time out from sending. Set to 0 or -1 to specify infinite timeout.</param>
        /// <param name="certificateValidator">If you want to validate the certificate in a SSL connection, pass a reference to your validator. Supply <see langword="null"/> if default should be used.</param>
        /// <exception cref="PopServerNotAvailableException">If the server did not send an OK message when a connection was established</exception>
        /// <exception cref="PopServerNotFoundException">If it was not possible to connect to the server</exception>
        /// <exception cref="ArgumentNullException">If <paramref name="hostname"/> is <see langword="null"/></exception>
        /// <exception cref="ArgumentOutOfRangeException">If port is not in the range [<see cref="IPEndPoint.MinPort"/>, <see cref="IPEndPoint.MaxPort"/> or if any of the timeouts is less than -1.</exception>
        public void Connect(string hostname, int port, bool useSsl, int receiveTimeout, int sendTimeout, RemoteCertificateValidationCallback certificateValidator)
        {
            AssertDisposed();

            if (hostname == null)
                throw new ArgumentNullException("hostname");

            if (hostname.Length == 0)
                throw new ArgumentException("hostname cannot be empty", "hostname");

            if (port > IPEndPoint.MaxPort || port < IPEndPoint.MinPort)
                throw new ArgumentOutOfRangeException("port");

            if (receiveTimeout < -1)
                throw new ArgumentOutOfRangeException("receiveTimeout");

            if (sendTimeout < -1)
                throw new ArgumentOutOfRangeException("sendTimeout");

            if (State != ConnectionState.Disconnected)
                throw new InvalidUseException("You cannot ask to connect to a POP3 server, when we are already connected to one. Disconnect first.");

            TcpClient clientSocket = new TcpClient();
            clientSocket.ReceiveTimeout = receiveTimeout;
            clientSocket.SendTimeout = sendTimeout;

            try
            {
                clientSocket.Connect(hostname, port);
            }
            catch (SocketException e)
            {
                // Close the socket - we are not connected, so no need to close stream underneath
                clientSocket.Close();

                DefaultLogger.Log.LogError("Connect(): " + e.Message);
                throw new PopServerNotFoundException("Server not found", e);
            }

            Stream stream;
            if (useSsl)
            {
                // If we want to use SSL, open a new SSLStream on top of the open TCP stream.
                // We also want to close the TCP stream when the SSL stream is closed
                // If a validator was passed to us, use it.
                SslStream sslStream;
                if (certificateValidator == null)
                {
                    sslStream = new SslStream(clientSocket.GetStream(), false);
                }
                else
                {
                    sslStream = new SslStream(clientSocket.GetStream(), false, certificateValidator);
                }
                sslStream.ReadTimeout = receiveTimeout;
                sslStream.WriteTimeout = sendTimeout;

                // Authenticate the server
                sslStream.AuthenticateAsClient(hostname);

                stream = sslStream;
            }
            else
            {
                // If we do not want to use SSL, use plain TCP
                stream = clientSocket.GetStream();
            }

            // Now do the connect with the same stream being used to read and write to
            Connect(stream, stream); //In/OutputStream屬性初始化
        }


一下子看到了TcpClient對象,這個不就是基于Socket,通過Socket編程實現POP3協議操作指令嗎?毫無疑問需要發起TCP連接,什么三次握手呀,發送命令操作服務器呀…一下子全想起來了。

我們知道一個TCP連接就是一個會話(Session),發送命令(比如獲取和刪除)需要通過TCP連接和郵件服務器通信。如果是多線程在一個會話上發送命令(比如獲取(TOP或者RETR)、刪除(DELE))操作服務器,這些命令的操作都不是線程安全的,這樣很可能出現OutputStream和InputStream數據不匹配而相互打架的情況,這個很可能就是我們看到的日志里有亂碼的原因。說到線程安全,突然恍然大悟,我覺得查收郵件應該也有問題。為了驗證我的想法,我又查看了下GetMessage方法的源碼:

復制代碼 代碼如下:

        public Message GetMessage(int messageNumber)
        {
            AssertDisposed();

            ValidateMessageNumber(messageNumber);

            if (State != ConnectionState.Transaction)
                throw new InvalidUseException("Cannot fetch a message, when the user has not been authenticated yet");

            byte[] messageContent = GetMessageAsBytes(messageNumber);

            return new Message(messageContent);
        }


內部的GetMessageAsBytes方法最終果然還是走SendCommand方法:
復制代碼 代碼如下:

      if (askOnlyForHeaders)
            {
                // 0 is the number of lines of the message body to fetch, therefore it is set to zero to fetch only headers
                SendCommand("TOP " + messageNumber + " 0");
            }
            else
            {
                // Ask for the full message
                SendCommand("RETR " + messageNumber);
            }

根據我的跟蹤,在測試中拋出異常的亂碼來自于LastServerResponse(This is the last response the server sent back when a command was issued to it),在IsOKResponse方法中它不是以“+OK”開頭就會拋出PopServerException異常:
復制代碼 代碼如下:

    /// <summary>
        /// Tests a string to see if it is a "+OK" string.<br/>
        /// An "+OK" string should be returned by a compliant POP3
        /// server if the request could be served.<br/>
        /// <br/>
        /// The method does only check if it starts with "+OK".
        /// </summary>
        /// <param name="response">The string to examine</param>
        /// <exception cref="PopServerException">Thrown if server did not respond with "+OK" message</exception>
        private static void IsOkResponse(string response)
        {
            if (response == null)
                throw new PopServerException("The stream used to retrieve responses from was closed");

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

            throw new PopServerException("The server did not respond with a +OK response. The response was: /"" + response + "/"");
        }


分析到這里,終于知道最大的陷阱是Pop3Client不是線程安全的。終于找到原因了,哈哈哈,此刻我猶如見到女神出現一樣異常興奮心花怒放,高興的差點忘了錯誤的代碼就是自己寫的。

片刻后終于冷靜下來,反省自己犯了很低級的失誤,暈死,我怎么把TCP和線程安全這茬給忘了呢?啊啊啊啊啊啊,好累,感覺再也不會用類庫了。

對了,保存為.eml的時候是通過Message對象的SaveToFile方法,并不需要和郵件服務器通信,所以異步保存沒有出現異常(二進制數組RawMessage也不會數據不匹配),它的源碼是下面這樣的:

復制代碼 代碼如下:

      /// <summary>
        /// Save this <see cref="Message"/> to a file.<br/>
        /// <br/>
        /// Can be loaded at a later time using the <see cref="LoadFromFile"/> method.
        /// </summary>
        /// <param name="file">The File location to save the <see cref="Message"/> to. Existent files will be overwritten.</param>
        /// <exception cref="ArgumentNullException">If <paramref name="file"/> is <see langword="null"/></exception>
        /// <exception>Other exceptions relevant to file saving might be thrown as well</exception>
        public void SaveToFile(FileInfo file)
        {
            if (file == null)
                throw new ArgumentNullException("file");

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


再來總結看看這個bug是怎么產生的:對TCP和線程安全沒有保持足夠的敏感和警惕,看見for循環就進行性能調優,測試數據不充分,不小心觸雷。歸根結底,產生錯誤的原因是對線程安全考慮不周異步場景選擇不當,這種不當的使用還有很多,比較典型的就是對數據庫連接的誤用。我看過一篇講數據庫連接對象誤用的文章,比如這一篇《解析為何要關閉數據庫連接,可不可以不關閉的問題詳解》,當時我也總結過,所以很有印象?,F在還是要羅嗦一下,對于using一個Pop3Client或者SqlConnection這種方式共用一個連接訪問網絡的情況可能不適合使用多線程,尤其是和服務器進行密集通信的時候,哪怕用對了多線程技術,性能也不見得有提升。

我們經常使用的一些Libray或者.NET客戶端,比如FastDFS、Memcached、RabbitMQ、Redis、MongDB、Zookeeper等等,它們都要訪問網絡和服務器通信并解析協議,分析過幾個客戶端的源碼,記得FastDFS,Memcached及Redis的客戶端內部都有一個Pool的實現,印象中它們就沒有線程安全風險。依個人經驗,使用它們的時候必須保持敬畏之心,也許你用的語言和類庫編程體驗非常友好,API使用說明通俗易懂,調用起來看上去輕而易舉,但是要用好用對也不是全部都那么容易,最好快速過一遍源碼理解大致實現思路,否則如不熟悉內部實現原理埋頭拿過來即用很可能掉入陷阱當中而不自知。當我們重構或調優使用多線程技術的時候,絕不能忽視一個深刻的問題,就是要清醒認識到適合異步處理的場景,就像知道適合使用緩存場景一樣,我甚至認為明白這一點比怎么寫代碼更重要。還有就是重構或調優必須要謹慎,測試所依賴的數據必須準備充分,實際工作當中這一點已經被多次證明,給我的印象尤其深刻。很多業務系統數據量不大的時候都可以運行良好,但在高并發數據量較大的環境下很容易出現各種各樣莫名其妙的問題,比如本文中所述,在測試多線程異步獲取和刪除郵件的時候,郵件服務器上只有一兩封內容和附件很小的郵件,通過異步獲取和刪除都正常運行,沒有任何異常日志,但是數據一多,出現異常日志,排查,調試,看源碼,再排查......這篇文章就面世了。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
成人免费视频在线观看超级碰| 亚洲国内高清视频| 久久久久国产精品一区| 亚洲r级在线观看| 亚洲网站在线看| 国产精品中文字幕在线| 亚洲国产精久久久久久久| 欧美国产日本高清在线| 国产精品激情自拍| 日韩成人av在线播放| 欧美一级bbbbb性bbbb喷潮片| 精品久久久久久中文字幕大豆网| 欧美激情亚洲视频| 九九热这里只有精品免费看| 亚洲少妇激情视频| 欧美大片在线看免费观看| 亚洲第一区第二区| 精品久久久久久久久久国产| 欧美性xxxxhd| 97国产真实伦对白精彩视频8| 国产区精品在线观看| 久久久伊人日本| 久久中文字幕视频| 久久精品国产清自在天天线| 亚洲成人精品久久久| 亚洲成人999| 国产精品久久久久77777| 久久久精品影院| 亚洲欧美国产日韩中文字幕| 国产精品偷伦视频免费观看国产| 正在播放欧美一区| 51色欧美片视频在线观看| 亚州av一区二区| 精品视频久久久久久久| 美女撒尿一区二区三区| 亚洲一区www| 亚洲国产高潮在线观看| 国产精品福利在线观看| 亚洲老板91色精品久久| 久久国产精品久久国产精品| 国产精品一区二区久久久久| 在线播放国产一区中文字幕剧情欧美| 久热在线中文字幕色999舞| 狠狠久久亚洲欧美专区| 亚洲国产私拍精品国模在线观看| 日韩欧美国产一区二区| 国产一区二区三区视频| 欧美激情xxxx性bbbb| 欧美另类极品videosbestfree| 国产视频精品在线| 国产精品白丝av嫩草影院| 国产亚洲欧美日韩美女| 一区二区三区国产在线观看| 51精品国产黑色丝袜高跟鞋| 一区二区欧美日韩视频| 日av在线播放中文不卡| 91久久精品国产91久久性色| 国产精品美乳在线观看| 久久久之久亚州精品露出| 欧美日韩国产二区| 欧美老女人xx| 欧美视频精品一区| 成人欧美一区二区三区黑人| 中日韩美女免费视频网址在线观看| 亚洲精品91美女久久久久久久| 久久久电影免费观看完整版| 性视频1819p久久| 亚洲午夜色婷婷在线| 尤物yw午夜国产精品视频| 国产精品亚洲片夜色在线| 日韩美女在线播放| 精品久久在线播放| 国产偷国产偷亚洲清高网站| 97国产一区二区精品久久呦| 亚洲精品视频免费在线观看| 欧美性猛交xxx| 欧美成人手机在线| 欧美日韩成人在线播放| 亚洲全黄一级网站| 成人精品久久一区二区三区| 成人性生交大片免费看视频直播| 色偷偷av亚洲男人的天堂| 国产精品美女主播在线观看纯欲| 国产亚洲精品久久久久久| 欧美成人亚洲成人日韩成人| 欧美猛交ⅹxxx乱大交视频| 国产成人免费av| 国产精品99久久久久久白浆小说| 欧美—级高清免费播放| 国产综合福利在线| 日韩精品免费在线视频| 清纯唯美亚洲激情| 中文字幕日本欧美| 国产精品国产福利国产秒拍| 精品调教chinesegay| 欧美贵妇videos办公室| 国产中文日韩欧美| 久久av资源网站| 色综合视频一区中文字幕| 久久频这里精品99香蕉| 国产激情综合五月久久| 亚洲黄在线观看| 国产精品视频免费观看www| 日韩极品精品视频免费观看| 亚洲国产一区二区三区在线观看| 欧美日韩国产第一页| 欧美自拍视频在线观看| 日韩a**站在线观看| 亚洲一区二区福利| 91久久精品日日躁夜夜躁国产| 久久久久久久国产精品| 久久av红桃一区二区小说| www.亚洲男人天堂| 日韩电影中文字幕在线| 日韩亚洲在线观看| 欧美亚洲成人精品| 国产精品香蕉国产| 久久伊人免费视频| 国内精品久久久久| 国产欧洲精品视频| 国产丝袜一区视频在线观看| 国产va免费精品高清在线观看| 性色av一区二区三区| 久久久久久国产精品三级玉女聊斋| 日本中文字幕不卡免费| 国产精选久久久久久| 精品国产精品三级精品av网址| 国产精品成人免费视频| 久久亚洲精品网站| 欧美日韩高清在线观看| 欧美人交a欧美精品| 久热国产精品视频| 国产视频亚洲视频| 欧美高清视频在线| 国产亚洲a∨片在线观看| 亚洲激情自拍图| 欧美性视频在线| 久久男人的天堂| 国产视频一区在线| 2019中文字幕在线观看| 97精品一区二区三区| 日韩成人中文字幕在线观看| 欧美高清视频一区二区| 亚洲激情视频网站| 欧美亚洲国产日本| 色噜噜久久综合伊人一本| 亚洲女同精品视频| 欧美午夜宅男影院在线观看| 国产在线播放不卡| 久久久精品2019中文字幕神马| 国产精品黄页免费高清在线观看| 欧美精品电影在线| 国产精品扒开腿做爽爽爽视频| 国产999视频| 国产精品福利小视频| 国产成人精品a视频一区www| 久久久中精品2020中文| 精品福利樱桃av导航| 91在线免费看网站| 91精品国产九九九久久久亚洲| 97国产精品久久| 中文字幕av一区| 久久人人97超碰精品888| 成人激情视频在线观看|