С# Высокая загрузка ЦП в потоке прослушивателя, спящий режим пропускает отключение

Мой обработчик подключения ниже (это больше для личных экспериментов, чем для производственного кода)

Если я не добавляю Thread.Sleep где-либо в цикле while, он начинает высасывать ЦП. И наоборот, если я делаю Sleep, чтобы облегчить бесконечный спам во время, я пропускаю отключение.. ЦП увеличивается прямо пропорционально к количеству запущенных клиентов/потоков, так что это не сам прослушиватель вызывает высокое использование, а фактический клиентский поток, опубликованный ниже. У кого-нибудь есть идеи о том, как решить эту проблему?

(Я избегаю решений, основанных на ожидании, так как я недостаточно знаком с async/await, а многопоточный метод отлично работает для этого довольно небольшого проекта)

Я только кратко обыскал SO в поисках решения и не заметил ничего, что было бы этой конкретной проблемой или предоставляло решение, кроме направления людей к статьям async/await, так что извините, если я пропустил применимый ответ.

        private void HandleConnection(CancellationToken ct) {
        int recv = 0;
        byte[] buf = new byte[4096];
        Trace.WriteLine($"{_name} Connected");
        if (_ns.CanWrite && _client.Connected) {
            _ns.Write(Encoding.BigEndianUnicode.GetBytes("■WEL"), 0, Encoding.BigEndianUnicode.GetBytes("■WEL").Length);
            try {
                while (_client.Connected && !ct.IsCancellationRequested) {

                    while (!_ns.DataAvailable) { //first attempted solution
                        Thread.Sleep(100); // miss discon if i sleep here
                        }

                    if (ct.IsCancellationRequested) {
                        Trace.WriteLine($"{(string)this} thread aborting");
                        break;
                        }

                    buf = new byte[4096];

                    if (_client.Connected && _ns.DataAvailable) {

                        recv = _ns.Read(buf, 0, buf.Length);
                        } else {
                        recv = 0;
                        }

                    if (recv > 0) {

                        string r = Encoding.BigEndianUnicode.GetString(buf);
                        r = r.TrimEnd('\0');
                        if (String.IsNullOrEmpty(r) || String.IsNullOrWhiteSpace(r))
                            r = null; //need the !not version
                        else
                            if (ParseMsg(r))
                                break;
                        }

                    //Thread.Sleep(100); // also miss discon here too

                    }
                } catch (IOException ioe) { }
            Trace.WriteLine($"{_name} Disconnected");
            if (OnDisconnected != null)
                OnDisconnected(this);
            }
        }

person MisterNad    schedule 10.04.2017    source источник
comment
Вам следует рассмотреть возможность использования async/await вариантов   -  person MickyD    schedule 10.04.2017
comment
Что ты имеешь в виду, ты скучаешь по разъединению? Вы говорите, что клиент все еще подключен после отключения, и вы застряли в первом цикле while?   -  person TyCobb    schedule 10.04.2017
comment
@TyCobb Да. Под отсутствием отключения я подразумеваю, что удаленное соединение отправляет ■DSC и разрывается, затем ParseMsg(r) видит это и изящно закрывает соединение. получает сообщение. Кроме того, _client.Connected никогда не переключается на false даже после того, как удаленный клиент отключился.   -  person MisterNad    schedule 10.04.2017
comment
Я провел большую часть дня/ночи, пытаясь найти ожидающий/непрерывный асинхронный сервер, который не является типичным примером подключения, получения, эхо-обратной связи, отключения и, таким образом, все еще имеет проблему с добавлением цикла while + отсутствие сна входящие данные.   -  person MisterNad    schedule 11.04.2017
comment
В итоге я потратил время на изучение асинхронного метода и пошел с ним. Когда я его очистил и он достоин сохранения здесь, на SO, я отвечу на свой вопрос. Спасибо, что нашли время ответить людям, очень ценим обратную связь.   -  person MisterNad    schedule 11.04.2017


Ответы (2)


У меня была та же проблема, что и у вас, но я обнаружил, что лучший способ решить эту проблему:

Не блокировать сокет с помощью спящего режима и потока.

ОБНОВЛЕНИЕ: Если вы используете потоки и спящий режим на своем сервере, он будет страдать от низкой производительности при получении и ответе на каждое сообщение для каждого соединения.

Если вам нужно высокопроизводительное приложение, вы не должны использовать спящий режим или создавать поток для каждого принимаемого соединения. Лучше всего использовать асинхронные методы, которые предоставляет NetworkStream, используя BeginRead и EndRead, например:

    public void run()
    {
        server = new TcpListener(IPAddress.Any, port);
        server.Start();

        log.Info("Starting SocketServer on Port [" + port + "]");

        while (keepRunning)
        {
            try
            {
                TcpClient socket = server.AcceptTcpClient();
                if (keepRunning)
                    RequestManager.createRequestForEvalue(socket, idLayout);
            }
            catch (Exception ex)
            {
                log.Error(ex.Message);
                log.Error(ex.StackTrace);
            }
        }

        log.Info("Server Stoped.");
    }

    public static bool createRequestForEvalue(TcpClient socket, int idLayout)
    {
        Request req = null;
        req = new Request(socket,idLayout);

        registerRequest(req.ID,req); //Registra el Request, para su posterior uso.

        // DO NOT CREATE THREADS FOR ATTEND A NEW CONNECTION!!!
        //Task.Factory.StartNew(req.RunForIVR);
        //ThreadPool.QueueUserWorkItem(req.RunForIVR);

        req.startReceiveAsync(); //Recive data in asyncronus way.
        return true;
    }

    public void startReceiveAsync()
    {
        try
        {
            log.Info("[" + id + "] Starting to read the Request.");
            requestBuffer = new byte[BUFFER_SIZE];
            NetworkStream nst = socket.GetStream();
            nst.BeginRead(requestBuffer, 0,BUFFER_SIZE, this.requestReceived, nst);
        }catch(Exception ex)
        {
            log.Error("[" + id + "] There was a problem to read the Request: " + ex.Message);
            RequestManager.removeRequest(id);
            closeSocket();
        }
    }

    public void requestReceived(IAsyncResult ar)
    {

        try
        {   
        NetworkStream nst = socket.GetStream();
        int bread = nst.EndRead(ar); //Block the socket until all the buffer has been available.
        message = Encoding.UTF8.GetString(requestBuffer, 0, BUFFER_SIZE);
            log.Info("[" + id + "] Request recived: [" + message +"]");
            RunForIVR();
        }
        catch (Exception ex)
        {
            log.Error("[" + id + "] There was a problem to read the Request: " + ex.Message);
            RequestManager.removeRequest(id);
            closeSocket();
        }

    }

    public void SendResponse(String Response)
    {
        StringBuilder sb = new StringBuilder();
        sb.Append(Response);
        sb.Append('\0', BUFFER_SIZE - Response.Length);
        string message = sb.ToString();

        log.Info("[" + id + "] ivrTrans CMD: [" + idCMD + "] RESPONSE: [" + Response + "]");

        NetworkStream nst = socket.GetStream();
        byte[] buffer = new byte[BUFFER_SIZE];
        for (int i = 0; i < BUFFER_SIZE; i++)
            buffer[i] = (byte)message.ElementAt(i);

        nst.BeginWrite(buffer, 0, BUFFER_SIZE, this.closeSocket, nst);
    }

    public void closeSocket(IAsyncResult ar = null)
    {

        try
        {
            if (ar != null) //Since 4.24
            {
                NetworkStream nst = socket.GetStream();
                nst.EndWrite(ar);
            }

            socket.Close();
            socket = null;
        }catch(Exception ex)
        {
            log.Warn("[" + id + "] There was a problem to close the socket. Error: " + ex.Message + Environment.NewLine + ex.StackTrace);
        }
        log.Info("[" + id + "] Socket closed.");
    }

Обновление Я использую EndRead, чтобы убедиться, что запрос вообще поступил.

Другими словами, вы можете использовать BeginWrite и EndWrite, чтобы узнать, когда сокет был завершен для записи, чтобы закрыть соединение.

Таким образом, вы обслуживаете соединение в непрерывном режиме и как можно скорее. В моем случае я уменьшаю загрузку ЦП с 30% до 0% для монтирования 15K запросов в час.

person Jorge Omar Medra    schedule 10.04.2017
comment
В этом примере я не использую EndRead, потому что мне это не нужно -- Как узнать, сколько байтов было прочитано или было ли исключение? - person acelent; 11.04.2017
comment
@acelent В моем определении протокола установлен размер 612 байт на запрос и ответ. Когда вы работаете с сокетами и являетесь владельцем протокола, хорошей практикой является создание протокола с заголовком, который имеет объем данных для получения или, в этом случае, устанавливает уникальный объем данных для отправки и получения ( Я не владелец этого протокола). - person Jorge Omar Medra; 11.04.2017
comment
Если ваш заголовок имеет размер данных, вы можете дважды вызвать BegineRead. Во-первых, чтобы получить заголовок: startReceiveAsync он вызывает nst.BeginRead(requestBuffer, 0,HEADER_SIZE, this.headerReceived, nst);, во-вторых, и в метод headerReceived получает объем данных и запрашивает оставшиеся байты nst.BeginRead(requestBuffer, 0,DataSize, this.requestReceived, nst); где DataSize — это объем данных, которые вы получаете из заголовка. - person Jorge Omar Medra; 11.04.2017
comment
Я не критикую ваш протокол. Без EndRead у вас нет возможности узнать, было ли чтение успешным, и если да, то действительно ли было прочитано столько байтов, сколько было запрошено. Это принятие желаемого за действительное, эквивалентное синхронному try { nst.Read(requestBuffer, 0, BUFFER_SIZE); } catch { }. - person acelent; 11.04.2017
comment
@acelent Я согласен с вами по поводу использования EndRead, чем рекомендовал. В моем случае я получаю запрос и отправляю ответ, прежде чем закрыть соединение. Я провел много тестов, чтобы решить, разрешить или удалить EndRead, и, в соответствии с моим результатом в лаборатории и на производстве, я решил удалить EndRead, потому что этого было достаточно, чтобы получить количество байтов запроса. Но, как вы комментируете, для этого ответа лучше позволить EndRead показать, как это работает. - person Jorge Omar Medra; 11.04.2017

Правильный способ общения через сокет:

  1. Постоянно читать. Эти чтения будут блокироваться до тех пор, пока не поступят данные или пока сокет не будет изящно отключен (обнаруживаемый чтением, завершающимся чтением 0 байтов).
  2. Периодически писать. Эти записи необходимы для обеспечения жизнеспособности соединения< /а>.

Правильный подход к многопоточности требует двух потоков на одно соединение. Я не уверен, что это проще, чем асинхронный подход.

P.S. Если в вашем коде используется Connected, то в нем есть ошибка. Правильные решения никогда не должны использовать Connected.

person Stephen Cleary    schedule 10.04.2017
comment
Цените время, потраченное на ответ, и даже краткий просмотр вашей статьи, ваш ответ больше похож на то, что вы ошибаетесь, а не на пример или ресурс о том, как правильно делать что-то с помощью потоков. - person MisterNad; 10.04.2017
comment
@MisterNad: Нет примеров или ресурсов, в которых используется многопоточный подход, потому что он по своей сути не масштабируется. Лучшим ресурсом в целом, как отмечено в моей серии сообщений в блоге, является TCP/IP Illustrated Стивенса. - person Stephen Cleary; 10.04.2017