Library: policy for self-managed thread

...after resolving the fundamental design problems,
a policy mix-in can be defined now for a thread that deletes
its own wrapper at the end of the thread-function.

Such a setup would allow for »fire-and-forget« threads, but with
wrapper and ensuring safe allocations. The prominent use case
for such a setup would be the GUI-Thread.
This commit is contained in:
Fischlurch 2023-10-10 02:55:23 +02:00
parent dd2fe7da59
commit 5f9683ef10
5 changed files with 197 additions and 63 deletions

View file

@ -67,17 +67,17 @@ namespace thread{
void
ThreadWrapper::markThreadStart (string id)
ThreadWrapper::markThreadStart()
{
TRACE (thread, "%s", lifecycleMsg ("start...", id).c_str());
TRACE (thread, "%s", lifecycleMsg ("start...", threadID_).c_str());
setThreadName();
}
void
ThreadWrapper::markThreadEnd(string id)
ThreadWrapper::markThreadEnd()
{
TRACE (thread, "%s", lifecycleMsg ("finished.", id).c_str());
TRACE (thread, "%s", lifecycleMsg ("finished.", threadID_).c_str());
}

View file

@ -123,6 +123,7 @@
#include "lib/error.hpp"
#include "lib/nocopy.hpp"
#include "include/logging.h"
#include "lib/meta/trait.hpp"
#include "lib/format-util.hpp"
#include "lib/result.hpp"
@ -207,10 +208,15 @@ namespace lib {
/** detect if the currently executing code runs within this thread */
bool invokedWithinThread() const;
void markThreadStart(string);
void markThreadEnd (string);
void markThreadStart();
void markThreadEnd ();
void setThreadName ();
void waitGracePeriod() noexcept;
/* empty implementation for some policy methods */
void handle_begin_thread() { }
void handle_after_thread() { }
void handle_thread_still_running() { }
};
@ -222,7 +228,7 @@ namespace lib {
* - thread detaches before terminating
* - »grace period« for thread to terminate on shutdown
*/
template<class BAS, typename>
template<class BAS, typename=void>
struct PolicyLaunchOnly
: BAS
{
@ -240,7 +246,7 @@ namespace lib {
}
void
handle_end_of_thread()
handle_after_thread()
{
if (BAS::isLive())
BAS::threadImpl_.detach();
@ -254,6 +260,36 @@ namespace lib {
};
/**
* Thread Lifecycle Policy Extension:
* additionally self-manage the thread-wrapper allocation.
* @warning the thread-wrapper must have been heap-allocated.
*/
template<class BAS, class TAR>
struct PolicySelfManaged
: PolicyLaunchOnly<BAS>
{
using BasePol = PolicyLaunchOnly<BAS>;
using BasePol::BasePol;
void
handle_after_thread()
{
TAR* selfAllocation = static_cast<TAR*>(
static_cast<void*> (this));
if (BAS::isLive())
BAS::threadImpl_.detach();
delete selfAllocation;
}
void
handle_thread_still_running()
{
ALERT (thread, "Self-managed thread was deleted from outside. Abort.");
}
};
/**
* Thread Lifecycle Policy:
* - thread with the ability to publish results
@ -286,7 +322,7 @@ namespace lib {
}
void
handle_end_of_thread()
handle_after_thread()
{
/* do nothing -- thread must be joined manually */;
}
@ -312,11 +348,11 @@ namespace lib {
void
invokeThreadFunction (ARGS&& ...args)
{
string id{Policy::threadID_}; // local copy
Policy::markThreadStart(id);
Policy::handle_begin_thread();
Policy::markThreadStart();
Policy::perform_thread_function (forward<ARGS> (args)...);
Policy::markThreadEnd(id);
Policy::handle_end_of_thread();
Policy::markThreadEnd();
Policy::handle_after_thread();
}
@ -490,7 +526,7 @@ namespace lib {
* @deprecated can't sleep well while this function is exposed;
* need a prime solution to address this relevant use case ////////////////////////////////////////OOO allow for a thread with explicit lifecycle
*/
void detach() { ThreadLifecycle::handle_end_of_thread(); }
void detach() { ThreadLifecycle::handle_after_thread(); }
};
@ -541,5 +577,34 @@ namespace lib {
/************************************************************************//**
* Special configuration for a »fire-and-forget«-Thread.
* @internal this class is meant for subclassing. Start with #launchDetached()
* @tparam TAR the concrete type of the subclass to be started as autonomous,
* self-managed thread. Must be passed down since thread deletes itself.
*/
class ThreadAutonomous
: public thread::ThreadLifecycle<thread::PolicySelfManaged, ThreadAutonomous>
{
using Impl = thread::ThreadLifecycle<thread::PolicySelfManaged, ThreadAutonomous>;
public:
using Impl::Impl;
};
/**
* Launch an autonomous self-managing thread (and forget about it).
* The thread-wrapper is allocated to the heap and will delete itself on termination.
* @tparam TAR concrete type of the subclass to be started as autonomous detached thread.
* @param args a valid argument list to call the ctor of thread::ThreadLifecycle
*/
template<typename...INVO>
inline void
launchDetached (INVO&& ...args)
{
new ThreadAutonomous{forward<INVO> (args)...}; // Thread will pick up and manage *this
}
} // namespace lib
#endif /*LIB_THREAD_H*/

View file

@ -144,7 +144,7 @@ namespace control {
{
return not queue_.empty();
})
, thread_{"Lumiera Session"
, thread_{"Session"
,&DispatcherLoop::runSessionThread
, this, notification}
{

View file

@ -27,19 +27,15 @@
#include "lib/test/run.hpp"
#include "lib/thread.hpp"
#include "lib/iter-explorer.hpp"
#include "lib/scoped-collection.hpp"
#include "lib/test/microbenchmark.hpp"
#include <atomic>
#include <chrono>
using test::Test;
using lib::explore;
using std::atomic_uint;
using std::this_thread::yield;
using std::this_thread::sleep_for;
using std::chrono::microseconds;
using namespace std::chrono_literals;
namespace lib {
@ -74,11 +70,10 @@ namespace test{
demonstrateSimpleUsage()
{
atomic_uint i{0};
Thread thread("counter", [&]{ ++i; }); // bind a λ and launch thread
while (thread) yield(); // ensure thread has finished and detached
launchDetached ("anarchy", [&]{ ++i; });
sleep_for(1ms);
CHECK (i == 1); // verify the effect has taken place
UNIMPLEMENTED ("actually launch detached");
}
@ -88,6 +83,7 @@ namespace test{
void
verifyMemoryManagement()
{
UNIMPLEMENTED ("verify thread manages itself");
struct TestThread
: Thread
{
@ -99,39 +95,10 @@ namespace test{
doIt (uint a, uint b) ///< the actual operation running in a separate thread
{
uint sum = a + b;
sleep_for (microseconds{sum}); // Note: explicit random delay before local store
// sleep_for (microseconds{sum}); // Note: explicit random delay before local store
local = sum;
}
};
// prepare Storage for these objects (not created yet)
lib::ScopedCollection<TestThread> threads{NUM_THREADS};
size_t checkSum = 0;
size_t globalSum = 0;
auto launchThreads = [&]
{
for (uint i=1; i<=NUM_THREADS; ++i)
{
uint x = rand() % 1000;
globalSum += (i + x);
threads.emplace (&TestThread::doIt, i, x);
} // Note: bind to member function, copying arguments
while (explore(threads).has_any())
yield(); // wait for all threads to have detached
for (auto& t : threads)
{
CHECK (0 < t.local);
checkSum += t.local;
}
};
double runTime = benchmarkTime (launchThreads, REPETITIONS);
CHECK (checkSum == globalSum); // sum of precomputed random numbers matches sum from threads
CHECK (runTime < NUM_THREADS * 1000/2); // random sleep time should be > 500ms on average
}
};

View file

@ -65196,6 +65196,13 @@
</p>
</body>
</html></richcontent>
<node CREATED="1696890454577" ID="ID_430553780" MODIFIED="1696890460170" TEXT="geht so gar nicht">
<icon BUILTIN="stop-sign"/>
</node>
<node CREATED="1696890461256" ID="ID_1183918146" MODIFIED="1696890497276" TEXT="ein unique_ptr w&#xfc;rde ja nie gel&#xf6;scht">
<icon BUILTIN="messagebox_warning"/>
</node>
<node CREATED="1696890472062" ID="ID_484865968" MODIFIED="1696890493175" TEXT="&#x27f9; vielmehr mu&#xdf; sich das Objekt selber einfach vom Heap l&#xf6;schen"/>
</node>
<node CREATED="1696529718984" ID="ID_1539716722" MODIFIED="1696529944892" TEXT="zus&#xe4;tzlichen Erweiterungspunkt nutzen">
<arrowlink COLOR="#fdfcc6" DESTINATION="ID_1373519021" ENDARROW="Default" ENDINCLINATION="-114;10;" ID="Arrow_ID_736802000" STARTARROW="None" STARTINCLINATION="116;8;"/>
@ -65234,10 +65241,76 @@
<icon BUILTIN="forward"/>
</node>
</node>
<node CREATED="1696532299942" ID="ID_596357210" MODIFIED="1696532380050" TEXT="die jeweilige Front-End-Klasse nur in der Factory definieren">
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1696898745626" ID="ID_1404625774" MODIFIED="1696898766166" TEXT="mu&#xdf; einen gewaltsamen Upcast machen">
<icon BUILTIN="messagebox_warning"/>
<node CREATED="1696898767838" ID="ID_982197270" MODIFIED="1696898773034" TEXT="und zwar &#xfc;ber void*"/>
<node CREATED="1696898773597" ID="ID_1027296232" MODIFIED="1696898787176" TEXT="weil wir einmal protected-Inheritance verwenden"/>
<node CREATED="1696898788100" ID="ID_229678951" MODIFIED="1696898804634" TEXT="&quot;PolicyAutonomous&quot; is inaccessible base">
<font ITALIC="true" NAME="SansSerif" SIZE="12"/>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1696898808050" ID="ID_649658845" MODIFIED="1696898912722" TEXT="w&#xe4;re potentiell gef&#xe4;hrlich">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
und zwar, sofern oben in der konkreten Subklasse irgendwo ein mix-in verwendet wird &#8212; in dem Fall m&#252;&#223;te n&#228;mlich der Pointer nachjustiert werden
</p>
</body>
</html></richcontent>
<icon BUILTIN="clanbomber"/>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1696532299942" ID="ID_596357210" MODIFIED="1696898738780" TEXT="die jeweilige Front-End-Klasse nur in der Factory definieren">
<icon BUILTIN="yes"/>
</node>
</node>
<node BACKGROUND_COLOR="#f0d5c5" COLOR="#990033" CREATED="1696897248135" ID="ID_433063268" MODIFIED="1696897258683" TEXT="Subklassen erm&#xf6;glichen?">
<icon BUILTIN="help"/>
<node CREATED="1696897270002" ID="ID_944165024" MODIFIED="1696897420063">
<richcontent TYPE="NODE"><html>
<head>
</head>
<body>
<p>
<i>ohne Subclassing</i>&#160;ist die ganze &#220;bung etwas &#252;berzogen
</p>
</body>
</html></richcontent>
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
...es ist ja nett, da&#223; der Thread-Wrapper sich selbst verwaltet &#8212; und der ganze Aufwand blo&#223; f&#252;r zwei interne Datenfelder?&#160;&#160;w&#228;re da nicht ein &#187;workaround&#171; angemessener?
</p>
</body>
</html></richcontent>
</node>
<node CREATED="1696897426970" ID="ID_419436552" MODIFIED="1696898701788" TEXT="mit Subklasse wird aber das Front-End fragiler &#x2014; und beliebiger">
<richcontent TYPE="NOTE"><html>
<head>
</head>
<body>
<p>
wozu dann &#252;berhaupt noch das Front-End? Um einen etwas sonderbaren new-Aufruf einzupacken?
</p>
</body>
</html></richcontent>
</node>
<node CREATED="1696897485658" ID="ID_885031549" MODIFIED="1696897505971" TEXT="Problem: mu&#xdf; die konkrete Klasse als Template-Parameter in die Policy einf&#xfc;hren">
<icon BUILTIN="messagebox_warning"/>
<node CREATED="1696897535396" ID="ID_1827915916" MODIFIED="1696897641026" TEXT="das macht die ganze Kette ziemlich komplex"/>
<node CREATED="1696897641633" ID="ID_309942358" MODIFIED="1696897719398" TEXT="auch das Front-End braucht nun einen Template-Parameter"/>
<node BACKGROUND_COLOR="#fdfdcf" COLOR="#ff0000" CREATED="1696897720103" ID="ID_785903718" MODIFIED="1696898648512" TEXT="noch schlimmer: das verhindert Verwendung ohne Subclassing">
<icon BUILTIN="stop-sign"/>
</node>
</node>
</node>
</node>
<node CREATED="1696532404400" ID="ID_1917558827" MODIFIED="1696539443047" TEXT="Variante-2 : optional-Lifecycle">
<linktarget COLOR="#898bab" DESTINATION="ID_1917558827" ENDARROW="Default" ENDINCLINATION="-1027;99;" ID="Arrow_ID_1553665185" SOURCE="ID_104020495" STARTARROW="None" STARTINCLINATION="532;77;"/>
@ -65571,7 +65644,7 @@
</body>
</html></richcontent>
<icon BUILTIN="messagebox_warning"/>
<node CREATED="1696534813654" ID="ID_1412510556" MODIFIED="1696535910711" TEXT="was aber exterm unwahrscheinlich ist">
<node CREATED="1696534813654" ID="ID_1412510556" MODIFIED="1696890781755" TEXT="was aber exterm unwahrscheinlich ist">
<richcontent TYPE="NOTE"><html>
<head/>
<body>
@ -65586,11 +65659,11 @@
das Kopieren des Funktors und der Argumente ebenfalls ohne Probleme erfolgt sein
</li>
<li>
aber <i>danach</i>&#160;ein Problem <i>im bereits eingerichteten Thread</i>&#160;auftreten, bevor unser Code den Manager (smart-ptr) aktivieren kann.
aber <i>danach</i>&#160;ein Problem <i>im bereits eingerichteten Thread</i>&#160;auftreten, bevor unser in den try-catch-Block eintritt.
</li>
</ul>
<p>
Hierzu kommt nur wenig in Frage: einmal die <i>Invocation</i>&#160;des Funktors selber mit als Wert vorliegenden Parametern, sowie dann unser eigener Library-Code bis zu der Stelle, an der der Pointer <i>gesichert</i>&#160;ist. Da sehe ich im Moment wenig Raum f&#252;r Fehler. Funktoren sind ja entweder Pointer oder Objekte, und durch die erzwungene Kopie sind die tats&#228;chlich dann aktiven Instanzen bereits konstruiert. Ein std::function verwendet zwar einen <i>Invoker, </i>aber das ist ein Trampolin, um die verschiedenen Aufruf-Technologien zu nivellieren; mir ist nicht bekannt, da&#223; das <i>f&#252;r den Aufruf</i>&#160;noch irgend etwas macht, was scheitern k&#246;nnte. Bleiben also nur noch <b>spezielle esoterische Argument-Typen</b>. Und au&#223;erdem m&#252;&#223;te der Optimiser so d&#228;mlich sein, ein bereits kopiertes Argument<i>&#160;noch einmal zu kopieren</i>&#160;nur f&#252;r den Aufruf. Abgesehen davon k&#246;nnten diese &#187;esoterischen&#171; Typen nur bei den weiteren Funktions-Argumenten auftreten (nicht der Funktor, nicht der this-Pointer). Danach haben wir nur noch einen nicht-virtuellen Aufruf in die Policy (ist inline) und das speichern des this-Pointers in den smart-ptr. Wenn <i>danach</i>&#160;noch was passiert, terminiert der Thread, der Funktor wird de-alloziert, und der darin eingebettete smart-ptr waltet seines Amtes
Hierzu kommt nur wenig in Frage: einmal die <i>Invocation</i>&#160;des Funktors selber mit als Wert vorliegenden Parametern, sowie dann unser eigener Library-Code bis zu der Stelle, an der der control-flow in den try-catch eintritt, sowie das Logging danach. Da sehe ich im Moment wenig Raum f&#252;r Fehler. Funktoren sind ja entweder Pointer oder Objekte, und durch die erzwungene Kopie sind die tats&#228;chlich dann aktiven Instanzen bereits konstruiert. Ein std::function verwendet zwar einen <i>Invoker, </i>aber das ist ein Trampolin, um die verschiedenen Aufruf-Technologien zu nivellieren; mir ist nicht bekannt, da&#223; das <i>f&#252;r den Aufruf</i>&#160;noch irgend etwas macht, was scheitern k&#246;nnte. Bleiben also nur noch <b>spezielle esoterische Argument-Typen</b>. Und au&#223;erdem m&#252;&#223;te der Optimiser so d&#228;mlich sein, ein bereits kopiertes Argument<i>&#160;noch einmal zu kopieren</i>&#160;nur f&#252;r den Aufruf. Abgesehen davon k&#246;nnten diese &#187;esoterischen&#171; Typen nur bei den weiteren Funktions-Argumenten auftreten (nicht der Funktor, nicht der this-Pointer). Danach haben wir nur noch nicht-virtuelle Aufrufe in die Policy (ist inline) und das Logging &#8212; <i>dieses</i>&#160; stellt vermutlich <i>die gr&#246;&#223;te Gefahr dar; </i>da hier aber kein sch&#252;tzender try-catch mehr dar&#252;ber liegt, f&#252;hren Fehler hier sofort zu std::terminate.
</p>
</body>
</html></richcontent>
@ -65769,8 +65842,7 @@
<font color="#1b0d7b" face="Monospaced" size="2">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;.atEnd(terminationHook)}); </font>
</p>
</body>
</html>
</richcontent>
</html></richcontent>
<icon BUILTIN="idea"/>
</node>
<node BACKGROUND_COLOR="#d2beaf" COLOR="#5c4d6e" CREATED="1696774321215" ID="ID_1696275940" MODIFIED="1696774330486" TEXT="Suche nach sonstigen Alternativen vertagt">
@ -65829,8 +65901,7 @@
wenn halt Argument-Packs einfach als Typ repr&#228;sentierbar w&#228;ren &#8212; aber dem ist nicht so; man mu&#223; gegen ein getemplatetes Argument matchen, um aus einem Argument-Pack einen anderen Argument-Pack zu konstruieren
</p>
</body>
</html>
</richcontent>
</html></richcontent>
</node>
<node CREATED="1696812473885" ID="ID_1487641552" MODIFIED="1696812488838" TEXT="wie &#xfc;blich: std::index_sequence_for&lt;TYPES...&gt;"/>
</node>
@ -65896,7 +65967,17 @@
<node CREATED="1696538729479" ID="ID_1821271950" MODIFIED="1696538732072" TEXT="handle_begin_thread"/>
<node CREATED="1696538713759" ID="ID_743139495" MODIFIED="1696538713759" TEXT="handle_after_thread"/>
</node>
<node CREATED="1696538607372" ID="ID_479618351" MODIFIED="1696538611095" TEXT="f&#xfc;r Variante-1"/>
<node CREATED="1696538607372" ID="ID_479618351" MODIFIED="1696538611095" TEXT="f&#xfc;r Variante-1">
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1696890808413" ID="ID_569216542" LINK="#ID_1183918146" MODIFIED="1696890916407" TEXT="es zeigt sich: ein smart-ptr l&#xf6;st das Problem gar nicht">
<icon BUILTIN="stop-sign"/>
</node>
<node CREATED="1696890822723" ID="ID_1039222161" MODIFIED="1696890919584" TEXT="damit ist aber auch keine eigene Storage mehr notwendig">
<icon BUILTIN="idea"/>
</node>
<node CREATED="1696890851961" ID="ID_397553721" MODIFIED="1696890865991" TEXT="und auch kein pre-Hook; einfach sich selbst wegschie&#xdf;en und gut is">
<icon BUILTIN="ksmiletris"/>
</node>
</node>
<node CREATED="1696538612261" ID="ID_661367597" MODIFIED="1696538614839" TEXT="f&#xfc;r Variante-2"/>
</node>
<node COLOR="#338800" CREATED="1696690988844" ID="ID_480585061" MODIFIED="1696861459249" TEXT="das Race-Problem addressieren">
@ -65955,6 +66036,18 @@
</node>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1696893013891" ID="ID_1856740667" MODIFIED="1696893025601" TEXT="damit bestehende Probleme im Applikations-Code l&#xf6;sen">
<icon BUILTIN="flag-yellow"/>
<node CREATED="1696893026770" ID="ID_253995958" MODIFIED="1696893031336" TEXT="GtkLumiera">
<node CREATED="1696893056374" MODIFIED="1696893056374" TEXT="Variante-1"/>
</node>
<node CREATED="1696893037535" ID="ID_1340852722" MODIFIED="1696893043218" TEXT="OutputDirector">
<node CREATED="1696893058148" MODIFIED="1696893058148" TEXT="Variante-1"/>
</node>
<node CREATED="1696893032057" ID="ID_926131453" MODIFIED="1696893046381" TEXT="SteamDispatcher">
<node CREATED="1696893059260" ID="ID_109825250" MODIFIED="1696893063103" TEXT="Variante-2"/>
</node>
</node>
<node CREATED="1696538743210" ID="ID_1370366397" MODIFIED="1696538750437" TEXT="Tests erg&#xe4;nzen">
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1696538756712" ID="ID_1688404846" MODIFIED="1696541608160" TEXT="ThreadWrapperAutonomous_test">
<icon BUILTIN="flag-yellow"/>
@ -82097,10 +82190,19 @@ Date:&#160;&#160;&#160;Thu Apr 20 18:53:17 2023 +0200<br/>
<arrowlink COLOR="#bc3562" DESTINATION="ID_1146069423" ENDARROW="Default" ENDINCLINATION="444;909;" ID="Arrow_ID_516910789" STARTARROW="None" STARTINCLINATION="-1280;-32;"/>
<icon BUILTIN="flag-yellow"/>
</node>
<node CREATED="1696468692302" ID="ID_1299573982" MODIFIED="1696468713991" TEXT="SteamDispatcher und OutputDirector brauchen das ebenso"/>
<node CREATED="1696468692302" ID="ID_1299573982" MODIFIED="1696468713991" TEXT="SteamDispatcher und OutputDirector brauchen das ebenso">
<arrowlink COLOR="#e71f4f" DESTINATION="ID_871343805" ENDARROW="Default" ENDINCLINATION="173;10;" ID="Arrow_ID_1685700346" STARTARROW="None" STARTINCLINATION="-14;8;"/>
</node>
</node>
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1696029465122" ID="ID_871343805" MODIFIED="1696541248860" TEXT="output-director.cpp">
<linktarget COLOR="#e71f4f" DESTINATION="ID_871343805" ENDARROW="Default" ENDINCLINATION="173;10;" ID="Arrow_ID_1685700346" SOURCE="ID_1299573982" STARTARROW="None" STARTINCLINATION="-14;8;"/>
<icon BUILTIN="flag-yellow"/>
<node BACKGROUND_COLOR="#e0ceaa" COLOR="#690f14" CREATED="1696888656242" HGAP="33" ID="ID_1500509085" MODIFIED="1696888747350" STYLE="fork" TEXT="Symptom hier: bad_alloc im Shutdown" VSHIFT="17">
<icon BUILTIN="broken-line"/>
<node CREATED="1696888694193" ID="ID_1029299240" MODIFIED="1696888746935" TEXT="dtor des Threads stellt isLife() fest"/>
<node CREATED="1696888714635" ID="ID_1361206868" MODIFIED="1696888746935" TEXT="versucht Warnung ausgzugeben"/>
<node CREATED="1696888722382" ID="ID_1232713816" MODIFIED="1696888746935" TEXT="aber der string threadID ist bereits korrumpiert"/>
</node>
</node>
<node BACKGROUND_COLOR="#eee5a3" COLOR="#883300" CREATED="1696029465122" ID="ID_556208204" MODIFIED="1696539012684" TEXT="steam-dispatcher.cpp">
<linktarget COLOR="#71be6f" DESTINATION="ID_556208204" ENDARROW="Default" ENDINCLINATION="1038;-401;" ID="Arrow_ID_1257115855" SOURCE="ID_1718267866" STARTARROW="None" STARTINCLINATION="602;40;"/>