{"id":1315,"date":"2012-08-26T01:40:34","date_gmt":"2012-08-25T23:40:34","guid":{"rendered":"http:\/\/blog.benny-baumann.de\/?p=1315"},"modified":"2012-08-26T01:55:23","modified_gmt":"2012-08-25T23:55:23","slug":"threads-und-fibers","status":"publish","type":"post","link":"https:\/\/blog.benny-baumann.de\/?p=1315","title":{"rendered":"Threads und Fibers"},"content":{"rendered":"<p>F\u00fcr ein Projekt, welches ich bereits seit etwas l\u00e4ngerer Zeit vorbereite, ben\u00f6tige ich eine sehr flexible IO-Schicht, mit der ich eine Reihe verschiedener T\u00e4tigkeiten wie IO und anderer Events m\u00f6glichst flexibel parallelisieren kann. Nun gibt es f\u00fcr solche Aufgaben zwar \u00fcblicherweise Threads, aber da die Aufgaben zum einen sehr kurzweilig sind, andererseits aber unter gewissen Umst\u00e4nden blockieren k\u00f6nnen, funktioniert der Ansatz \u00fcber Thread Pools nur bedingt. Eine vollst\u00e4ndige asynchrone Bearbeitung der Ereignisse scheided auf Grund der Komplexit\u00e4t aber auch aus, da das System leicht erweiterbar bleiben muss. Was also ben\u00f6tigt wurde, ist ein Mittelweg aus beiden Ans\u00e4tzen.<\/p>\n<p>Ein Ansatz f\u00fcr einen solchen Mittelweg bieten Fibers, die analog zu POSIX Threads dem Programm erlauben, mehrere Ausf\u00fchrungsstr\u00e4nge zu erzeugen und damit die Abl\u00e4ufe in der Anwendung zu parallelisieren. Fibers fungieren dabei vollst\u00e4ndig im User Mode und sind dadurch gegen\u00fcber PThreads oder gar geforkten Prozessen wesentlich leichtgewichtiger beim Wechseln des Zustands.<!--more--><\/p>\n<p>Und genau diese Leichtgewichtigkeit wollte ich in meinem Projekt gerne nutzen: Jede Aufgabe soll als ein eigener Ausf\u00fchrungsstrang betrachtet werden, wobei spezifische Funktionen, die blockieren k\u00f6nnen, leicht in einen separaten Ausf\u00fchrungsstrang verlegt werden k\u00f6nnen sollten. Sollte hierbei festgestellt werden, dass f\u00fcr die Ausf\u00fchrung einer Aufgabe ggf. blockiert werden muss, so soll tas Blockieren der anfragenden Aufgabe nicht gleichzeitig den ausf\u00fchrenden Thread des Thread-Pools belegen, sondern stattdessen auf eine andere Aufgabe wechseln, die in der Zwischenzeit ausgef\u00fchrt werden kann.<\/p>\n<p>Ein kleines Beispiel hierzu: Wenn man in einer Netzwerk-Anwendung f\u00fcr die Behandlung von IO R\u00fcckfragen ausf\u00fchren muss, muss man ein Event, welches man gerade frisch hereinbekommen hat, kurzzeitig beiseite legen. Gleichzeitig m\u00f6chte man aber auch die Menge der ben\u00f6tigten Resourcen (z.B. ge\u00f6ffnete Threads) m\u00f6glichst gering halten. Dies kann z.B. im Falle einer DNS-Anfrage der Fall sein, die f\u00fcr die Entscheidung, ob ein Client erlaubt wird, oder nicht, n\u00f6tig ist. W\u00fcrde man die Bearbeitung an dieser Stelle synchron durchf\u00fchren, m\u00fcsste die Verarbeitung weiterer Events ggf. warten, bis die DNS-Anfrage durch ist. W\u00fcrde man die Abfrage asynchron durchf\u00fchren, w\u00fcrde dies den Code f\u00fcr die Behandlung des Abfrage-Ergebnisses im Programm stark verstreuen. Also warum nicht so etwas?<\/p>\n<pre lang=\"cpp\" escaped=\"true\" highlight=\"8,9\">bool Client::handleAccept() {\r\n    string hostname = DNS::getHostNameByIP(this-&gt;ip);\r\n    return \"localhost\" == hostname;\r\n}\r\n\r\nstring DNS::getHostNameByIP(const IP&amp; ip) {\r\n    DNSQuery query(DNS::RecordType::PTR, ip);\r\n    Scheduler::AddTask(DNS::internalResolve, query);\r\n    Scheduler::yield();\r\n    return query-&gt;success ? query-&gt;toString() : \"\";\r\n}<\/pre>\n<p>Nun bringt es reichlich wenig, wenn man jede Teilaufgabe einfach nur so verz\u00f6gern w\u00fcrde, wenn man jedoch die Zeit zwischen Stellen der Anfrage und deren Ausf\u00fchrung nutzt, um diese noch anzupassen (f\u00fcr einen Hostnamen wird zuerst ein A Record angefragt, ein anderer Task brauch aber auch den passenden AAAA und MX), so kann dies enorm die Effizienz steigern, ohne dass der Code un\u00fcbersichtlicher wird.<\/p>\n<p>Au\u00dferdem kann man sich auf diese Weise sehr elegant die eigentlich im Hintergrund stattfindende IO vom Hals halten, diese aber dennoch leicht b\u00fcndeln, ohne sich an jeder Stelle mit der parallelen Ausf\u00fchrung befassen zu m\u00fcssen. Hat man beispielsweise einen 1-zu-n-Filter, so l\u00e4sst sich dieser einmalig ansto\u00dfen, und durch einfaches Synchronisieren auf alle Events auf dessen Abschluss warten:<\/p>\n<pre lang=\"cpp\" escaped=\"true\" highlight=\"3,5\">bool Client::checkPermissions() {\r\n    for(PermissionCheck c: permissionchecks) {\r\n        Scheduler::WaitFor(Scheduler::AddTask(c-&gt;perform, this));\r\n    }\r\n    Scheduler::Yield();\r\n    for(PermissionCheck c: permissionchecks) {\r\n        if(!c-&gt;success) return false;\r\n    }\r\n    return true;\r\n}<\/pre>\n<p>Durch diese Art der kooperativen Aufgabenverteilung ist es m\u00f6glich, Aufgaben parallel zu stellen, die ansonsten seriel abgearbeitet werden w\u00fcrden. Durch diese Parallelisierung k\u00f6nnen lange Wartezeiten, die bspw. durch Netzwerk-Latenzen verursacht werden, abgefedert werden, ohne dass man sich um diese Details k\u00fcmmern muss.<\/p>\n<p>Um genau diesen Ansatz einmal zu testen, ist nun folgende Testimplementierung des Ansatzes entstanden (dezeit noch ohne Multithreading). Die Erweiterung um Multithreading bei der Abarbeitung der Tasks kann hierbei z.B. mittels eines Thread-Pools oder anderen Mechanismen realisiert werden. Aber hier einmal der (blanke) Proof-of-Concept:<\/p>\n<pre lang=\"cpp\" escaped=\"true\">#include &lt;list&gt;\r\n\r\n#include &lt;stdio.h&gt;\r\n\r\n#include &lt;ucontext.h&gt;\r\n\r\nucontext_t ctx_default;\r\n\r\nclass Fiber {\r\n\r\n\u00a0\u00a0\u00a0 static const int stack_size = 8192;\r\n\r\npublic:\r\n\r\n\u00a0\u00a0\u00a0 typedef void (* fiber_proc_t)(Fiber&amp; fiber);\r\n\r\n\u00a0\u00a0\u00a0 Fiber(fiber_proc_t proc): active(true) {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 this-&gt;proc = proc;\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ctx = new ucontext_t();\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 getcontext(ctx);\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ctx-&gt;uc_stack.ss_size = Fiber::stack_size;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ctx-&gt;uc_stack.ss_sp = new char[Fiber::stack_size];\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ctx-&gt;uc_link = &amp;ctx_default;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 makecontext(ctx, reinterpret_cast&lt;void (*)()&gt;(Fiber::runInternal), 1, this);\r\n\u00a0\u00a0\u00a0 }\r\n\r\n\u00a0\u00a0\u00a0 virtual ~Fiber() {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 delete[] (char *)ctx-&gt;uc_stack.ss_sp;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 delete ctx;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 ctx = 0;\r\n\u00a0\u00a0\u00a0 }\r\n\r\n\u00a0\u00a0\u00a0 bool active;\r\n\r\n\u00a0\u00a0\u00a0 static std::list&lt;Fiber *&gt; fibers;\r\n\r\nprivate:\r\n\u00a0\u00a0\u00a0 ucontext_t *ctx;\r\n\u00a0\u00a0\u00a0 fiber_proc_t proc;\r\n\r\n\u00a0\u00a0\u00a0 static void runInternal(Fiber *f) {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 if(f) f-&gt;run();\r\n\u00a0\u00a0\u00a0 }\r\n\r\n\u00a0\u00a0\u00a0 void run() {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 if(proc) {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 proc(*this);\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 }\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 active = false;\r\n\u00a0\u00a0\u00a0 }\r\n\r\npublic:\r\n\u00a0\u00a0\u00a0 void yield() {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 swapcontext( this-&gt;ctx, &amp;ctx_default );\r\n\u00a0\u00a0\u00a0 }\r\n\r\n\u00a0\u00a0\u00a0 friend void mainfiber(void);\r\n};\r\n\r\nstd::list&lt;Fiber *&gt; Fiber::fibers;\r\n\r\nvoid test1(Fiber&amp; fiber);\r\nvoid test2(Fiber&amp; fiber);\r\n\r\nvoid test1(Fiber&amp; fiber) {\r\n\u00a0\u00a0\u00a0 printf(\"t1: start: %p\\n\", &amp;fiber);\r\n\u00a0\u00a0\u00a0 Fiber *sub = new Fiber(test2);\r\n\u00a0\u00a0\u00a0 Fiber::fibers.push_back(sub);\r\n\u00a0\u00a0\u00a0 fiber.yield();\r\n\u00a0\u00a0\u00a0 printf(\"t1: mid:\u00a0\u00a0 %p\\n\", &amp;fiber);\r\n\u00a0\u00a0\u00a0 fiber.yield();\r\n\u00a0\u00a0\u00a0 printf(\"t1: stop:\u00a0 %p\\n\", &amp;fiber);\r\n}\r\n\r\nvoid test2(Fiber&amp; fiber) {\r\n\u00a0\u00a0\u00a0 printf(\"t2: start: %p\\n\", &amp;fiber);\r\n\u00a0\u00a0\u00a0 fiber.yield();\r\n\u00a0\u00a0\u00a0 printf(\"t2: mid:\u00a0\u00a0 %p\\n\", &amp;fiber);\r\n\u00a0\u00a0\u00a0 fiber.yield();\r\n\u00a0\u00a0\u00a0 printf(\"t2: stop:\u00a0 %p\\n\", &amp;fiber);\r\n}\r\n\r\nvoid mainfiber(void) {\r\n\u00a0\u00a0\u00a0 while(!Fiber::fibers.empty()) {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Fiber *f = Fiber::fibers.front();\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Fiber::fibers.pop_front();\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 if(!f-&gt;active) {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 delete f;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 continue;\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 }\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Fiber::fibers.push_back(f);\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 swapcontext(&amp;ctx_default, f-&gt;ctx);\r\n\u00a0\u00a0\u00a0 }\r\n}\r\n\r\nint main (void) {\r\n\u00a0\u00a0\u00a0 Fiber *f;\r\n\r\n\u00a0\u00a0\u00a0 f = new Fiber(test1);\r\n\u00a0\u00a0\u00a0 Fiber::fibers.push_back(f);\r\n\r\n\u00a0\u00a0\u00a0 f = new Fiber(test1);\r\n\u00a0\u00a0\u00a0 Fiber::fibers.push_back(f);\r\n\r\n\u00a0\u00a0\u00a0 f = new Fiber(test1);\r\n\u00a0\u00a0\u00a0 Fiber::fibers.push_back(f);\r\n\r\n\u00a0\u00a0\u00a0 mainfiber();\r\n\r\n\u00a0\u00a0\u00a0 return 0;\r\n}<\/pre>\n<p>Wer sich nun wundert, wo hier die Behandlung von Abh\u00e4ngigkeiten, Warten auf Events und \u00e4hnliches ist, der sei beruhigt: Die Sachen fehlen im PoC, weil sie f\u00fcr den einfachsten Fall unabh\u00e4ngiger Aufgaben nicht ben\u00f6tigt werden. Das Scheduling wird \u00fcbrigens von mainfiber realisiert, test1 und test2 sind zwei Aufgaben, die u.U. lange IO-Events haben k\u00f6nnten. Dieser Code-Stand ist zwar bei weitem nicht optimal und bei weitem noch nicht vollst\u00e4ndig, da aber der grundlegende Ansatz bereits funktioniert, l\u00e4sst sich damit gut weiterarbeiten.<\/p>\n<p>Bliebe jetzt nur noch die Realisierung des Schedulers und der Event-Verarbeitung f\u00fcr IO und Timing \u00fcbrig. Aber daf\u00fcr hab ich auch schon Ans\u00e4tze \ud83d\ude09 Auch wenn jetzt nicht alles klar geworden sein sollte, kann man sich zumindest folgendes im Hinterkopf behalten: Mit Threads kann man parallelisieren, mit Fibers kann man Warten inerhalb von Threads mit sinnvollen Aufgaben \u00fcberbr\u00fccken.<\/p>\n<p>P.S.: Beim vorliegenden Programm beschwert sich Valgrind \u00fcber den wandernden Stack. Nunja, das Programm ist trotzdem korrekt und ohne Speicherl\u00f6cher ;-P<\/p>\n<p class=\"wp-flattr-button\"><a href=\"https:\/\/blog.benny-baumann.de\/?flattrss_redirect&amp;id=1315&amp;md5=3d64aa4f99c1e3da8fb6d4c3c616e666\" title=\"Flattr\" target=\"_blank\"><img src=\"http:\/\/blog.benny-baumann.de\/wp-content\/plugins\/flattr\/img\/flattr-badge-large.png\" srcset=\"http:\/\/blog.benny-baumann.de\/wp-content\/plugins\/flattr\/img\/flattr-badge-large.png\" alt=\"Flattr this!\"\/><\/a><\/p>","protected":false},"excerpt":{"rendered":"<p>F\u00fcr ein Projekt, welches ich bereits seit etwas l\u00e4ngerer Zeit vorbereite, ben\u00f6tige ich eine sehr flexible IO-Schicht, mit der ich eine Reihe verschiedener T\u00e4tigkeiten wie IO und anderer Events m\u00f6glichst flexibel parallelisieren kann. Nun gibt es f\u00fcr solche Aufgaben zwar \u00fcblicherweise Threads, aber da die Aufgaben zum einen sehr kurzweilig sind, andererseits aber unter gewissen [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[29],"tags":[98,329,69,322],"class_list":["post-1315","post","type-post","status-publish","format-standard","hentry","category-software","tag-developement","tag-fibers","tag-internet","tag-threads"],"_links":{"self":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts\/1315","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1315"}],"version-history":[{"count":8,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts\/1315\/revisions"}],"predecessor-version":[{"id":1323,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=\/wp\/v2\/posts\/1315\/revisions\/1323"}],"wp:attachment":[{"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1315"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1315"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.benny-baumann.de\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1315"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}