Zpět na domů

Fan-Out/Fan-In v Go: vzorec pro paralelní zpracování dat

Článek podrobně rozebírá vzorec paralelního zpracování Fan-Out/Fan-In v jazyce Go. Jsou probrány principy fungování, rozdíly od Worker Pool, praktická implementace s příklady kódu a klíčová doporučení pro použití v production prostředí.

Flexibilní paralelnost v Go: rozebereme Fan-Out/Fan-In
Advertisement 728x90

Vzor Fan-Out/Fan-In v Go: flexibilní paralelní zpracování dat

Vzor Fan-Out/Fan-In poskytuje v Go výkonný mechanismus pro rozdělování úkolů mezi gorutiny a následnou agregaci výsledků. Na rozdíl od Worker Poolu nabízí větší flexibilitu díky individuálním zpracovatelským kanálům pro každého pracovníka, což je zvláště užitečné pro složité řetězce transformace dat.

Základní principy vzoru

Vzor se skládá ze dvou klíčových fází. Fáze Fan-Out je zodpovědná za rozdělování vstupních dat mezi několik pracovníků (gorutin). Každý pracovník obdrží svůj úkol a zpracuje jej nezávisle, odesílající výsledek do vyhrazeného kanálu. Fáze Fan-In shromažďuje výsledky ze všech individuálních kanálů pracovníků do jednoho společného výstupního kanálu pro další zpracování konzumentem.

Hlavní rozdíl oproti Worker Poolu spočívá v architektuře kanálů:

Google AdInline article slot
  • Worker Pool: všichni pracovníci obvykle čtou úkoly z jednoho kanálu a zapisují výsledky do jiného společného kanálu.
  • Fan-Out/Fan-In: každý pracovník má svůj vlastní výstupní kanál, což vyžaduje dodatečnou fázi sloučení (merge).

Tento přístup vytváří určité režijní náklady kvůli nutnosti dodatečných gorutin a kanálů pro sloučení, ale poskytuje výhody pro pipeline-architektury, kde následující fáze zpracování mohou vyžadovat samostatné datové toky.

Implementace vzoru v praxi

Podívejme se na praktickou implementaci vzoru, která se záměrně liší od klasického Worker Poolu pro demonstraci klíčových vlastností.

Funkce pracovníka (Worker)

Pracovník v této implementaci je funkce, která spouští gorutinu a vrací kanál pro výsledky. To se liší od přístupu, kde pracovník přijímá kanál jako parametr.

Google AdInline article slot
func worker(x int, workerID int) chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        fmt.Printf("worker %d, job %d\n", workerID, x)
        out <- x * x
    }()
    return out
}

Důležité aspekty implementace:

  • Gorutina uzavírá svůj kanál out pomocí defer po odeslání výsledku.
  • Pro demonstrační účely je práce triviální – umocnění čísla na druhou.
  • V produkčním kódu vyžaduje taková implementace vylepšení: je nutné řízení životního cyklu prostřednictvím kontextu pro graceful shutdown a zpracování časových limitů.

Fáze Fan-Out (Rozdělení)

Distributor (producer) spouští jednoho pracovníka pro každý prvek vstupních dat. Kanály vrácené pracovníky jsou uloženy v seznamu pro následné sloučení.

for i, x := range data {
    chnls = append(chnls, worker(x, i+1))
}

Klíčová vlastnost: počet pracovníků je dynamicky určen počtem úkolů, což není povinné, ale demonstruje flexibilitu vzoru. V reálných scénářích se počet paralelních pracovníků obvykle omezuje.

Google AdInline article slot

Fáze Fan-In a funkce sloučení (Merge)

Funkce merge provádí kriticky důležitý úkol – slučuje data z více kanálů do jednoho. K tomu:

  • Vytvoří společný nebuferovaný výstupní kanál out.
  • Pro každý vstupní kanál spustí gorutinu, která čte data a přesměrovává je do out.
  • V samostatné gorutině čeká na dokončení všech gorutin-čtenářů pomocí sync.WaitGroup a poté uzavře kanál out.
func merge(chnls ...chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup

    for _, ch := range chnls {
        wg.Add(1)
        go func(c chan int) {
            defer wg.Done()
            for x := range c {
                out <- x
            }
        }(ch)
    }

    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

Technický detail: čekání na dokončení (wg.Wait()) je provedeno v samostatné gorutině. Pokud by bylo provedeno v hlavním vlákně, mohlo by dojít k deadlocku, protože konzument nemusí stihnout začít číst z kanálu out a operace zápisu by se zablokovala.

Konzument (Consumer)

Konzument čte výsledky z jednotného kanálu získaného po sloučení. Cyklus for range se automaticky ukončí, když funkce merge uzavře kanál out.

for x := range out {
    results = append(results, x)
}

Co je důležité

  • Flexibilita architektury: Individuální kanály pro pracovníky umožňují snadné vkládání dodatečných fází zpracování dat.
  • Dynamické škálování: Teoreticky je podporováno vytváření pracovníka pro každý úkol, ačkoli v praxi je vyžadována kontrola nad počtem paralelních gorutin.
  • Režijní náklady: Vzor vyžaduje dodatečné zdroje (kanály, gorutiny) pro fázi sloučení ve srovnání s jednoduššími přístupy.
  • Řízení životního cyklu: Demonstrační kód nepoužívá kontexty, což je nepřijatelné pro produkční prostředí, kde je nutný kontrol nad zrušením operací a časovými limity.
  • Čitelnost kódu: Použití nebuferovaných kanálů a cyklu for range dělá kód kompaktním a srozumitelným.

Doporučení pro použití v produkci

Demonstrační kód slouží výukovým účelům. Pro průmyslovou exploataci jsou nutná podstatná vylepšení:

  • Zavedení kontextu (context.Context): Povinně předejte kontext pracovníkům a funkci merge pro zpracování zrušení operací, termínů a časových limitů.
  • Omezení paralelismu: Spuštění neomezeného počtu gorutin (jedna na úkol) může vést k vyčerpání zdrojů. Použijte semafory nebo fond pracovníků s pevnou velikostí uvnitř fáze Fan-Out.
  • Zpracování chyb: Realizujte mechanismus předávání chyb od pracovníků konzumentovi, například prostřednictvím kanálu speciálního typu nebo struktury výsledku.
  • Buferování kanálů: V některých scénářích mohou buferované kanály zlepšit výkonnost, snížit blokace, ale jejich použití vyžaduje pečlivou analýzu.

Vzor Fan-Out/Fan-In je výkonným nástrojem v arzenálu Go-vývojáře pro vytváření efektivních a škálovatelných konvejerů paralelního zpracování dat, zejména když je vyžadována flexibilita v organizaci toků informací mezi fázemi výpočtů.

— Editorial Team

Advertisement 728x90

Číst dál