1 Punkte von GN⁺ 2024-08-29 | 1 Kommentare | Auf WhatsApp teilen

Einführung

  • Wir schreiben Dolt, die weltweit erste SQL-Datenbank mit Versionsverwaltung, in der Programmiersprache Go
  • Wie die meisten Go-Codebasen verwenden wir Channels und Goroutines, um nebenläufige Ausführung zu implementieren
  • Da nebenläufige Programmierung im Allgemeinen schwierig ist, verwenden wir normalerweise einfache und intuitive Methoden
  • Allerdings haben wir aus einem anderen Open-Source-Projekt Code geerbt, der Channels auf sehr originelle Weise einsetzt
var c chan chan struct{}
  • Dabei werden Channels zwischen verschiedenen Goroutines weitergegeben, um ein Fan-out-Muster zwischen Worker-Goroutines zu implementieren
  • Dieser Ansatz war schwer zu verstehen und unter Berücksichtigung von Goroutine-Leaks schwer zu handhaben
  • Am Ende haben wir diesen Code neu geschrieben und chan chan struct{} entfernt

Warum tut man so etwas?

  • Aus der Zeit, als C und seine Ableger dominierend waren, gibt es einen alten Programmiererwitz
  • Viele Menschen hatten Schwierigkeiten, Pointer zu verstehen
  • Da Go ebenfalls von C abgeleitet ist, kann man dort dasselbe tun
func main() {
  i := 1
  setInt(&i)
  fmt.Printf("i is now %d", i)
}

func setInt(i *int) {
  setInt2(&i)
}

func setInt2(i **int) {
  setInt3(&i)
}

func setInt3(i ***int) {
  setInt4(&i)
}

func setInt4(i ****int) {
  ****i = 100
}
  • Dieser Code kompiliert und gibt i is now 100 aus
  • Dasselbe lässt sich in Go auch mit Channels machen

Der 4-chan-Go-Programmierer

  • Wir schreiben ein Programm, das vier Ebenen von Channel-Indirektion verwendet
  • Der oberste Channel wird als 4-chan deklariert
_4chan := make(chan chan chan chan int)
  • Der Wert, der in diesen Channel gesendet wird, ist ein 3-chan
_3chan := make(chan chan chan int)
  • Auf jeder Indirektionsebene erzeugen wir Produzenten entsprechend einem festen Verzweigungsfaktor
func sendChanChanChan(c chan chan chan chan int) {
  for range factor {
    go func() {
      logrus.Debug("starting 3chan producer")
      _3chan := make(chan chan chan int)
      sendChanChan(c, _3chan)
    }()
  }
}
  • Mit den Konsumenten verfahren wir genauso
func receiveChanChanChan(c chan chan chan chan int) {
  for _3chan := range c {
    logrus.Debug("got message from 4chan")
    for range factor {
      logrus.Debug("starting 3chan consumer")
      go receiveChanChan(_3chan)
    }
  }
}
  • Schließlich erreichen wir die Ebene, auf der die tatsächlichen Werte gesendet werden
func send(_2chan chan chan int, _1chan chan int) {
  _2chan <- _1chan
  for range factor {
    go func() {
      logrus.Debug("starting int producer")
      for range factor {
        go func() {
          logrus.Debug("sending int")
          _1chan <- 1
        }()
      }
    }()
  }
}
  • Der Konsument summiert die empfangenen Werte auf
var sum = &atomic.Int32{}

func receive(c chan int) {
  for s := range c {
    logrus.Debug("received int")
    sum.Add(int32(s))
  }
}
  • Setzt man alles zusammen und führt es aus
const factor = 3
var sum = &atomic.Int32{}

func main() {
  // logrus.SetLevel(logrus.DebugLevel)
  _4chan := make(chan chan chan chan int)
  go sendChanChanChan(_4chan)
  go receiveChanChanChan(_4chan)
  time.Sleep(500 * time.Millisecond)
  fmt.Printf("%d ^ 5: %d", factor, sum.Load())
}
  • Dieses Programm berechnet die fünfte Potenz einer Zahl auf möglichst verteilte Weise

Kommentar

  • Es gibt viele Gründe, das in echtem Code nicht so zu machen: die Schwierigkeit bei Implementierung und Debugging, verletzter Stolz, der Tadel von Kollegen usw.
  • Trotzdem ist es interessant, weil es sehr unterhaltsam ist und tatsächlich funktioniert
  • Ein praktischer Grund ist, dass es sehr schwierig wird, einen Channel zu schließen, wenn man Channels durch Channels schickt

Fazit

  • Wenn ihr Fragen oder Meinungen zu interessanten Nebenläufigkeitsmustern in Go habt, könnt ihr auf Discord mit unserem Team und anderen Dolt-Nutzern sprechen

Zusammenfassung von GN⁺

  • Dieser Artikel behandelt ein originelles Nebenläufigkeitsmuster mit Channels in der Programmiersprache Go
  • Für den Einsatz in echtem Code ist es ineffizient, aber konzeptionell interessant
  • Er zeigt, wie sich die Nebenläufigkeitsfunktionen von Go in Projekten wie Dolt nutzen lassen
  • Projekte mit ähnlicher Funktionalität sind unter anderem PostgreSQL und MySQL

1 Kommentare

 
GN⁺ 2024-08-29
Hacker-News-Kommentare
  • Als Wissenschaftler verstehe ich vieles nicht, was professionelle Softwareingenieure tun, wenn ich mit ihnen zusammenarbeite

    • Ich habe schon gesehen, wie eine einzige Codezeile über vier „Interface-Funktionen“ aufgerufen wurde
    • Jede Funktion lag in einer anderen Datei und einem anderen Ordner, was das Lesen des Codes sehr ermüdend machte
    • Nach ein paar Ebenen fragt man sich, ob man jemals bei dem Teil ankommt, der tatsächlich etwas berechnet
  • Ich möchte einen wenig substanziellen Kommentar mit geringem Aufwand hinterlassen

    • Das Meme in den ersten paar Absätzen war als C-Programmierer lustig
    • Ich mag es, seltsame Variationen von Sprachen zu sehen, und es ist interessant, das in Go zu sehen
  • Ein alter Programmierwitz aus der Zeit, als C und seine abgeleiteten Sprachen dominierten, ist immer noch gültig

  • Es erinnert mich an klassische Musik von Buena Vista Social Club

  • Ich habe das Muster „chan chan Value“ oder „chan struct{resp chan Value}“ in bestimmten Situationen verwendet

    • Man hätte stattdessen einen Message Bus verwenden können, aber dann muss man sich eben um den Message Bus kümmern
  • Channels von Channels sind ein gängiges Muster und erscheinen meist als Feld eines Struct-Typs, das ein Channel ist

    • Man sendet eine Anfrage, und nachdem der Worker die Arbeit erledigt hat, legt er das Ergebnis in den Antwort-Channel
    • type request struct { params, reply chan response } in dieser Art
    • Zwei Channels sind nützlich, drei oder mehr habe ich noch nicht gesehen
  • Ein Blog mit der Gegenposition, in dem Channels verwendet werden, um einen Dynamic-Dispatch-Mechanismus zu implementieren

    • Wird in der Sprache Limbo verwendet und ist dasselbe Konzept wie in Go
    • Blog-Link
  • Es erinnert an Joe Armstrongs „My favorite Erlang Program“

  • Als ich auf den Link geklickt habe, hatte ich etwas anderes erwartet

    • Ich bin kein Go-Programmierer, deshalb habe ich den Witz nicht sofort verstanden
  • In LabVIEW-Code wird etwas Ähnliches verwendet, um asynchrone Antwortdaten zu empfangen

    • Statt Antworten in eine Queue zu kippen, wird eine Nachricht übergeben, die einen Callback-Event-Channel enthält
    • Das verschwendet zwar Speicher, ist aber effizient, weil er nach der einzelnen Verwendung bei der Antwort geschlossen wird