Asynchrone Systemaufrufe

Warum ein Threadpool komplizierter ist als asynchrone Calls

Samstag, 04. Februar 2012

Blockierende Systemaufrufe

Natürlich unterbricht jeder noch so kleine Systemaufruf die Ausführung des eigenen Programmes. Je nach Art des Aufrufes kommen so kürzere oder längere Wartezeiten auf die Rückkehr aus dem Kernel zustande. Unter einem blockierenden Aufruf verstehe ich hier das Warten auf eine Ressource, etwa auf eine Nachricht in der Netzwerkqueue des Prozesses oder das Laden von Daten auf der Festplatte.

Im Falle eines blockierenden Aufrufes ist es erforderlich, daß synchron kurz vor dem Aufruf ein neuer Thread gestartet wird und die Aufgaben des aktuellen übernimmt. Klingt einfach, ist es aber nicht.

Dazu ist zu sagen, daß jeder Systemaufruf künstlich unterschieden werden muß in blockierend und nichtblockierend. Zudem darf nie vergessen werden, im Falle des Blockierens, einen neuen Thread zu starten und diesen nach dem Aufruf auch wieder zu beenden. Ohne aspektorientierte Programmierumgebung bzw. einer anderen Möglichkeit zur Implementation von cross-cutting concerns ein Wartungsalptraum.

Ein weiteres Problem ist die Synchronisation zwischen den Ressourcen der einzelnen Tasks. Ein blockierter Task muß sich vor dem Systemaufruf aus der Liste der aktiven Tasks herausnehmen und die Ressourcen, mit denen er arbeitet, müssen speziell ausgezeichnet, d.h. gelockt werden, damit es zu keiner race condition kommt.

int reader_task(size_t datalen, char data[datalen])
{
   int err ;
   file_t file ;
   ...

   lock_data(datalen, data) ;

   enter_blocking_call() ; // remove task from active list
   // from here on another thread is scheduled for execution
   err = read_from_file(&file, datalen, data)
   leave_blocking_call() ; // set task as active 

   unlock_data(datalen, data) ;

   return err ;
}
Abbildung 1: Blockierender Thread

Asynchrone Systemaufrufe

Asynchrone Systemaufrufe sind in einigen Fällen nicht notwendig, wenn etwa aus einer Netzwerkqueue nur soviel an Daten gelesen werden, wie vorhanden sind. In anderen Fällen wie Dateizugriffe erfordern Sie eine spezielle, oft vom Betriebssystem abhängende Art des Zugriffs. In jedem Fall erfordern sie eine zusätzliche Verwaltung der Ressourcen, die gerade in asynchroner Bearbeitung sind.

Da jetzt aber immer nur ein Thread Zugriff auf die Ressourcen hält, sind Locking­operationen überflüssig. Aktuell in Bearbeitung befindliche Ressourcen sind allerdings in einer Warteschlange zu halten, bis die asynchrone Operation abgeschlossen ist. Statt eines Locks, der implizit eine Art von »in Bearbeitung«-Zustand setzt, ist dies jetzt durch die Warteliste explizit ersichtlich.

Aus den Ausführungen sollte klar werden, daß der Aufwand nicht höher ist, jedoch eine etwas andere Architektur erfordert mit dem großen Vorteil der expliziten Information, welche Ressourcen gerade in Bearbeitung sind. Ein weiterer, noch größerer Vorteil scheint mir zu sein, auf jegliche Locks (mutex_t) verzichten zu können und damit eine potenzielle Fehlerquelle komplett auszuschließen !

Zusätzlich ist noch eine Art von synchronem Task zu programmieren, im vorherigen Entwurfsdokument als exothread_t bezeichnet, im folgenden Beispiel als synctask_t. Auch wenn sich der Name noch ändern wird, so ist es mit dieser Technik möglich, ein Taskswitching in C zu implementieren, ohne auf Threads zurückgreifen zu müssen.

Sie können sich den Quellcode zur älteren exothread_t Implementierung anschauen. Wobei Funktionen wie waitevent noch nicht implementiert sind.

int reader_task(synctask_t * task,
                size_t datalen, char data[datalen])
{
   int err ;
   file_t         file ;
   eventid_t      eventid ;
   asyncioqueue_t * queue ;
   ...

   eventid = add_readcmd_asyncioqueue(&queue, &file,
                                      datalen, data) ;
   // put task in wait queue 
   waitevent_synctask(task, eventid) ;

   err = getstatus_asyncioqueue(&queue, eventid) ;
   remove_cmd_asyncioqueue(&queue, eventid) ;

   return err ;
}
Abbildung 2: Synchroner Task mit Warteliste