2012-12-12 15 views
15

W aplikacji C# (.NET 4.0) używam rozszerzeń reaktywnych (2.0.20823.0) do generowania granic czasowych dla grupowania zdarzeń w wartości zagregowane. Aby uprościć zapytania do wynikowej bazy danych, granice te muszą być wyrównane do pełnych godzin (lub sekund w poniższym przykładzie).Observable.Timer(): Jak uniknąć dryfu zegara?

Korzystanie Observable.Timer():

var time = DefaultScheduler.Instance; 

var start = new DateTimeOffset(time.Now.DateTime, time.Now.Offset); 

var span = TimeSpan.FromSeconds(1); 

start -= TimeSpan.FromTicks(start.Ticks % 10000000); 
start += span; 

var boundary = Observable.Timer(start, span, time); 

boundary.Select(i => start + TimeSpan.FromSeconds(i * span.TotalSeconds)) 
    .Subscribe(t => Console.WriteLine("ideal: " + t.ToString("HH:mm:ss.fff"))); 

boundary.Select(i => time.Now) 
    .Subscribe(t => Console.WriteLine("actual: " + t.ToString("HH:mm:ss.fff"))); 

Widać, że zamierzona i rzeczywisty czas timera kleszcze rozejścia się dość mocno:

ideal: 10:06:40.000 
actual: 10:06:40.034 
actual: 10:06:41.048 
ideal: 10:06:41.000 
actual: 10:06:42.055 
ideal: 10:06:42.000 
ideal: 10:06:43.000 
actual: 10:06:43.067 
actual: 10:06:44.081 
ideal: 10:06:44.000 
ideal: 10:06:45.000 
actual: 10:06:45.095 
actual: 10:06:46.109 
ideal: 10:06:46.000 
ideal: 10:06:47.000 
actual: 10:06:47.123 
actual: 10:06:48.137 
ideal: 10:06:48.000 
... 

ja również skorzystać z HistoricalScheduler i oczywiście Nie mam żadnych problemów. Mogę tolerować niewielkie niedokładności i nie muszę przejmować się zmianami zegara systemowego. Nie ma żadnych ciężkich operacji wywołanych przez te obserwowalne.

Ponadto wiem, że istnieje długa dyskusja na temat problemów z dryftami timera RX w tym blog post, ale wydaje mi się, że nie jestem w stanie owinąć się wokół niego.

Jaki byłby właściwy sposób okresowego planowania Observable bez systematycznego dryfowania zegara?

Odpowiedz

11

Można użyć Observable.Generate:

var boundary = Observable.Generate(
    0, _ => true, // start condition 
    i => ++i,  // iterate 
    i => i,  // result selector 
    i => start + TimeSpan.FromSeconds(i * span.TotalSeconds), 
    time); 

Będzie to przełożyć na podstawie czasu absolutnego każdej iteracji.

Oto niektóre przykładowe dane wyjściowe:

actual: 01:00:44.003 
ideal: 01:00:44.000 
actual: 01:00:44.999 
ideal: 01:00:45.000 
actual: 01:00:46.012 
ideal: 01:00:46.000 
actual: 01:00:47.011 
ideal: 01:00:47.000 
actual: 01:00:48.011 
ideal: 01:00:48.000 
actual: 01:00:49.007 
ideal: 01:00:49.000 
actual: 01:00:50.009 
ideal: 01:00:50.000 
actual: 01:00:51.006 
ideal: 01:00:51.000 

To nie pasuje dokładnie, wyobrażam sobie, ze względów wyjaśnionych przez Hansa, ale nie ma dryf.

EDIT:

Oto niektóre komentarze z RxSource

// BREAKING CHANGE v2 > v1.x - No more correction for time drift based on absolute time. This 
//        didn't work for large period values anyway; the fractional 
//        error exceeded corrections. Also complicated dealing with system 
//        clock change conditions and caused numerous bugs. 
// 
// - For more precise scheduling, use a custom scheduler that measures TimeSpan values in a 
// better way, e.g. spinning to make up for the last part of the period. Whether or not the 
// values of the TimeSpan period match NT time or wall clock time is up to the scheduler. 
// 
// - For more accurate scheduling wrt the system clock, use Generate with DateTimeOffset time 
// selectors. When the system clock changes, intervals will not be the same as diffs between 
// consecutive absolute time values. The precision will be low (1s range by default). 
+0

Wielkie dzięki, doskonała odpowiedź i wystarczająco dokładne, aby rozwiązać mój problem. Zabawnie, już to wykluczyłem, ponieważ 'Observable.Generate()' daje mi bóle głowy w [powiązanym scenariuszu] (http://stackoverflow.com/questions/13462713/why-does-observable-generate-throw- system-stackoverflowexception). Wygląda na to, że dobrze znam swój sprzęt za każdym razem, gdy zadaję pytanie związane z RX :-) –

+0

OK, dziękuję również za wskazanie komentarzy w źródłach RX. Wygląda na to, że powinienem je częściej sprawdzać, teraz, gdy są one dostępne. –

26

Domyślna częstotliwość blokowania zegara Windows na większości komputerów wynosi 64 przerwań na sekundę. Zaokrąglone przez CLR do 15,6 milisekund. To nie jest szczęśliwy numer, jeśli poprosisz o odstęp 1000 milisekund, nie ma integralnego dzielnika. Najbliższe dopasowania to 64 x 15,6 = 998 (za krótkie) i 65 x 15,6 = 1014 milisekund.

To jest dokładnie to, co widzisz, 41.048 - 40.034 = 1.014. 44,081 - 43,067 = 1,014, itd.

W rzeczywistości można zmienić szybkość przerwań, można wydzielić czasBeginPeriod() i poprosić o interwał 1 milisekundy. Będziesz potrzebował timeEndPeriod() przy zakończeniu programu, aby go zresetować. Nie jest to bardzo rozsądne zadanie, ma ogólnoustrojowe skutki uboczne i jest bardzo szkodliwe dla zużycia energii. Ale rozwiąże twój problem.

Bardziej rozsądnym podejściem jest po prostu potwierdzenie, że nie można nigdy dokładnie zachować czasu, dodając interwały. 15.6 ms, które używa CLR, jest już przybliżeniem. Zawsze ponawiaj kalibrację z absolutnym zegarem. Podejdź bliżej, prosząc o 998 ms zamiast 1000. Etcetera.

+2

To jest odpowiedź zabójca, upvoted –

+0

Wow, rzeczywiście wielki odpowiedź. Sądząc po przegranej Paula Zakładam, że nie ma sposobu na "Observable.Timer()" wokół tego ograniczenia.Chciałbym móc zaznaczyć zarówno twoje, jak i Matthew, odpowiedzi jako poprawne. Postanowiłem przejść z konkretnym RX, ale mimo to dziękuję. –

Powiązane problemy