응용 프로그램의 다 중 스 레 드 오류 에 대한 분석 에 대한 상세 한 설명

1.수요 와 초기 구현 이 간단 한 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(실제로 이것 은 오픈 SMTP.Net 과 OpenPop 두 프로젝트 의 집합)을 사 용 했 고 인 터 페 이 스 를 호출 하 는 것 은 간단 하 다.코드 를 다 쓴 후에 기본 적 인 기능 은 만족 되 었 고 안정 적 인 기초 위 에서 더욱 빠 르 고 효율 적 인 원칙 에 따라 최종 적 으로 성능 을 향상 시 켰 다.
2.성능 개선 및 발생 BUG 분석 은 잠시 이곳 의 시간 소모 작업 이 계산 집약 형 이 든 IO 밀집 형 이 든 간 에 집합 이 있 는 것 을 보면 순서대로 처리 해 야 하기 때문에 다 중 스 레 드 비동기 병행 작업 의 충동 을 참 을 수 없다.조건 이 있 으 면 이 보 를 최대한 옮 기 고 조건 이 없 으 면 이 보 를 창조 해 야 한다.다 중 스 레 드 장점 을 진정 으로 발휘 하고 서버 의 강력 한 처리 능력 을 충분히 이용 해 야 한다.또한 규정 에 맞 게 다 중 스 레 드 프로그램 을 많이 썼 다 고 자신 한다.이 업무 논 리 는 비교적 간단 하고 이상 한 부분 도 쉽게 통제 할 수 있다(문제 가 있어 도 보상 조치 가 있다.후기 처리 에서 보완 할 수 있 습 니 다)이론 적 으로 매일 받 아야 할 메 일의 수량 도 많 지 않 고 CPU 와 메모리 킬러 가 되 지 않 습 니 다.이런 다 중 스 레 드 비동기 서 비 스 는 받 아들 일 수 있 을 것 입 니 다.그리고 분석 에 따 르 면 이것 은 전형 적 인 빈번 한 인터넷 IO 밀집 형 응용 프로그램 으로 당연히 IO 처리 에 공 을 들 여야 한다.
1.메 일 을 받 는 것 은 Mail.Net 의 예제 코드 에서 볼 수 있 습 니 다.메 일 을 받 으 려 면 1 부터 색인 이 필요 하고 질서 가 있어 야 합 니 다.비동기 로 여러 요청 을 하면 이 색인 은 어떻게 들 어 갑 니까?질서 가 있어 야 한 다 는 것 은 나 로 하여 금 약간 망 설 이게 한다.만약 에 Lock 이나 Interlocked 등 동기 화 구 조 를 통 해 다 중 스 레 드 의 장점 을 잃 게 된다 면 순서 적 으로 동기 화 하 는 속도 가 빠 를 것 이 라 고 생각한다.
분석 은 분석 이 고,우 리 는 코드 를 좀 써 서 효율 이 어떤 지 시험 해 보 자.
비동기 적 인 방법 으로 전체 파 라 메 터 를 전달 하 는 동시에 Interlocked 통 제 를 통 해 메 일 총수 의 변 화 를 추출 합 니 다.모든 비동기 적 인 방법 을 얻 은 후에 Lock 을 통 해 Message 를 listAllMsg 목록 에 추가 하면 됩 니 다.
메 일 서버 테스트 메 일이 많 지 않 습 니 다.테스트 를 통 해 메 일 한두 통 을 얻 을 수 있 습 니 다.네,좋 습 니 다.메 일 을 추출 하 는 데 성공 하면 초보 적 으로 조정 하면 얻 을 수 있 습 니 다.기 쁘 고 축하 할 수 있 습 니 다.
2.메 일 저장 조정 과정 은 다음 과 같 습 니 다.eml 로 옮 겨 다 니 며 저장 하 는 실현 코드 를 다 중 스 레 드 로 바 꾸 고 message.SaveToFile 저장 작업 을 병행 처리 합 니 다.테스트 를 통 해 1~2 통 의 메 일 을 저장 합 니 다.CPU 는 높 은 것 을 보지 못 했 고 저장 효율 이 약간 향상 되 었 으 며 발전 한 것 같 습 니 다.
3.메 일 삭제 재 조정:다 중 스 레 드 저장 작업 을 모방 하여 메 일 을 삭제 하 는 코드 를 수정 하고 다 중 스 레 드 를 통 해 삭제 하 는 작업 도 병행 합 니 다.좋아,좋아,좋아.이 럴 때 내 마음 속 에 무슨 Thread 야,ThreadPool 아,CCR 아,TPL 아,EAP 아,APM 아,내 가 아 는 것 을 다 써 주 고 가장 잘 쓰 는 가장 효율 적 인 것 중 하 나 를 골 라 서 기술적 인 함량 이 있어 보 여.와 하하.
그리고 비동기 삭제 방법 을 빠르게 써 서 테스트 를 시작 했다.우편물 이 많 지 않 은 상황 에서 예 를 들 어 편지 세 통,정상적으로 일 할 수 있어 서 매우 빠 른 것 같다.
여기까지 와 서 큰 성 과 를 축하 할 준 비 를 하고 있 습 니 다.
4.BUG 원인 분석 은 위의 1,2,3 독립 효 과 를 보면 모든 스 레 드 가 서로 통신 하거나 데이터 공 유 를 필요 로 하지 않 고 독립 적 으로 운행 할 수 있 는 것 같 습 니 다.또한 비동기 다 중 스 레 드 기술 을 사용 하여 빠 른 속도 로 저장 하고 빠 른 속도 로 삭제 하 며 메 일 처리 가 가장 좋 은 상태 에 들 어 갈 것 같 습 니 다.그러나 마지막 으로 추출,저장,삭제 통합 연결 테스트.로 그 를 보 는 동안 실행 되 었 습 니 다.비극 이 발생 했 습 니 다.
테스트 메 일이 많 을 때,예 를 들 어 20,30 통 정도,로그 에서 PopServer Exception 이상 이 있 는 것 을 보 았 습 니 다.약간 어 지 러 운 것 같 습 니 다.그리고 매번 어 지 러 운 것 이 다른 것 같 습 니 다.편지 서 너 통 을 더 테스트 해 보 니 정상 적 인 작업 이 가능 할 때 도 있 고,팝 서버 Exception 이상 이 있 는 지,어 지 러 운 코드 가 있 는 지,오류 스 택 이 있 는 지,메 일 을 삭제 하 는 곳 인지 분석 했다.
나 카 오,이게 무슨 짓 이 야?메 일 서버 랑 관계 가 안 돼?왜 자꾸 PopServerException 이 이상해?
설마 비동기 삭제 방법 에 문제 가 있 는 건 아니 겠 지?비동기 삭제,색인 1 의 번호,응,색인 문제?아직 확실 하지 않 습 니 다.
여기까지 다 중 스 레 드 처리 삭제 작업 이 이상 한 이 유 를 발견 할 수 있 습 니까?당신 은 이미 원인 을 알 고 있 습 니까?OK,아래 의 내용 은 너 에 게 아무런 의미 가 없 으 니 아래 를 보지 않 아 도 된다.
나의 조사 경 과 를 이야기 하 다.
로 그 를 보 니 메 일 을 삭제 하 는 방법 에 문제 가 있 는 지 의 심 스 러 웠 지만 눈대중 을 보 니 믿 을 만하 다.이 어 삭제 할 때 메 일 인 코딩 이 정확 하지 않 은 것 으로 추정 되 며,나중에 불가능 하 다 고 생각 했 습 니 다.같은 메 일 동기 화 코드 를 확인 하고 저장 하 며 삭제 하 는 세 가지 작업 은 이상 하 게 던 지지 않 았 습 니 다.마음 이 놓 이지 않 습 니 다.또 몇 번 에 걸 쳐 각각 몇 통 의 메 일 을 테스트 했 습 니 다.첨부 파일 이 있 는 것 은 첨부 파일 이 없 는 것 입 니 다.html 의 일반 텍스트 는 동기 코드 처리 가 잘 되 었 습 니 다.
아무리 생각해 도 이해 가 되 지 않 습 니 다.Mail.NET 소스 코드 를 열 고 DeleteMessage 방법 에서 Mail.Net 의 Pop 3 Client 류 의 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
");

            // 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 프로 그래 밍 을 통 해 POP 3 프로 토 콜 작업 명령 을 실현 하 는 것 이 아 닙 니까?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);
        }

내부 의 GetMessage AsBytes 방법 은 결국 Send Command 방법 으로 갑 니 다.

      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);
            }
제 추적 에 따라 테스트 에서 이상 한 코드 를 던 진 것 은 LastServer Response(This is the last response the server sent back when a command was issued to it)입 니 다.IsOKResponse 방법 에 서 는'+OK'로 시작 하면 PopServer Exception 이상 을 던 지 는 것 이 아 닙 니 다.

    /// <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 순환 을 보면 성능 을 향상 시 키 고 테스트 데이터 가 충분 하지 않 으 며 부주의 로 천둥 을 쳤 다.결국 오류 가 발생 한 이 유 는 스 레 드 안전 에 대한 고려 가 부족 한 비동기 장면 의 선택 이 부당 하기 때문이다.이런 부당 한 사용 은 아직도 많다.비교적 전형 적 인 것 은 바로 데이터 베이스 연결 에 대한 오용 이다.나 는 데이터베이스 연결 대상 의 오용 을 이야기 하 는 글 을 본 적 이 있다.예 를 들 어 이 는 당시 에 나 도 총 결 했 기 때문에 매우 인상적 이 었 다.지금 은 수 다 를 떨 어야 합 니 다.using pop3Client 나 SqlConnection 같은 방식 으로 하나의 연결 로 네트워크 를 방문 하 는 경우 에는 다 중 스 레 드 를 사용 하기에 적합 하지 않 을 수 있 습 니 다.특히 서버 와 밀집 통신 을 할 때 다 중 스 레 드 기술 을 사용 하 더 라 도 성능 이 향상 되 지 않 을 수 있 습 니 다.
저희 가 자주 사용 하 는 Libray 나.NET 클 라 이언 트,예 를 들 어 FastDFS,Memcached,RabbitMQ,Redis,MongDB,Zookeeper 등 은 모두 네트워크 와 서버 통신 을 방문 하고 협 의 를 분석 해 야 합 니 다.몇 개의 클 라 이언 트 의 소스 코드 를 분 석 했 습 니 다.FastDFS,Memcached 와 Redis 의 클 라 이언 트 내부 에 모두 Pool 의 실현 이 있 고 스 레 드 안전 위험 이 없 는 것 으로 기억 합 니 다.개인 적 인 경험 에 따 르 면 그들 을 사용 할 때 경외 심 을 가 져 야 합 니 다.아마도 당신 이 사용 하 는 언어 와 라 이브 러 리 프로 그래 밍 체험 이 매우 우호 적일 것 입 니 다.API 사용 설명 은 통속 적 이 고 알 기 쉬 우 며 호출 하기 가 쉬 워 보이 지만 잘 사용 하 는 것 이 전부 가 아 닙 니 다.소스 코드 를 빨리 이해 하고 대체적으로 생각 을 실현 하 는 것 이 좋 습 니 다.그렇지 않 으 면 내부 실현 원리 에 익숙 하지 않 으 면우리 가 다 중 스 레 드 기술 을 재 구성 하거나 조정 할 때 심각 한 문 제 를 소홀히 해 서 는 안 된다.바로 비동기 처리 에 적합 한 장면 을 깨 닫 는 것 이다.마치 캐 시 장면 을 사용 하기에 적합 하 다 는 것 을 알 고 있 는 것 처럼 나 는 심지어 이 점 이 코드 를 어떻게 쓰 는 것 보다 더 중요 하 다 고 생각한다.그리고 재 구성 이나 조정 은 신중 해 야 한다.테스트 에 의존 하 는 데 이 터 는 반드시 충분 한 준 비 를 해 야 한다.실제 업무 에서 이 점 은 여러 번 증명 되 었 고 저 에 게 깊 은 인상 을 주 었 습 니 다.많은 업무 시스템 의 데이터 양 이 많 지 않 을 때 잘 작 동 할 수 있 지만 동시 다발 데이터 양 이 많은 환경 에서 각종 알 수 없 는 문제 가 발생 하기 쉽다.예 를 들 어 본 고 에서 말 한 바 와 같이 다 중 스 레 드 비동기 로 메 일 을 얻 고 삭제 하 는 것 을 테스트 할 때 메 일 서버 에는 한두 통 의 내용 과 첨부 파일 만 있 고 비동기 로 얻 고 삭제 하 는 것 이 모두 정상적으로 작 동 된다.아무런 이상 로그 도 없 지만 데이터 가 많 으 면 이상 로그,조사,디 버 깅,소스 코드 를 보고 다시 조사 합 니 다.이 글 은 출시 되 었 습 니 다.

좋은 웹페이지 즐겨찾기