Pergunta

Estou encadeando 15 operações assíncronas por meio de portas e receptores.Isso me deixou muito preocupado com o tempo de mensagens entre threads, especificamente o tempo que leva entre uma tarefa postar dados em uma porta e uma nova tarefa começar a processar os mesmos dados em um thread diferente.Assumindo a melhor situação em que cada thread está ocioso no início, gerei um teste que usa a classe stop watch para medir o tempo de dois despachantes diferentes, cada um operando na prioridade mais alta com um único thread.

O que descobri me surpreendeu: meu equipamento de desenvolvimento é um computador Q6600 Quad Core 2,4 Ghz rodando Windows 7 x64, e o tempo médio de troca de contexto do meu teste foi de 5,66 microssegundos com um desvio padrão de 5,738 microssegundos e um máximo de quase 1,58 milissegundos ( um fator de 282!).A frequência do cronômetro é de 427,7 nano segundos, então ainda estou bem longe do ruído do sensor.

O que eu gostaria de fazer é reduzir ao máximo o tempo de mensagens entre threads e, igualmente importante, reduzir o desvio padrão da troca de contexto.Sei que o Windows não é um sistema operacional em tempo real e não há garantias, mas o agendador do Windows é um cronograma justo baseado em prioridade round robin, e os dois threads neste teste têm a prioridade mais alta (os únicos threads que deveriam ser isso alto), portanto não deve haver nenhuma mudança de contexto nos threads (evidente pelo maior tempo de 1,58 ms...Acredito que o windows quanta tem 15,65 ms?) A única coisa que consigo pensar é na variação no tempo das chamadas do sistema operacional para os mecanismos de bloqueio usados ​​pelo CCR para passar mensagens entre threads.

Informe-me se alguém mediu o tempo de mensagens entre threads e tem alguma sugestão sobre como melhorá-lo.

Aqui está o código fonte dos meus testes:

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Microsoft.Ccr.Core;

using System.Diagnostics;

namespace Test.CCR.TestConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Starting Timer");
            var sw = new Stopwatch();
            sw.Start();

            var dispatcher = new Dispatcher(1, ThreadPriority.Highest, true, "My Thread Pool");
            var dispQueue = new DispatcherQueue("Disp Queue", dispatcher);

            var sDispatcher = new Dispatcher(1, ThreadPriority.Highest, true, "Second Dispatcher");
            var sDispQueue = new DispatcherQueue("Second Queue", sDispatcher);

            var legAPort = new Port<EmptyValue>();
            var legBPort = new Port<TimeSpan>();

            var distances = new List<double>();

            long totalTicks = 0;

            while (sw.Elapsed.TotalMilliseconds < 5000) ;

            int runCnt = 100000;
            int offset = 1000;

            Arbiter.Activate(dispQueue, Arbiter.Receive(true, legAPort, i =>
                                                                            {
                                                                                TimeSpan sTime = sw.Elapsed;
                                                                                legBPort.Post(sTime);
                                                                            }));
            Arbiter.Activate(sDispQueue, Arbiter.Receive(true, legBPort, i =>
                                                                             {
                                                                                 TimeSpan eTime = sw.Elapsed;
                                                                                 TimeSpan dt = eTime.Subtract(i);
                                                                                 //if (distances.Count == 0 || Math.Abs(distances[distances.Count - 1] - dt.TotalMilliseconds) / distances[distances.Count - 1] > 0.1)
                                                                                 distances.Add(dt.TotalMilliseconds);

                                                                                 if(distances.Count > offset)
                                                                                 Interlocked.Add(ref totalTicks,
                                                                                                 dt.Ticks);
                                                                                 if(distances.Count < runCnt)
                                                                                     legAPort.Post(EmptyValue.SharedInstance);
                                                                             }));


            //Thread.Sleep(100);
            legAPort.Post(EmptyValue.SharedInstance);

            Thread.Sleep(500);

            while (distances.Count < runCnt)
                Thread.Sleep(25);

            TimeSpan exTime = TimeSpan.FromTicks(totalTicks);
            double exMS = exTime.TotalMilliseconds / (runCnt - offset);

            Console.WriteLine("Exchange Time: {0} Stopwatch Resolution: {1}", exMS, Stopwatch.Frequency);

            using(var stw = new StreamWriter("test.csv"))
            {
                for(int ix=0; ix < distances.Count; ix++)
                {
                    stw.WriteLine("{0},{1}", ix, distances[ix]);
                }
                stw.Flush();
            }

            Console.ReadKey();
        }
    }
}
Foi útil?

Solução

O Windows não é um sistema operacional em tempo real.Mas voce ja sabia disso.O que está matando você são os tempos de troca de contexto, não necessariamente os tempos de mensagem.Você realmente não especificou COMO funciona a comunicação entre processos.Se você realmente estiver executando vários threads, encontrará alguns ganhos ao não usar a mensagem do Windows como protocolo de comunicação; em vez disso, tente lançar seu próprio IPC usando filas de mensagens hospedadas em aplicativos.

A melhor média que você pode esperar é 1 ms com qualquer versão do Windows quando ocorrem mudanças de contexto.Você provavelmente está vendo os tempos de 1 ms em que seu aplicativo precisa ceder ao kernel.Isso ocorre por design para aplicativos Ring-1 (espaço do usuário).Se for absolutamente crítico que você fique abaixo de 1ms, você precisará mudar alguns de seus aplicativos para Ring-0, o que significa escrever um driver de dispositivo.

Os drivers de dispositivo não sofrem os mesmos tempos de mudança de contexto que os aplicativos do usuário e também têm acesso a temporizadores de resolução de nanossegundos e chamadas de suspensão.Se você precisar fazer isso, o DDK (Device Driver Development Kit) está disponível gratuitamente na Microsoft, mas eu recomendo ALTAMENTE que você invista em um kit de desenvolvimento de terceiros.Eles geralmente têm amostras realmente boas e muitos assistentes para configurar as coisas corretamente, o que levaria meses lendo documentos DDK para descobrir.Você também desejará obter algo como o SoftIce porque o depurador normal do Visual Studio não ajudará a depurar drivers de dispositivos.

Outras dicas

Faça as 15 operações assíncronas ter ser assíncrono?ou sejavocê é forçado a operar dessa forma por uma limitação de alguma biblioteca ou tem a opção de fazer chamadas síncronas?

Se você tiver escolha, precisará estruturar seu aplicativo para que o uso da assincronicidade seja controlado por parâmetros de configuração.A diferença entre operações assíncronas que retornam em um thread diferente vs.operações síncronas que retornam no mesmo thread devem ser transparentes no código.Dessa forma você pode ajustá-lo sem alterar a estrutura do código.

A frase "embaraçosamente paralelo" descreve um algoritmo no qual a maior parte do trabalho realizado é obviamente independente e, portanto, pode ser feito em qualquer ordem, facilitando a paralelização.

Mas você está "encadeando 15 operações assíncronas por meio de portas e receptores".Isso poderia ser descrito como "embaraçosamente sequencial".Em outras palavras, o mesmo programa poderia ser escrito logicamente em um único thread.Mas então você perderia qualquer paralelismo para o trabalho vinculado à CPU que ocorre entre as operações assíncronas (supondo que haja algum significado).

Se você escrever um teste simples para eliminar qualquer trabalho significativo vinculado à CPU e apenas medir o tempo de troca de contexto, então adivinhe, você medirá a variação no tempo de troca de contexto, como descobriu.

A única razão para executar vários threads é porque você tem uma quantidade significativa de trabalho para as CPUs realizarem e, portanto, gostaria de compartilhá-lo entre várias CPUs.Se os pedaços individuais de computação tiverem vida curta o suficiente, então a troca de contexto será uma sobrecarga significativa para qualquer SO.Ao dividir seu cálculo em 15 estágios, cada um muito curto, você está essencialmente pedindo ao sistema operacional para fazer muitas trocas de contexto desnecessárias.

ThreadPriority.Highest não significa que apenas o próprio agendador de threads tenha uma prioridade mais alta.A API Win32 tem um nível mais granular de prioridade de thread (clicável) com vários níveis acima do mais alto (IIRC mais alto é normalmente a prioridade mais alta em que o código não administrativo pode ser executado, os administradores podem agendar uma prioridade mais alta, assim como qualquer driver de hardware/código de modo kernel), portanto, não há garantia de que eles não serão antecipados .

Mesmo que um thread esteja sendo executado com a prioridade mais alta, as janelas podem promover outros threads acima de sua prioridade básica se eles estiverem mantendo bloqueios de recursos exigidos por threads de prioridade mais alta, o que é outra possibilidade de você estar sofrendo mudanças de contexto.

Mesmo assim, como você disse, o Windows não é um sistema operacional em tempo real e, de qualquer maneira, não é garantido que honre as prioridades dos threads.

Para atacar esse problema de uma maneira diferente, você precisa ter tantas operações assíncronas desacopladas?Pode ser útil pensar sobre:particionar verticalmente o trabalho (processar de forma assíncrona pedaços de dados numCores de ponta a ponta) em vez de particionar horizontalmente o trabalho (como agora, com cada pedaço de dados processado em 15 estágios desacoplados);acoplando de forma síncrona alguns de seus 15 estágios para reduzir o total a um número menor.

A sobrecarga da comunicação entre threads sempre não será trivial.Se algumas de suas 15 operações estiverem realizando apenas uma pequena parte do trabalho, as mudanças de contexto irão incomodá-lo.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top