2013-05-19 21 views
8

Szukam utworzyć wątek wywołania zwrotnego wysokiej częstotliwości. Zasadniczo potrzebuję funkcji do wykonania w regularnych odstępach wysokich częstotliwości (do 100 Hz). Zdaję sobie sprawę, że Windows ma normalny przekrój wykonania wątku to ~ 15ms. Chciałbym określić regularny interwał, który może być szybszy niż 15 ms.timing wysokiej częstotliwości .NET

Oto, co staram się osiągnąć. Mam urządzenie zewnętrzne, które musi być powiadamiane w określonym przedziale czasu. Odstęp jest zmienny w zależności od sytuacji. Oczekuję, że nigdy nie będę potrzebował więcej niż 100Hz (10ms) wskaźnika wiadomości.

Mogę oczywiście zaimplementować pętlę spinową, jednak miałem nadzieję, że istnieje rozwiązanie, które nie wymagałoby tak wielu zmarnowanych zasobów.

Podane linki do pytań/odpowiedzi nie rozwiązują tego problemu. Chociaż zgadzam się, że pytanie zostało zadane na kilka różnych sposobów, nie było dobrego rozwiązania, które faktycznie rozwiązało problem.

Większość udzielonych odpowiedzi mówi o użyciu stopera i ręcznym wykonywaniu zadania synchronizacji, które jest całkowicie zbyt intensywne dla procesora. Jedynymi realnymi rozwiązaniami były czasomierze multimedialne, które miały kilka pułapek, o czym wspomniał Haans. Znalazłem jednak inne rozwiązanie, które dodam poniżej. W tej chwili nie znam pułapek, ale planuję przeprowadzić testy i badania. Nadal jestem zainteresowany komentarzami dotyczącymi rozwiązania.


wezwanie WINAPI poprzez

BOOL WINAPI CreateTimerQueueTimer(
    _Out_  PHANDLE phNewTimer, 
    _In_opt_ HANDLE TimerQueue, 
    _In_  WAITORTIMERCALLBACK Callback, 
    _In_opt_ PVOID Parameter, 
    _In_  DWORD DueTime, 
    _In_  DWORD Period, 
    _In_  ULONG Flags 
); 

i

BOOL WINAPI DeleteTimerQueueTimer(
    _In_opt_ HANDLE TimerQueue, 
    _In_  HANDLE Timer, 
    _In_opt_ HANDLE CompletionEvent 
); 

Link - http://msdn.microsoft.com/en-us/library/windows/desktop/ms682485%28v=vs.85%29.aspx Używam PInvoke do osiągnięcia tego celu. Będzie to również wymagane w przypadku obsługi timerów multimedialnych.

Moja sygnatura typu PInvoke dla zainteresowanych. Pinvoke link

[DllImport("kernel32.dll")] 
static extern bool CreateTimerQueueTimer(out IntPtr phNewTimer, 
    IntPtr TimerQueue, WaitOrTimerDelegate Callback, IntPtr Parameter, 
    uint DueTime, uint Period, uint Flags); 

// This is the callback delegate to use. 
public delegate void WaitOrTimerDelegate (IntPtr lpParameter, bool TimerOrWaitFired); 

[DllImport("kernel32.dll")] 
static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer, 
    IntPtr CompletionEvent); 

użycie CreateTimerQueueTimer rozpocząć oddzwanianie timera. Użyj DeleteTimerQueueTimer, aby zatrzymać oddzwonienie timera. Jest to dość elastyczne, ponieważ można również tworzyć niestandardowe kolejki. Jednak najłatwiejszą implementacją, jeśli potrzebna jest tylko jedna instancja, byłoby użycie domyślnej kolejki.

Przetestowałem to rozwiązanie wzdłuż strony pierwszej używając stopera z pętlą spinową, a wyniki otrzymane w odniesieniu do synchronizacji były prawie identyczne. Jednak obciążenie procesora było znacząco różne na moim komputerze.

Stoper z pętli spinu - ~ 12-15% obciążenie CPU stałej (około 50% jednego z moich rdzeni) CreateTimerQueueTimer - ~ 3-4% CPU stałe obciążenie

czuję też utrzymanie kod będzie zmniejszono za pomocą opcji CreateTimerQueueTimer. ponieważ nie wymaga dodawania logiki do przepływu kodu.

Odpowiedz

10

Istnieje część z niedokładnych informacji rozpowszechnianych za pośrednictwem linków i komentarzy. Tak, domyślnie przerwanie taktowania zegara wynosi 1/64 sekundy = 15,625 ms w przypadku większości komputerów, ale można je zmienić. Również powód, dla którego niektóre maszyny wydają się działać na innej stawce. Multimedialny api Windows, dostępny z Winmm.dll, pozwala ci go modyfikować.

Nie musisz robić za pomocą stopera. Otrzymasz dokładny pomiar interwału, gdy użyjesz go w gorącej pętli, która nieustannie sprawdza, czy minął okres czasu. Windows traktuje taki wątek nieumiejętnie, gdy wygasa jego kwant, wątek nie zostanie ponownie zaplanowany, aby działał przez jakiś czas, gdy inne wątki rywalizują o procesor. Ten efekt jest łatwy do przeoczenia, ponieważ zazwyczaj nie debugujesz kodu przy pomocy innych aktywnych procesów i nagrywania czasu procesora.

Funkcja, której chcesz użyć to timeSetEvent(), zapewnia bardzo dokładny timer, który może trwać nawet 1 milisekundę. Jest to autodopasowanie, zmniejszając interwał, jeśli jest to konieczne (i możliwe), aby nadrobić zaległości, gdy poprzednie wywołanie zwrotne zostało opóźnione z powodu ograniczeń dotyczących harmonogramu. Uważaj jednak, że jest to trudne w użyciu, wywołanie zwrotne jest wykonywane z wątku wątku, podobnie jak w przypadku System.Threading.Timer, więc należy używać bezpiecznych blokad i podejmować środki zaradcze, które zapewnią, że nie będzie kłopotów z ponownym wprowadzeniem.

Całkowicie odmienne podejście to timeBeginPeriod(), zmienia częstotliwość przerwań zegara. Ma to wiele skutków ubocznych, ponieważ jeden Thread.Sleep() staje się dokładniejszy. Które są łatwiejsze, ponieważ możesz zrobić to synchronicznie. Po prostu spać przez 1 ms, a otrzymasz wskaźnik przerwania. Niektóre kodu do zabawy, który demonstruje sposób to działa:

using System; 
using System.Runtime.InteropServices; 
using System.Diagnostics; 
using System.Threading; 

class Program { 
    static void Main(string[] args) { 
     timeBeginPeriod(10); 
     while (!Console.KeyAvailable) { 
      var sw = Stopwatch.StartNew(); 
      for (int ix = 0; ix < 100; ++ix) Thread.Sleep(1); 
      sw.Stop(); 
      Console.WriteLine("{0} msec", sw.ElapsedMilliseconds); 
     } 
     timeEndPeriod(10); 
    } 

    [DllImport("winmm.dll")] 
    public static extern uint timeBeginPeriod(int msec); 
    [DllImport("winmm.dll")] 
    public static extern uint timeEndPeriod(int msec); 
} 

Wyjście na moim komputerze:

1001 msec 
995 msec 
999 msec 
999 msec 
999 msec 
991 msec 
999 msec 
999 msec 
999 msec 
999 msec 
999 msec 
990 msec 
999 msec 
998 msec 
... 

Czy uważaj jednak problem z tym podejściem, czy inny proces na komputerze został już obniżony wskaźnik przerwania taktowania poniżej 10 ms, wtedy nie dostaniesz sekundy z tego kodu. To bardzo trudne do rozwiązania, możesz tylko uczynić to naprawdę bezpiecznym, prosząc o 1 msek stawki i śpiąc na 10. Nie uruchamiaj tego na maszynie zasilanej bateryjnie. Wykonaj polecenie timeSetEvent().

+1

Dzięki Hans, zawsze mogę docenić twoje odpowiedzi i porady. Znalazłem coś, co rozwiązuje mój problem podczas badania interfejsu API, o którym wspomniałeś. To jest użycie CreateTimerQueueTimer. Wydaje mi się, że daje mi to rozwiązanie z API, o którym wspomniałeś, ale bez wad związanych ze zmianą kolejności zdarzeń. Kolejną dodatkową korzyścią jest to, że pochodzi z pliku kernal32.dll. Wszelkie myśli dotyczące klasy API CreateTimerQueueTimer? – galford13x

+0

Tak, jest to zalecana alternatywa dla timeSetEvent(). Nie wiem wystarczająco dużo o jego zdolności do zmiany częstotliwości przerywania zegara, będziesz musiał eksperymentować. –

+0

Widząc, jak nie można podać więcej odpowiedzi. Zaznaczam to jako odpowiedź. Zapewnia rozwiązanie, które zadziała w mojej sytuacji. – galford13x

1

Być może warto wypróbować model StopWatch, który wykorzystuje te same zegary wysokiej wydajności używane przez DirectX.

+1

Dostępność. Nie zapomnij sprawdzić [IsHighResolution] (http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.ishighresolution.aspx) –

+1

Powinno to być prawdą w przypadku najnowocześniejszego sprzętu, jak sądzę. –

+0

Tak, ale na wszelki wypadek trzeba to sprawdzić. –

0

Zauważ, że jednym z powodów, dla których 15ms jest efektywnym minimum Sleep, jest to, że może on zmienić tę wielkość, aby zamienić swój proces, zamienić go na inny, pozwolić mu na zrobienie czegokolwiek, zamienić na inny. się i zamień ponownie. Zakładając, że masz wiele rdzeni, dedykując jeden do ręki, twój aktualizator 100Hz jest prawdopodobnie najlepszy, szczególnie jeśli wiesz, kiedy chcesz pominąć kilka cykli dla ręcznie wygenerowanego garbage collection i/lub użyć C/C++ zgodnie z sugestią.

+0

Nie wspominając o tym, że kontekst zamiany odbywa się z określoną minimalną częstotliwością, która różni się od wersji systemu Windows dla poszczególnych klientów. Zobacz Windows quantum –

+0

Powód, dla którego 15 ms odgrywa rolę, jest taki jak wspomniany Haans, czyli minimalny odstęp czasu przydzielony wątkowi przez okna. Oczywiście tak jak powiedział, jest to zależne od wersji platformy i systemu operacyjnego. Możesz to przetestować, pisząc wielowątkową aplikację, która rejestruje czas i używa Thread.Sleep (2) przez kilka sekund, a następnie wydrukuje wszystkie czasy zapisane w wątku. Zauważysz, że dzieli je ~ 15,6ms. Oczywiście wątek może zużyć mniej niż 15 ms, po prostu dzwoniąc Thread.Sleep (0), oddając resztę swojego plasterka na coś ważniejszego. – galford13x

0

Thread.Sleep() nie wydaje się być tak niedokładne, jak można myśleć o takich małych odstępach, następujące w systemie Windows 8.1 x64 i7 Laptop .NET 4.0:

using System; 
using System.Diagnostics; 
using System.Threading; 

namespace HighFrequency 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      var count = 0; 
      var stopwatch = new Stopwatch(); 
      stopwatch.Start(); 
      while(count <= 1000) 
      { 
       Thread.Sleep(1); 
       count++; 
      } 
      stopwatch.Stop(); 
      Console.WriteLine("C# .NET 4.0 avg. {0}", stopwatch.Elapsed.TotalSeconds/count); 
     } 
    } 
} 

wyjściowa:

C#. NET 4.0 śr. 0.00197391928071928

Czas poniżej 2ms! Sądzę, że gdyby było więcej sprzeczek, które mogłyby powstać dość szybko, chociaż mam wiele otwartych aplikacji, po prostu nic nie miażdżyło.

Przy zasypianiu przez 10 ms, tj. 100 Hz, średnia wynosiła 0,0109 ...prawie na miejscu!

+0

Jeśli coś nie zmieniło się w Win8 dla wezwania snu, to ogólnie sen nie jest dobrym mechanizmem do zatrzymania czasu. Ponadto, sen może być nieprzewidywalny w jego minimalnym czasie, gdy timer wysokiej częstotliwości zostanie zmieniony w systemie. Podsumowując, podczas gdy otrzymujesz ~ 10ms, kiedy uruchomię to na moim komputerze Win7, uzyskuję 15.6ms na średnim poziomie. Również nie szukałem średnich wartości, szukałem precyzyjnego, spójnego taktowania, więc 12ms, 9ms, 11ms, 8ms byłoby nie do zaakceptowania. – galford13x

+0

Prawda, nie użyłbym go do utrzymania czasu, tylko dla pętli wysokiej częstotliwości - zmierzyłbym czas, który upłynął, używając "Stopera" - ale wciąż nie jest to opcja, jeśli chcesz mieć stały czas. – markmnl

Powiązane problemy