2009-05-07 9 views
3

Ich habe ein Problem, das scheint mir beunruhigend. Es scheint, dass ich eine Situation gefunden habe, die leicht genug ist, um zu arbeiten, aber das könnte zu Problemen führen, wenn a) ich beim Programmieren eine Konzentrationsstörung habe oder b) jemand anders meine Interfaces implementiert und nicht weiß, wie ich damit umgehen soll diese Situation.Vermeiden Sie Deadlock mit nicht-virtuellen öffentlichen Schnittstelle und Scoped Locks in C++

Hier ist meine Grundeinstellung:

ich eine abstrakte Klasse habe, die ich als eine generische Schnittstelle zu mehreren Datentypen verwenden. Ich habe das Paradigma der nicht-virtuellen öffentlichen Schnittstelle (Sutter, 2001) zusammen mit dem Scoped-Locking übernommen, um eine gewisse Thread-Sicherheit zu bieten. Ein Beispiel Interface-Klasse würde wie folgt aussehen (ich habe weggelassen Details über scoped Schließ- und der Mutex Implementierung, da ich nicht glaube, sie sind relevant):

class Foo 
{ 
public: 
    A() 
    { 
     ScopedLock lock(mutex); 
     aImp(); 
    } 
    B() 
    { 
     ScopedLock lock(mutex); 
     bImp(); 
    } 
protected: 
    aImp() = 0; 
    bImp() = 0; 
} 

Es liegt dann an den Benutzer . zu implementieren AIMP und BIMP, das ist, wo das Problem kommt Wenn AIMP eine Operation durchführt, die BIMP verwendet, ist es extrem leicht ist (und fast logisch, in gewissem Sinne), dies zu tun:

class Bar 
{ 
protected: 
    aImp() 
    { 
     ... 
     B(); 
     ... 
    } 
    bImp() 
    { 
     ... 
    } 
} 

Deadlock. Natürlich besteht die einfache Lösung darin, immer die geschützten virtuellen Funktionen und nicht ihre öffentlichen Varianten aufzurufen (ersetzen Sie B() durch bImp() im obigen Snippet). Aber es scheint mir immer noch viel zu leicht zu sein, mich selbst aufzuhängen, wenn ich einen Fehler begehe, oder schlimmer noch, anderen zu erlauben, sich aufzuhängen.

Gibt es jemanden, der versucht, einen Implementierer der abstrakten Klasse daran zu hindern, diese öffentlichen Funktionen zur Kompilierungszeit aufzurufen oder die Deadlock-Lösung zu vermeiden?

Nur für den Kick, ermöglichen einige Mutexe Betrieb, die Deadlock-Probleme zu vermeiden. Wenn ich dies beispielsweise mit den Windows-Funktionen EnterCriticalSection und LeaveCriticalSection implementiere, gibt es kein Problem. Aber ich würde eher plattformspezifische Funktionalität vermeiden. Ich benutze derzeit boost :: mutex und boost :: shared_mutex in meiner Scoped-Lock-Implementierung, und soweit ich gesehen habe, versuche ich nicht Deadlock zu vermeiden (was ich glaube, ich bevorzuge es fast).

+0

Eine weitere Sutter-Empfehlung: "Vermeiden Sie den Aufruf virtueller Methoden" in "Vermeiden Sie den Aufruf unbekannter Codes in einem kritischen Bereich" (http://www.ddj.com/architect/202802983). –

+0

Danke für den Link. Ich werde das jetzt lesen. – Perculator

Antwort

7

private Vererbung verwenden wird möglicherweise Ihr Problem lösen:

class Foo 
{ 
public: 
    void A() 
    { 
     ScopedLock lock(mutex); 
     aImp(); 
    } 
    void B() 
    { 
     ScopedLock lock(mutex); 
     bImp(); 
    } 

protected: 
    virtual void aImp() = 0; 
    virtual void bImp() = 0; 
}; 

class FooMiddle : private Foo 
{ 
public: 
    using Foo::aImp; 
    using Foo::bImp; 
}; 

class Bar : public FooMiddle 
{ 
    virtual void aImpl() 
    { 
    bImp(); 
    B();     // Compile error - B is private 
    } 
}; 

von Foo Ableitung privat, und dann mit FooMiddle sorgt dafür, dass Bar keinen Zugriff auf A oder B. jedoch bar noch in der Lage ist, außer Kraft zu setzen aImp und bImp, und die Verwendung von Deklarationen in FooMiddle bedeutet, dass diese immer noch von Bar aus aufgerufen werden können.

Alternativ, eine Option, die Hilfe hilft, aber das Problem nicht lösen, ist die Pimpl-Muster zu verwenden.Sie würden mit etwas enden wie folgt:

class FooImpl 
{ 
public: 
    virtual void aImp() = 0; 
    virtual void bImp() = 0; 
}; 

class Foo 
{ 
public: 
    void A() 
    { 
     ScopedLock lock(mutex); 
     m_impl->aImp(); 
    } 
    void B() 
    { 
     ScopedLock lock(mutex); 
     m_impl->bImp(); 
    } 

private: 
    FooImpl * m_impl; 
} 

Der Vorteil ist, dass in den Klassen von FooImpl abzuleiten, sie nicht mehr ein „Foo“ Objekt und können so nicht einfach als „A“ oder „B“.

+2

Ich denke, die Version mit dem Pimpl-Muster ist klarer. Es gibt eine gute Trennung von Bedenken: Klasse Foo ist verantwortlich für die Client-Schnittstelle und das Sperren, während FooImpl und abgeleitete Klassen den Algorithmus für A() und B() behandeln. Der Schlüssel ist, die Deklarationen für Foo und FooImpl in separaten Header-Dateien zu halten (der Header für Klasse Foo benötigt nur eine Vorwärtsdeklaration der Klasse FooImpl). Client-Code, der Foo verwendet, kennt FooImpl nicht, und Code, der eine von FooImpl abgeleitete Klasse implementiert, muss wissen, dass die Klasse Foo überhaupt existiert. –

+0

Einverstanden, ich denke Pimpl sieht aus wie der Weg zu gehen. Obwohl beide Vorschläge das Problem auf eine Weise lösen, die mich anspricht. Hilft mir dabei, mich immer wieder in den Fuß zu schießen. – Perculator

6

Ihr Mutex darf kein rekursiver Mutex sein. Wenn es kein rekursiver Mutex ist, führt ein zweiter Versuch, den Mutex im selben Thread zu sperren, zu diesem Thread-Blocking. Da dieser Thread den Mutex gesperrt hat, aber auf diesem Mutex blockiert ist, hast du einen Deadlock.

Sie wollen wahrscheinlich betrachten:

boost::recursive_mutex 

http://www.boost.org/doc/libs/1_32_0/doc/html/recursive_mutex.html

Es soll platform.Note Win32 CRITICAL_SECTION des rekursiven Mutex Verhalten Kreuz (über/Leavecriticalsection Geben Sie verwendet) implementieren sind rekursiv, was die schaffen würde Verhalten, das du beschreibst.

+0

Ich kannte die Terminologie für diese Art von Schloss nicht, also half Ihre Antwort. Wie jemand weiter unten erwähnt, scheint die Verwendung einer rekursiven Sperre der einfache Ausweg zu sein, also werde ich das wahrscheinlich nicht verwenden. Aber es war eine hilfreiche Antwort. – Perculator

1

Während eine rekursive Sperre Ihr Problem lösen würde, hatte ich immer das Gefühl, dass eine rekursive Sperre in vielen Situationen als einfache Ausweichmöglichkeit verwendet wird, die zu oft blockiert.

Ihr veröffentlichter Code ist offensichtlich zu Demonstrationszwecken vereinfacht, daher bin ich mir nicht sicher, ob dies zutrifft.

Als Beispiel, sagen wir, die Verwendung von Ressource X ist nicht threadsafe. Du hast so etwas wie.

A() { 
    ScopedLock 
    use(x) 
    aImp() 
    use(x) 
} 

aImp() { 
    ScopedLock 
    use(x) 
} 

Offensichtlich würde dies zu einem Deadlock führen.

Mit Ihren Schlössern viel schmaler würde jedoch das Problem beseitigen. Die Verwendung von Sperren in einem so kleinen Umfang wie möglich ist immer eine gute Idee, sowohl aus Performance-Gründen, als auch als Deadlock-Vermeidung.

A() { 
    { 
     ScopedLock 
     use(x) 
    } 
    aImp() 
    { 
     ScopedLock 
     use(x) 
    } 
} 

Sie erhalten die Idee.

Ich weiß, das ist nicht immer möglich (oder würde zu schrecklich ineffizienten Code führen), ohne weitere Details zu wissen, weiß ich nicht, ob es für Ihr Problem gilt. Aber ich dachte, es lohnt sich, es trotzdem zu veröffentlichen.