Bug: Timeouts in einer Multithreadapplikation

Vor kurzem lief mir ein interessanter Bug über den Weg, dazu erst kurzer Beispielcode:

uint64_t nTimeout;
 
void Func1()
{
    uint64_t nTickCount = GetTickCount();
    myLock.lock();
    ...
    nTimeout = nTickCount;
    ...
    myLock.unlock();
}
 
void Func2()
{
    uint64_t nTickCount = GetTickCount();
    myLock.lock();
    ...
    if((nTickCount-nTimeout)/1000>30) // Beispiel 30s 
        // Timeout
    ...
    myLock.unlock();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Auswirkungen

Func1 und Func2 werden von unterschiedlichen Threads aufgerufen. Func1 setzt ab und zu, aber rechtzeitig innerhalb der 30s den Timeout zurück. Trotzdem wirft Func2 ab und zu einen Timeout.

Analyse

Der Code hat mindestens zwei Probleme:

Das erste Problem ist, dass der TickCounter jeweils vor der Sperre gelesen wird. So kann es passieren, das innerhalb der Sperre in Func2 nTickCount kleiner als nTimeout ist. Dies ist dann der Fall, wenn nach dem Lesen des TickCounters in Func2 der Thread angehalten wird und Func1 voll durchläuft.

Hierdurch schlägt das zweite Problem ein, die vorzeichenlose Subtraktion bei der Bedingung im zweiten if. In dieser Situation, entsteht ein riesen Wert, der immer über dem Timeout liegt, es sei denn ein Timeout von einigen Millionen Jahren ist benötigt.

Verbesserungen

Problem Eins kann leicht durch Verschiebung des Auslesens des TickCounters in die Sperre beheben. Dies verhindert auch direkt Problem Zwei. Andererseits kann überlegt werden, ob Systemaufrufe innerhalb von Sperren unbedingt nötig sind.

Die weitere Wahl wäre die Timeoutbedingung zu reparieren. Statt einer Subtraktion der beiden Werte, lieber einen Vergleich benutzen, der einfach keinen Überlauf erzeugen kann.

if(nTickCount>nTimeout+1000*30) // Beispiel 30s
18

Fazit

Auch wenn es nur eine Zeile im Code war, die auf diese Art und Weise einen Timeout prüft, sollte immer bedacht werden, dass es Situationen gibt bei denen die aktuelle Prüfzeit nicht die aktuellste ist.