Domanda

Sto concatenando 15 operazioni asincrone tramite porte e ricevitori.Ciò mi ha lasciato molto preoccupato per il tempo di messaggistica interthread, in particolare il tempo necessario tra un'attività che invia dati a una porta e una nuova attività che inizia a elaborare gli stessi dati su un thread diverso.Supponendo la situazione migliore in cui ciascun thread è inattivo all'avvio, ho generato un test che utilizza la classe cronometro per misurare il tempo di due diversi dispatcher, ciascuno dei quali opera con la massima priorità con un singolo thread.

Ciò che ho scoperto mi ha sorpreso, il mio impianto di sviluppo è un computer Q6600 Quad Core da 2,4 Ghz con Windows 7 x64 e il tempo medio di cambio di contesto dal mio test è stato di 5,66 microsecondi con una deviazione standard di 5,738 microsecondi e un massimo di quasi 1,58 millisecondi ( un fattore di 282!).La frequenza del cronometro è di 427,7 nanosecondi, quindi sono ancora ben lontano dal rumore del sensore.

Quello che vorrei fare è ridurre il più possibile il tempo di messaggistica tra thread e, cosa altrettanto importante, ridurre la deviazione standard del cambio di contesto.Mi rendo conto che Windows non è un sistema operativo in tempo reale e non ci sono garanzie, ma l'utilità di pianificazione di Windows è una pianificazione basata su priorità round robin equa e i due thread in questo test hanno entrambi la massima priorità (gli unici thread che dovrebbero essere così alto), quindi non dovrebbero esserci cambi di contesto sui thread (evidente dal tempo più grande di 1,58 ms...Credo che Windows Quanta sia 15,65 ms?) L'unica cosa a cui riesco a pensare è la variazione nei tempi delle chiamate del sistema operativo ai meccanismi di blocco utilizzati dal CCR per passare i messaggi tra i thread.

Per favore fatemi sapere se qualcun altro là fuori ha misurato il tempo di messaggistica interthread e ha qualche suggerimento su come migliorarlo.

Ecco il codice sorgente dei miei test:

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();
        }
    }
}
È stato utile?

Soluzione

Windows non è un sistema operativo in tempo reale.Ma questo lo sapevi già.Ciò che ti sta uccidendo sono i tempi di cambio di contesto, non necessariamente i tempi dei messaggi.Non hai realmente specificato COME funziona la comunicazione tra processi.Se stai davvero eseguendo più thread, troverai alcuni vantaggi non utilizzando il messaggio di Windows come protocollo di comunicazione, prova invece a eseguire il rollover del tuo IPC utilizzando le code di messaggi ospitate dall'applicazione.

La media migliore che puoi sperare è 1 ms con qualsiasi versione di Windows quando si verifica il cambio di contesto.Probabilmente stai vedendo i tempi di 1 ms in cui la tua applicazione deve cedere al kernel.Questo è previsto dalla progettazione per le applicazioni Ring-1 (spazio utente).Se è assolutamente fondamentale che scendi al di sotto di 1 ms dovrai convertire alcune delle tue applicazioni in Ring-0, il che significa scrivere un driver di dispositivo.

I driver di dispositivo non subiscono gli stessi tempi di cambio di contesto delle app utente e hanno accesso anche a timer con risoluzione in nanosecondi e chiamate di sospensione.Se hai bisogno di farlo, il DDK (Device Driver Development Kit) è disponibile gratuitamente da Microsoft, ma ti consiglio vivamente di investire in un kit di sviluppo di terze parti.Di solito hanno campioni davvero buoni e molte procedure guidate per impostare le cose nel modo giusto, la cui scoperta richiederebbe mesi di lettura dei documenti DDK.Ti consigliamo anche di ottenere qualcosa come SoftIce perché il normale debugger di Visual Studio non ti aiuterà a eseguire il debug dei driver di dispositivo.

Altri suggerimenti

Esegui le 15 operazioni asincrone Avere essere asincrono?cioè.sei costretto a operare in questo modo da una limitazione di alcune librerie o hai la possibilità di effettuare chiamate sincrone?

Se puoi scegliere, devi strutturare la tua applicazione in modo che l'uso dell'asincronicità sia controllato dai parametri di configurazione.La differenza tra operazioni asincrone che restituiscono su un thread diverso e operazioni asincrone che restituiscono su un thread diverso.le operazioni sincrone che restituiscono sullo stesso thread dovrebbero essere trasparenti nel codice.In questo modo puoi ottimizzarlo senza modificare la struttura del codice.

La frase "imbarazzantemente parallelo" descrive un algoritmo in cui la maggior parte del lavoro svolto è ovviamente indipendente e quindi può essere eseguito in qualsiasi ordine, facilitando la parallelizzazione.

Ma stai "concatenando 15 operazioni asincrone attraverso porte e ricevitori".Questo potrebbe essere descritto come "imbarazzantemente sequenziale".In altre parole, lo stesso programma potrebbe essere scritto logicamente su un singolo thread.Ma in questo caso perderesti qualsiasi parallelismo per il lavoro legato alla CPU che si verifica tra le operazioni asincrone (supponendo che ce ne sia qualcosa di significativo).

Se scrivi un semplice test per eliminare qualsiasi lavoro significativo legato alla CPU e misurare semplicemente il tempo di cambio di contesto, indovina un po', misurerai la variazione nel tempo di cambio di contesto, come hai scoperto.

L'unico motivo per eseguire più thread è perché hai una quantità significativa di lavoro da svolgere per le CPU e quindi vorresti condividerlo tra più CPU.Se i singoli blocchi di calcolo hanno una durata sufficientemente breve, il cambio di contesto comporterà un sovraccarico significativo Qualunque sistema operativo.Suddividendo il calcolo in 15 fasi, ciascuna molto breve, stai essenzialmente chiedendo al sistema operativo di eseguire molti cambi di contesto non necessari.

ThreadPriority.Highest non significa che solo lo scheduler del thread stesso abbia una priorità più alta.L'API Win32 ha un livello più granulare di priorità del thread (cliccabile) con diversi livelli superiori al Più alto (IIRC Più alto è in genere la priorità più alta con cui può essere eseguito il codice non amministrativo, gli amministratori possono programmare una priorità più alta così come qualsiasi driver hardware/codice in modalità kernel) quindi non vi è alcuna garanzia che non verranno preceduti .

Anche se un thread è in esecuzione con la priorità più alta, le finestre possono promuovere altri thread al di sopra della loro priorità di base se detengono blocchi di risorse richiesti dai thread con priorità più alta, che è un'altra possibilità per cui potresti subire cambi di contesto.

Anche in questo caso, come dici tu, Windows non è un sistema operativo in tempo reale e non è comunque garantito che rispetti le priorità dei thread.

Per affrontare questo problema in un modo diverso, è necessario disporre di così tante operazioni asincrone disaccoppiate?Potrebbe essere utile riflettere su:partizionamento verticale del lavoro (elaborazione asincrona di blocchi di dati numCores dall'inizio alla fine) invece di partizionamento orizzontale del lavoro (come ora, con ogni blocco di dati elaborato in 15 fasi disaccoppiate);accoppiando in modo sincrono alcune delle 15 fasi per ridurre il totale a un numero inferiore.

Il sovraccarico della comunicazione tra thread non sarà sempre banale.Se alcune delle tue 15 operazioni svolgono solo una piccola parte del lavoro, i cambi di contesto ti morderanno.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top