Mutex-Verfahren (TestAndSet, FB_IecCriticalSection) zum Absichern von kritischen Bereichen
Bei der Verwendung von Mutex-Verfahren bzw. bei der Implementierung eines gegenseitigen Ausschlusses werden die Bereiche, in denen konkurrierende Zugriffe stattfinden, als kritische Bereiche (engl. Critical Sections) bezeichnet. Diese Bereiche können mithilfe der Funktion TestAndSet() oder des Funktionsbausteins FB_IecCriticalSection (beide aus der SPS-Bibliothek Tc2_System) synchronisiert werden, sodass die Bereiche unter gegenseitigen Ausschluss gestellt werden und zu einem Zeitpunkt jeweils nur eine Task auf die gemeinsam genutzten Daten zugreifen kann.
Das Betreten eines kritischen Bereichs kann von einer oder mehrerer Bedingungen abhängen. Zudem können verschiedene kritische Bereiche von jeweils unterschiedlichen Bedingungen abhängen.
Beispiele
- Wenn der Bereich 1a lesend und der Bereich 1b schreibend auf die Daten „Data1“ zugreift, müssen die Bereiche 1a und 1b miteinander synchronisiert werden. Wenn die Bereiche 2a, 2b und 2c jeweils lesend und schreibend auf die Daten „Data2“ zugreifen, müssen die Bereiche 2a, 2b und 2c miteinander synchronisiert werden.
Die Bereiche 1x und 2x sind jeweils nur von einer Bedingung abhängig (Bedingung: „Data1“ bzw. „Data2“ nicht gesperrt). Zudem sind sie in diesem Fall nicht übergreifend voneinander abhängig, da in den Bereichen jeweils auf unterschiedliche Daten zugriffen wird. D. h. auch wenn Bereich 1b die Daten „Data1“ sperrt, kann Bereich 2a gleichzeitig die Daten „Data2“ sperren und auf diese zugreifen.
Somit müssen die Datenressourcen zwischen den folgenden kritischen Bereichen synchronisiert werden: - die Verwendung der Ressource „Data1“ zwischen den Bereichen 1a und 1b
- die Verwendung der Ressource „Data2“ zwischen den Bereichen 2a, 2b und 2c
- Wenn die Bereiche 1x und 2x zusätzlich jeweils auf die Daten „DataA“ zugreifen (dabei findet mindestens ein schreibender Zugriff statt), ändert sich die Situation.
Dann sind alle Bereiche 1x und 2x von zwei Bedingungen abhängig: Zum Betreten von Bereich 1x müssen die Daten „Data1“ sowie „DataA“ und zum Betreten von Bereich 2x müssen die Daten „Data2“ sowie „DataA“ freigegeben sein.
Zudem sind die Bereiche 1x und 2x aufgrund der gemeinsamen Nutzung der Daten „DataA“ nun übergreifend voneinander abhängig und dürfen nicht mehr gleichzeitig ausgeführt werden.
Somit müssen die Datenressourcen zwischen den folgenden kritischen Bereichen synchronisiert werden: - die Verwendung der Ressource „Data1“ zwischen den Bereichen 1a und 1b
- die Verwendung der Ressource „Data2“ zwischen den Bereichen 2a, 2b und 2c
- die Verwendung der Ressource „DataA“ zwischen allen Bereichen (1a, 1b, 2a, 2b, 2c)
Bei der Funktion TestAndSet() repräsentiert jeweils ein Flag (Boolesche Variable) eine Bedingung, von der das Betreten des kritischen Bereichs abhängt (z. B. bLockData1). Bei FB_IecCriticalSection wird jeweils eine Instanz des Funktionsbausteins als Sperrbedingung verwendet.
Blockierung
- TestAndSet(): Mit der Funktion kann die Belegung eines kritischen Bereiches markiert und geprüft werden. Die Funktion blockiert jedoch nicht und es besteht die Möglichkeit, dass der Bereich in einem Zyklus nicht durchlaufen werden kann.
- FB_IecCriticalSection: Wenn eine weitere Task durch Aufruf der Methode Enter() einen bereits belegten kritischen Abschnitt betreten will, wird sie durch den TwinCAT Scheduler blockiert. Die Task wird angehalten, bis der Bereich wieder freigegeben ist.
VORSICHT | |
Zykluszeitüberschreitung durch angehaltene Task Die Dauer der Taskblockierung kann je nach (Auslastung der) Zykluszeit zur Zykluszeitüberschreitung der Task führen.
|
Daraus ergibt sich der Vorteil der Funktion TestAndSet() gegenüber dem Funktionsbaustein FB_IecCriticalSection, dass eine Task in keinem Fall blockiert wird. Der Nachteil ist wiederum, dass für den Fall, dass die Funktion TestAndSet() keinen Zugriff gewährt, eine alternative Implementierung vorhanden sein muss. Dies könnte beispielsweise über eine Zustandsmaschine realisiert werden, um den Zugriff im nächsten Zyklus erneut zu versuchen.
Verklemmung (engl. deadlock)
Beachten Sie, dass es bei einer ungünstigen Verwendung des Funktionsbausteins FB_IecCriticalSection aufgrund der möglichen Blockierung von Tasks zu Verklemmungen kommen kann. An einer Verklemmung sind immer mindestens zwei Tasks beteiligt. Sie entsteht dann, wenn die Tasks während ihrer Blockierung gegenseitig auf die Freigabe einer weiteren Ressource warten, die die jeweils andere Task bereits gesperrt hat.
VORSICHT | |
Dauerhafter Taskstillstand durch Verklemmung Wenn eine so beschriebene Verklemmung einmal eingetreten ist, lässt sich diese nicht mehr programmatisch beseitigen. Es kommt zum dauerhaften Stillstand der beteiligten Tasks. |
Beispiel:
- Task 1 und 2 benötigen beide Zugriff auf die Daten „DataA“ und „DataB“.
- Task 1 sperrt zunächst die Ressource „DataA“ und Task 2 sperrt gleichzeitig zunächst die Ressource „DataB“.
- Anschließend möchte Task 1 die Daten „DataB“ sperren. Da diese Ressource bereits von Task 2 gesperrt ist, ist dies nicht möglich ist und Task 1 wird blockiert.
- Task 2 wiederum möchte zusätzlich zu „DataB“ noch die Daten „DataA“ sperren. Da diese bereits von Task 1 gesperrt sind, ist auch diese Sperrung nicht möglich und auch Task 2 wird blockiert.
- Somit sind beide Tasks blockiert. Sie warten jeweils auf die Freigabe von Daten, die die jeweils andere Task sperrt, aber aufgrund ihrer eigenen Blockierung nicht freigeben kann. Folglich warten die beiden Tasks unendlich lange aufeinander.
Auch bei einer falschen Synchronisation muss nicht zwangsläufig eine Verklemmungssituation eintreten. Es kommt nur dann zu einer Verklemmung, wenn sich die Abläufe zufällig in einer ungünstigen Reihenfolge ereignen.
Vermeidung von Verklemmungen
Generell können Sie Verklemmungen vermeiden, wenn jede Task immer nur eine Ressource zu einer Zeit sperren möchte.
Ebenso können Sie Verklemmungen dadurch vermeiden, dass Ressourcen immer nur in einer bestimmten Reihenfolge angefordert und gesperrt werden.
Beispiel:
- Das Problem bei dem zuvor genannten Beispiel ist, dass Task 1 und Task 2 die Ressourcen „DataA“ und „DataB“ in einer unterschiedlichen Reihenfolge anfordern und sperren.
- Wenn beide Tasks die Ressourcen hingegen immer in der gleichen Reihenfolge sperren, wird die Verklemmungsproblematik vermieden. Das heißt: Wenn jede Task, die in einem kritischen Bereich Zugriff auf die Daten „DataA“ und „DataB“ benötigt, immer zuerst „DataA“ anfordert und, wenn möglich, sperrt und erst bei erfolgreicher Sperrung von „DataA“ die Ressource „DataB“ anfordert und, wenn möglich, sperrt, kann es nicht zu der oben beschriebenen Verklemmung kommen, bei der sich die Tasks die benötigten Ressourcen in unterschiedlichen Reihenfolgen „wegschnappen“.
Beispielprogramm: Zugriffs-Synchronisation mittels TestAndSet()
Dieses Beispiel zeigt, wie aus verschiedenen Taskkontexten sicher auf gemeinsame Daten zugegriffen werden kann. Die Daten sind in einer Strukturinstanz zusammengefasst. Diese beinhaltet zusätzlich eine boolesche Variable bLocked als Test-Flag.
Bevor Sie auf Daten dieser globalen Strukturinstanz lesend oder schreibend zugreifen, müssen sie den Zugriff auf diese erfragen, indem Sie die Funktion TestAndSet() mit dem entsprechenden Test-Flag aufrufen. Wenn der Zugriff nicht gestattet ist, können Sie in diesem Zyklus nicht auf die Daten zugreifen. In diesem Fall muss applikativ eine Alternativbehandlung vorgesehen werden. Bei Bedarf wird der Zugriff im nächsten Zyklus erneut angefragt. Wenn der Zugriff gestattet ist, entsprechend TestAndSet() erfolgreich aufgerufen wurde, können Sie die Daten lesen oder verändern. Sobald Sie die Bearbeitung abgeschlossen haben, geben Sie den Zugriff wieder frei, indem Sie das Test-Flag mit FALSE belegen.
Funktionstest
Starten Sie das Beispielprogramm. Innerhalb MAIN1 und MAIN2 befindet sich jeweils eine Zählervariable (nLocalBlockedCounter), die bei fehlgeschlagenem TestAndSet()-Aufruf hochzählt. Wenn diese 0 ist, konnte der Datenzugriff auf die globalen Variablen bei jedem Versuch erfolgreich ausgeführt werden.
Um ein Gefühl für die Notwendigkeit der Zugriffs-Synchronisation zu erhalten, können Sie die zwei Tasks von unterschiedlichen CPU-Cores ausführen lassen. Wenn Sie nun das Beispielprogramm starten, dann sehen Sie, wie die Zählervariablen unregelmäßig hochzählen und somit anzeigen, dass der Datenzugriff ab und zu gesperrt war.
Die Zugriffs-Synchronisation ist jedoch nicht nur bei Multi‑Core‑Verwendung notwendig, sondern auch wenn die zwei Tasks auf einer CPU laufen. Durch gegenseitige Taskunterbrechungen kann es hier ebenso zu kritischen Inkonsistenzen bei ungesichertem Datenzugriff kommen.
Beispielprogramm: Zugriffs-Synchronisation mittels FB_IecCriticalSection
Windows CE Die Funktionalität vom FB_IecCriticalSection wird unter Windows-CE-Betriebssystemen ab TwinCAT v3.1.4022.29 unterstützt. |
Das Beispiel zeigt die Verwendung von Critical Sections in der SPS anhand von Geldtransfers bei Geldkonten. Ein Konto wird durch einen Funktionsbaustein FB_Account repräsentiert. Vier Konten sind beteiligt. Alle vier Funktionsbausteininstanzen sind in einer globalen Variablenliste deklariert, um den Zugriff (hier: Geldtransfer) aus verschiedenen Taskkontexten zu ermöglichen.
Jedes Konto hat zu Beginn einen Kontostand von 1000. Der Kontostand von jedem Konto kann mit den Methoden Get() und Set() ausgelesen und neu gesetzt werden. Die folgenden Geldtransfers sind in vier unterschiedlichen Taskkontexten implementiert.
Task 1: A->B 500
Task 2: B->C 250, B->D 250
Task 3: C->A 250
Task 4: D->A 250
Nach Abschluss dieser Geldtransfers sollte jedes Konto wieder über einen Betrag von 1000 verfügen.
Es muss sichergestellt werden, dass der Zugriff auf ein Konto niemals aus zwei Taskkontexten zugleich geschieht.
Im FB_Account ist der Funktionsbaustein FB_IecCriticalSection verwendet. Die Methode FB_Account.Lock() führt ein Enter() der Critical Section aus und die Methode FB_Account.Unlock() führt ein Leave() der Critical Section aus. Bevor auf den Kontostand eines Kontos zugegriffen werden darf, muss die Methode Lock() erfolgreich ausgeführt werden. Der Zugriff auf dieses Konto ist dann für andere gesperrt.
Um eine Verklemmung (engl. deadlock) zu vermeiden, wird eine Sperrreihenfolge festgelegt. Diese soll folgendermaßen definiert sein:
Sperrreihenfolge: A vor B vor C vor D
Damit ergibt sich als Entsperrreihenfolge: D vor C vor B vor A
Implementierung vom Geldtransfer innerhalb der Task 3:
(* Task 3: C->A 250*)
IF GVL.fbDepotA.Lock() THEN
IF GVL.fbDepotC.Lock () THEN
GVL.fbDepotC.Set (GVL.fbDepotC.Get() - 250);
GVL.fbDepotA.Set (GVL.fbDepotA.Get() + 250);
GVL.fbDepotC.Unlock();
END_IF
GVL.fbDepotA.Unlock();
END_IF
Funktionstest
Starten Sie das Beispielprogramm. Innerhalb Main1 können Sie die Summe aller Kontostände sehen, welche im SPS-Online-Ansicht immer 4000 betragen muss.
Um ein Gefühl für die Notwendigkeit der Verwendung von Critical Sections in diesem Beispiel zu erhalten, können Sie die vier Tasks von unterschiedlichen CPUs ausführen lassen und die globale Variable bIgnoreLock auf TRUE setzen. Wenn Sie nun das Beispielprogramm starten, dann sehen Sie, wie die Kontostände fehlerhafte Werte annehmen und die Summe aller Kontostände ebenfalls eine Fehlfunktion der Geldtransfers belegt.
Die Zugriffs-Synchronisation ist jedoch nicht nur bei Multi-Core-Verwendung notwendig, sondern auch wenn die vier Tasks auf einem CPU-Core laufen. Durch gegenseitige Taskunterbrechungen kann es hier ebenso zu kritischen Inkonsistenzen bei ungesichertem Datenzugriff kommen.
Download: TC3_PlcSample_MultiTaskSync_IecCriticalSection.zip