Freigeben über


Dieser Artikel wurde maschinell übersetzt.

Windows mit C++

Leichte kooperatives Multitasking mit C++

Kenny Kerr

 

Kenny KerrWenn Sie für ein Unternehmen, die hat eine dieser Codierung Normen Dokumente, die einen gesamten Regenwald vernichten würde arbeiten, waren es überhaupt gedruckt werden, würden Sie besser aufhören zu lesen jetzt. Die Chancen stehen gut, was ich bin, zu zeigen, Sie werden gegen viele der die heiligen Kühe im vorgenannten Epos. Ich werde Ihnen sagen, über eine besondere Technik, die ich ursprünglich entwickelt, um mich vollständig asynchronen Code effizient und ohne die Notwendigkeit einer komplexen Zustandsautomaten schreiben zu ermöglichen.

Es sei denn, Ihr Name Donald Knuth ist, ist es wahrscheinlich jeder Code, den Sie schreiben etwas ähneln wird, der bereits getan wurde. In der Tat, das älteste Konto finde ich die Technik, die ich hier beschreibe ist eine Erwähnung von Knuth selbst, und das jüngste ist von einem Herrn aus England durch den Namen von Simon Tatham, bekannt für die beliebte PuTTY-terminal-Emulator. Dennoch, den Richter in der jüngsten Oracle V zu paraphrasieren. Google-Streit, "Sie können nicht patent eine for-Schleife." Dennoch sind wir alle verpflichtet, unsere Kollegen im Bereich der Computer Programmierung, wie wir das Handwerk vorantreiben.

Bevor ich eintauchen und beschreiben, was meine Technik ist und wie es funktioniert, brauche ich eine schnelle Ablenkung in der Hoffnung zu präsentieren, die es Ihnen wird ein bisschen mehr Perspektive für was kommen wird. In "Compiler: Prinzipien, Techniken und Tools, zweite Ausgabe"(Lehrling Hall, 2006) von Alfred V. Aho, Monica S. Lam, Ravi Sethi und Jeffrey D. Ullman – besser bekannt als der Dragon Book — die Autoren fassen den Zweck eines Compilers als ein Programm, eine Programm in einer Sprache zu lesen und in einem entsprechenden Programm in einer anderen Sprache übersetzen kann. Wenn Sie C++-Designer Bjarne Stroustrup Fragen welche Sprache verwendet er C++ zu schreiben, er würde Ihnen sagen, dass es C++ war. Was dies bedeutet ist, dass schrieb er einen Präprozessor, der Lesen von C++ und produziert C, die ein Norm-C-Compiler dann weiter in einigen Maschinensprache übersetzen könnte. Wenn man nahe genug, können Sie Varianten dieser Idee an vielen Orten sehen. Der C#-Compiler, übersetzt z. B. das scheinbar magische Yield-Schlüsselwort in einer regulären Klasse, die einen Iterator implementiert. Ende letzten Jahres bemerkte ich, dass der Visual C++-Compiler zunächst Teile des c übersetzt + + / CX-Syntax in C++-Standardbibliothek vor dem Kompilieren es weiter. Dies kann haben sich seither verändert, aber der Punkt ist, dass Compiler grundsätzlich über die Übersetzung sind.

Manchmal können bei Verwendung eine bestimmte Sprache man schließen, dass ein Feature wünschenswert wäre, aber das Wesen der Sprache verhindert, dass Sie deren Umsetzung. Dies geschieht selten in C++, da die Sprache von Design für den Ausdruck verschiedener Techniken, dank seiner reichen Syntax und meta-programming Ausstattung geeignet ist. Allerdings geschah dies in der Tat für mich.

Ich verbringe die meiste Zeit in diesen Tagen arbeiten auf ein eingebettetes OS, das ich von Grund auf neu erstellt. Weder Linux, Windows oder einem anderen Betriebssystem läuft unter der Decke. Ich verlasse mich auf keine open-Source-Software auch immer und in der Tat, die in der Regel wäre unmöglich sowieso aus Gründen, die deutlich werden. Dies hat mir die Augen für eine ganze Welt von C- und C++-Programmierung eröffnet, die ganz anders als die traditionelle Welt der PC-Software-Entwicklung ist. Die meisten embedded-Systeme haben sehr unterschiedliche Einschränkungen, von denen der "normalen" Programme. Sie müssen äußerst zuverlässig sein. Fehler kann teuer werden. Benutzer sind selten um "einem ausgefallenen Programm neu starten". Systeme möglicherweise laufen seit Jahren ohne Unterbrechung und ohne menschlichen Eingriff. Stell dir eine Welt ohne Windows Update oder ähnliches. Diese Systeme möglicherweise relativ knappe Rechnerressourcen. Richtigkeit, Zuverlässigkeit und vor allem Parallelität alle in die Gestaltung solcher Systeme zentrale Rollen spielen.

Als solche ist die Plüsch Welt von Visual C++, mit seinen leistungsstarken Bibliotheken, selten angebracht. Auch wenn Visual C++ wurden meine eingebettete Mikrocontroller als Ziel, die zugehörigen Bibliotheken sind nicht gut geeignet für Systeme mit solchen knappen Ressourcen und, oft, hart in Echtzeit Einschränkungen. Einer der Mikroprozessoren, die ich derzeit benutze hat nur 32 KB RAM mit weniger als 50 MHz, und das ist noch luxuriöser zu einigen in der embedded-Community. Es sollte klar sein, dass von "embedded" ich nicht Ihre durchschnittliche Smartphone mit einem halb-Gig RAM und einen 1 GHz-Prozessor meine.

In "Programmierung: Prinzipien und Praxis mithilfe von C++ "(Addison-Wesley Professional, 2008), Stroustrup nennt gratis-Speicher Zuweisungen (z. B., neue und löschen), Ausnahmen und Dynamic_cast in eine kurze Liste der Features, die in den meisten embedded-Systemen vermieden werden muss, weil sie nicht vorhersehbar sind. Leider, das schließt die Verwendung der meisten der Standard, Anbieters und öffnen Sie Quelle C++ Bibliotheken verfügbar heute.

Das Ergebnis ist, dass die meisten eingebettete Programmierung — und für diese Angelegenheit, Kernelmodus-Programmierung unter Windows — noch beschäftigt c anstatt in C++. Da c in erster Linie eine Teilmenge der C++ ist, neige ich dazu, einen C++-Compiler, aber halten Sie sich eine genaue Teilmenge der Sprache zu verwenden, das vorhersehbare, portable und gut in eingebetteten Systemen funktioniert.

Dies führte mich auf eine Reise, eine geeignete Technik zur Parallelität in meinem wenig eingebettetes OS aktivieren zu finden. Bis zu diesem Zeitpunkt hatte mein OS einen einzelnen Thread, wenn man es so nennen können. Es gibt keine blockierenden Operationen, so jeder Zeit musste ich etwas umzusetzen, die einige Zeit, z. B. warten auf eine Speicher e/A annehmen könnte unterbrechen oder für ein Netzwerk-Zeitüberschreitungswert ich sorgfältig konstruierten Automaten zu schreiben muss würde. Dies ist gängige Praxis mit ereignisgesteuerte Systeme, aber es führt Code, die schwer zu Grund durch logisch, da der Code nicht sequenzielle ist.

Stellen Sie sich einen Speichertreiber. Möglicherweise gibt es eine Storage_read-Funktion eine Seite des Speichers von persistent flash-Speicher des Geräts zu lesen. Die Storage_read-Funktion möglicherweise zuerst überprüfen, ob das Peripheriegerät oder Bus ist beschäftigt, und wenn ja, einfach die Lese Anforderung in die Warteschlange vor der Rückgabe. Einige zeigen des Peripheriegeräts und Bus frei werden und die Anforderung können fortfahren. Dabei wird z. B. das Deaktivieren des Transceivers, Formatierung einen Befehl für den Bus, Vorbereitung der DMA Zugriff Puffer, den Transceiver wieder aktivieren und anschließend damit kann den Anrufer etwas anderes zu tun, während die Übertragung im Hardware abgeschlossen wird zurückgegeben. Schließlich signalisiert der Bus seiner Fertigstellung und der Anrufer wird über einige Rückrufmechanismus benachrichtigt, und alle anderen Anforderungen in der Warteschlange fortfahren. Unnötig zu sagen, kann es ziemlich kompliziert Verwalten von Warteschlangen, Rückrufe und Zustandsautomaten erhalten. Überprüft wird, ob alles korrekt ist, ist noch schwieriger. Ich habe nicht einmal beschrieben wie ein nicht blockierender Dateisystem auf diese Abstraktion implementiert werden könnte oder wie ein Webserver das Dateisystem verwenden kann, um Daten dienen — alles ohne jemals blockieren. Ein anderer Ansatz ist erforderlich, die unvermeidliche und wachsende Komplexität reduzieren.

Nun, Stell dir vor, dass C++ hatte ein paar Stichworte, mit denen Sie den Prozessor aus einer Aufrufliste in eine andere Mid-function zu transportieren. Stellen Sie sich des folgenden hypothetischen C++-Codes:

void storage_read(storage_args & args) async
{
  wait while (busy);
  busy = true;
  begin_transfer(args);
  yield until (done_transmitting());
  busy = false;
}

Beachten Sie das kontextbezogene Schlüsselwort "Async" hinter der Parameterliste. Ich benutze auch zwei imaginäre Abständen Schlüsselwörtern namens "warten Sie, während" und "Rendite bis." Betrachten Sie, was es für C++, solche Stichwörter zu haben bedeutet. Der Compiler hätte irgendwie die Vorstellung von einem Zwischenspiel, zum Ausdruck bringen, wenn man so will. Knuth nannte dies eine Koroutine. Das Async-Schlüsselwort scheinen mit einer Funktionsdeklaration, den Compiler weiß, dass die Funktion asynchron ausführen kann und daher entsprechend aufgerufen werden muss zu lassen. Die hypothetische warten und Ausbeute Schlüsselwörter sind die tatsächlichen Punkte, an denen die Funktion nicht mehr synchron ausgeführt und kann potenziell an den Aufrufer, wo es aufgehört hat zu einem späteren Zeitpunkt wieder zurückgeben. Man könnte auch ein "warten" bedingte Schlüsselwort sowie eine bedingungslose Yield-Anweisung vorstellen.

Ich habe gesehen, dass Alternativen zu dieser Kooperative Form der Parallelität — insbesondere der Asynchronous Agents Library in Visual C++ enthaltenen — aber alle Lösungen ich fand einige Runtime Planungsmodul abhing. Was ich hier vorschlagen und wird zeigen, in einem Moment ist es durchaus möglich, daß ein C++-Compiler kooperative Parallelität in der Tat ohne jede Laufzeit Kosten jeglicher Art darstellen könnte. Denken Sie daran, dass ich nicht sage, dass dies die vielkernigen Skalierbarkeit Herausforderung lösen wird. Was ich sagen will ist, dass wir in der Lage, schnell und reagieren Ereignisgesteuerte Programme zu schreiben, ohne über einen Scheduler sein sollten. Und wie bei vorhandenen Sprachen C und C++, nichts sollte verhindern diese Techniken zusammen mit OS-Threads oder anderen Laufzeiten Parallelität.

C++ unterstützt nicht offensichtlich, was ich jetzt beschreibe. Was ich entdeckte, jedoch ist, dass es recht gut mit Standard C oder C++ simuliert werden kann und ohne auf Assembler-Trickserei. Mit diesem Ansatz, könnte die weiter oben beschriebene Storage_read-Funktion wie folgt aussehen:

void storage_read(storage_args & args) async
{
  wait while (busy);
  busy = true;
  begin_transfer(args);
  yield until (done_transmitting());
  busy = false;
}

Natürlich bin ich unter Berufung auf Makros hier. Schnappen! Klar, das Element 16 in C++ Coding Standards verletzt (bit.ly/8boiZ0), aber die Alternativen sind noch schlimmer. Die ideale Lösung ist für die Sprache direkt unterstützen. Alternativen sind mit Longjmp, aber ich finde, das ist schlimmer und hat seine eigenen Tücken. Ein anderer Ansatz könnte Assembly-Sprache verwenden, aber dann würde ich alle Portabilität verlieren. Es ist fraglich, ob es noch so effizient in Assemblersprache, getan werden könnte, weil, die höchstwahrscheinlich in einer Lösung führen würde, die mehr Speicher durch den Verlust von Kontextinformationen und die unvermeidliche eins-Stack-pro-Task-Implementierung verwendet. Also mir humor wie ich zeigen Ihnen wie einfach und effektiv ist dies, und dann wie alles funktioniert.

Um die Dinge klar, werde ich fortan diese asynchrone Funktionen Aufgaben nennen. Angesichts die Aufgabe, die ich weiter oben beschrieben, kann es geplant werden einfach durch es als Funktion aufrufen:

storage_args = { ... };
storage_read(args);

Sobald die Aufgabe, daß es kann nicht fortgesetzt werden entscheidet, wird einfach an den Aufrufer zurückgegeben. Aufgaben beschäftigen einen Rückgabetyp Bool an Anrufern, ob sie abgeschlossen haben. So könnten Sie kontinuierlich einen Task planen, bis es abgeschlossen, wie folgt ist:

while (storage_read(args));

Natürlich würde dies den Anrufer blockieren, bis der Vorgang abgeschlossen ist. Dies könnte tatsächlich vielleicht geeignet wenn Ihr Programm erstmals gestartet wird, um eine Konfigurationsdatei oder ähnliches zu laden. Abgesehen von dieser Ausnahme wollen Sie nur selten in dieser Weise zu blockieren. Was Sie brauchen, ist ein Weg für eine Aufgabe kooperativ zu warten:

task_wait_for(storage_read, args);

Dabei wird davon ausgegangen, der Anrufer ist selbst eine Aufgabe und wird dann an den Aufrufer Ausbeute, bis die geschachtelte Aufgabe abgeschlossen ist, an welcher Stelle es weitergeht. Nun lassen Sie mich lose definieren die Aufgabe Keywords oder Pseudofunktionen, und dann gehen Sie durch ein Beispiel oder zwei, die Sie tatsächlich für sich selbst ausprobieren können:

  • Task_declare (Name, Parameter) wird eine Aufgabe, in der Regel in einer Headerdatei deklariert.
  • Task_begin (Name, Parameter) beginnt die Definition einer Aufgabe, in der Regel in einer C++-Quelldatei.
  • Task_endEnds die Definition einer Aufgabe.
  • Task_return () beendet die Ausführung eines Vorgangs und Kontrolle an den Aufrufer zurückgibt.
  • Task_wait_until (Ausdruck) wartet, bis der Ausdruck wahr, bevor Sie fortfahren ist.
  • Task_wait_while (Ausdruck) wartet, während der Ausdruck wahr, bevor Sie fortfahren ist.
  • Task_wait_for (Name, Parameter) führt die Aufgabe aus und wartet, bis es abgeschlossen ist, bevor Sie fortfahren.
  • (Task_yield) Steuern Erträge bedingungslos, fortfahren, wenn die Aufgabe neu geplant ist.
  • Task_yield_until (Ausdruck) Steuern Erträge mindestens einmal fortfahren, wenn der Ausdruck NULL ist.

Es ist wichtig, dass keines dieser Routinen in irgendeiner Weise zu blockieren. Sie sind alle entworfen, um einen hohen Grad an Parallelität kooperativ zu erreichen. Lassen Sie mich mit einem einfachen Beispiel veranschaulichen. In diesem Beispiel verwendet zwei Aufgaben, man fordert den Benutzer für eine Zahl und die andere, um eine einfache arithmetische Mittel der Zahlen zu berechnen, wie sie ankommen. Zunächst ist die durchschnittliche Aufgabe, gezeigt Abbildung 1.

Abbildung 1 die durchschnittliche Aufgabe

struct average_args
{
  int * source;
  int sum;
  int count;
  int average;
  int task_;
};
task_begin(average, average_args & args)
{
  args.sum = 0;
  args.count = 0;
  args.average = 0;
  while (true)
  {
    args.sum += *args.source;
    ++args.count;
    args.average = args.sum / args.count;
    task_yield();
  }
}
task_end

Eine Aufgabe akzeptiert genau ein Argument, das als Verweis übergeben werden muss und eine Ganzzahlvariable Mitglied namens Task_ enthalten. Offensichtlich ist dies etwas, was der Compiler des Aufrufers gegeben das ideale Szenario der erstklassigen Sprachunterstützung verbergen würde. Jedoch für die Zwecke dieser Simulation brauche ich eine Variable zum Nachverfolgen des Vorgangs Fortschritt. Alles, was der Anrufer tun muss, ist auf 0 (null) initialisiert.

Die Aufgabe selbst ist interessant, dass es eine unendliche while-Schleife mit einem Task_yield-Aufruf innerhalb der Schleifenrumpf enthält. Die Aufgabe initialisiert einige Zustand vor dem Eintritt in diese Schleife. Es aktualisiert dann die Aggregate und Erträge, so dass andere Aufgaben ausführen vor dem Wiederholen auf unbestimmte Zeit.

Als nächstes ist die Eingabe Aufgabe, wie in Abbildung 2.

Abbildung 2 die Input-Aufgabe

struct input_args
{
  int * target;
  int task_;
};
task_begin(input, input_args & args)
{
  while (true)
  {
    printf("number: ");
    if (!scanf_s("%d", args.target))
    {
      task_return();
    }
    task_yield();
  }
}
task_end

Diese Aufgabe ist interessant, dass es illustriert, dass Aufgaben können in der Tat blockieren, als die scanf Funktion tun während er auf Eingaben wartet. Zwar nicht ideal für ein ereignisgesteuertes System. Diese Aufgabe veranschaulicht auch mithilfe des Task_return-Aufrufs zum Abschließen der Aufgabe in Mid-function anstelle von einen bedingten Ausdruck in der While Anweisung. Eine Aufgabe abgeschlossen ist, durch Aufrufen Task_return oder durch Herunterfallen Ende der Funktion, so zu sprechen. So oder so, der Anrufer sehen dies als die Aufgabe abschließen, und es werden wieder aufnehmen.

Um diese Aufgaben zu leben, alles was Sie brauchen ist eine einfache main-Funktion als einen Planer zu handeln:

int main()
{
  int share = -1;
  average_args aa = { &share };
  input_args ia = { &share };
  while (input(ia))
  {
    average(aa);
    printf("sum=%d count=%d average=%d\n",
      aa.sum, aa.count, aa.average);
  }
}

Die Möglichkeiten sind endlos. Sie könnten Aufgaben darstellt, Timer, Erzeuger und Verbraucher, TCP-Verbindung-Handler und vieles mehr schreiben.

Wie funktioniert es? Zunächst Bedenken wieder, die ideale Lösung für den Compiler an, diese umzusetzen, in welchem Fall es alle Arten von clevere Tricks verwenden kann, um es effizient umsetzen, und was ich zu beschreiben eigentlich werden nicht annähernd so anspruchsvoll oder kompliziert.

Als beste wie ich sagen kann, läuft dies auf eine Entdeckung von einem Programmierer namens Tom Duff, wer fand heraus, dass Sie, clevere Tricks mit der Switch-Anweisung spielen können. Solange es syntaktisch gültig ist, können Sie verschiedene Auswahl Schachteln oder Iterationsanweisungen innerhalb einer Switch-Anweisung effektiv innerhalb und außerhalb einer Funktion zu springen werden. Duff veröffentlicht ein Verfahren zum manuellen Schleife Ausrollen und Tatham erkannte dann, dass sie verwendet werden könnte, um Coroutinen zu simulieren. Ich nahm diese Ideen und Aufgaben wie folgt implementiert.

Die Task_begin und Task_end Makros definieren die umgebenden Switch-Anweisung:

#define task_begin(name, parameter) \
                                    \
  bool name(parameter)              \
  {                                 \
    bool yield_ = false;            \
    switch (args.task_)             \
    {                               \
      case 0:
#define task_end                    \
                                    \
    }                               \
    args.task_ = 0;                 \
    return false;                   \
  }

Es sollte nun klar sein, was für die einzelnen Task_-Variable ist und wie alles funktioniert. Task_ auf NULL initialisiert wird sichergestellt, dass an den Anfang der Aufgabe springt. Wenn eine Aufgabe beendet wird, hat es wieder zurück auf als Bequemlichkeit 0 gesetzt so dass die Aufgabe einfach neu gestartet werden kann. Da bietet das Task_wait_until-Makro die notwendigen Sprung Lage und die kooperative zurück:

#define task_wait_until(expression)      \
                                         \
  args.task_ = __LINE__; case __LINE__:  \
  if (!(expression))                     \
  {                                      \
    return true;                         \
  }

Das Task_ gesetzt ist auf die vordefinierten Zeile Anzahl Makro, und die Case-Anweisung ruft die gleichen Zeilennummer, wodurch sichergestellt wird, dass bei der Aufgabe ergibt, das nächste Mal es, den Code geplant wurde wird fortgesetzt, Recht, wo es aufgehört. Die restlichen Makros werden angezeigt, Abbildung 3.

Abbildung 3 die restlichen Makros

#define task_return()                    \
                                         \
  args.task_ = 0;                        \
  return false;
#define task_wait_while(expression)      \
                                         \
  task_wait_until(!(expression))
#define task_wait_for(name, parameter)   \
                                         \
  task_wait_while(name(parameter))
#define task_yield_until(expression)     \
                                         \
  yield_ = true;                         \
  args.task_ = __LINE__; case __LINE__:  \
  if (yield_ || !(expression))           \
  {                                      \
    return true;                         \
  }
#define task_yield()                     \
                                         \
  task_yield_until(true)

Diese sollten alle offensichtlich sein. Vielleicht die einzige Subtilität erwähnenswert ist Task_yield_until, da es ähnliche Task_wait_until, sondern die Tatsache, das es immer mindestens einmal liefert. Task_yield, wiederum, wird immer nur Ausbeute genau einmal, und ich bin zuversichtlich, dass jeder respektable Compiler entfernt mein Kurzschrift optimiert wird. Ich sollte erwähnen, dass dieses Task_wait_until ist auch eine gute Möglichkeit, mit Speichermangel Bedingungen umzugehen. Anstatt versagt bei einem tief verschachtelten Vorgang mit zweifelhaften Wiederherstellbarkeit, können Sie einfach Ausbeute bis die Speicherreservierung erfolgreich war, die andere Aufgaben Gelegenheit zu vervollständigen und hoffentlich einige dringend benötigte Arbeitsspeicher freizugeben. Auch ist dies kritisch für eingebettete Systeme wo Scheitern ist keine Option.

Angesichts der Tatsache, dass ich Coroutinen emulieren bin, gibt es einige Fallstricke. Zuverlässig können keine lokale Variablen innerhalb von Aufgaben wird, und jeder Code, der die Gültigkeit der versteckten Switch-Anweisung verletzt, Ärger zu machen. Noch, angesichts der Tatsache, dass ich meine eigene Task_args definieren können — und wenn man bedenkt, wie viel einfacher mein Code Dank dieser Technik ist — ich bin dankbar, dass es genauso funktioniert wie es tut.

Ich fand es nützlich, die folgenden Visual C++-Compiler-Warnungen deaktivieren:

#pragma warning(disable: 4127) // Conditional expression is constant
#pragma warning(disable: 4189) // Local variable is initialized but not referenced
#pragma warning(disable: 4706) // Assignment within conditional expression

Schließlich, wenn Sie die Visual C++-IDE verwenden, achten Sie darauf, mit/Zi statt/Zi "Bearbeiten und fortfahren, Debuggen" deaktivieren.

Da ich diese Spalte geschlossen, ich schaute rund um das Internet für ähnlichen Initiativen und die neue Async gefunden und warten auf Schlüsselwörter, die der Visual Studio 2012 c#-Compiler eingeführt hat. In vielerlei Hinsicht ist dies ein Versuch, ein ähnliches Problem zu lösen. Ich erwarte, dass der C++-Community zu folgen. Die Frage ist, ob diese Lösungen für c und C++ in einer Art und Weise kommt, die vorhersagbaren Code geeignet für eingebettete Systeme produzieren wird, wie ich in diesem Artikel beschrieben habe oder ob sie sich auf eine Parallelität Runtime verlassen werde, als die aktuelle Visual C++ Bibliotheken zu tun. Meine Hoffnung ist, dass eines Tages, die diese Makros wegwerfen zu können, aber bis dieser Tag kommt, werde ich mit dieser Technik leichte Genossenschaft und Multitasking produktiv bleiben.

Stay tuned für die nächste Rate von Windows mit C++ in denen werde ich Ihnen einige neuen Techniken zeigen, die Niklas Gustafsson und Artur Laksberg aus dem Visual C++-Team gearbeitet habe C++ fortsetzbare Funktionen bringen.

Kenny Kerr ist Softwarespezialist mit dem Schwerpunkt auf der systemeigenen Windows-Entwicklung. Sie erreichen ihn unter kennykerr.ca.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Artur Laksberg