czy zapis współbieżny na standardowym poziomie wątków jest bezpieczny?

Dec 09 2020

poniższy kod nie rzuca wyścigu danych

package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {
    x := strings.Repeat(" ", 1024)
    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"aa\n")
        }
    }()

    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"bb\n")
        }
    }()

    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"cc\n")
        }
    }()

    go func() {
        for {
            fmt.Fprintf(os.Stdout, x+"dd\n")
        }
    }()

    <-make(chan bool)
}

Próbowałem różnych długości danych, z wariantem https://play.golang.org/p/29Cnwqj5K30

Ten post mówi, że to nie jest TS.

Ten e-mail tak naprawdę nie odpowiada na pytanie lub nie rozumiem.

Dokumentacja pakietów os i fmt nie wspomina o tym wiele. Przyznaję, że nie wykopałem kodu źródłowego tych dwóch pakietów, aby znaleźć dalsze wyjaśnienia, wydają mi się zbyt skomplikowane.

Jakie są zalecenia i ich odniesienia ?

Odpowiedzi

6 kostix Dec 09 2020 at 17:10

Nie jestem pewien, czy kwalifikowałoby się to jako ostateczna odpowiedź, ale spróbuję przedstawić trochę wglądu.

Na F*działanie funkcji z fmtpakietu jedynie stwierdzić one przybierać wartości z typu wykonawczego io.Writerinterfejs i rozmowy Writena jej temat. Same funkcje są bezpieczne do jednoczesnego użycia - w tym sensie, że można wywołać dowolną liczbę fmt.Fwhaveterjednocześnie: sam pakiet jest do tego przygotowany, ale obsługa interfejsu w Go nie mówi nic o współbieżności typu rzeczywistego.

Innymi słowy, rzeczywisty punkt, w którym współbieżność może lub nie może być dozwolona, ​​jest odroczony do „pisarza”, do którego funkcje fmtzapisu. (Należy również pamiętać, że fmt.*Print*funkcje mogą wywoływać Writeswoje miejsce docelowe dowolną liczbę razy - w przeciwieństwie do tych, które zapewnia pakiet magazynowy log).

Mamy więc zasadniczo dwa przypadki:

  • Niestandardowe implementacje io.Writer.
  • Standardowe implementacje tego, takie jak *os.Filelub owijki wokół gniazd wytwarzanych przez funkcje netpakietu.

Pierwszy przypadek jest prosty: cokolwiek zrobił wdrażający.

Drugi przypadek jest trudniejszy: jak rozumiem, stanowisko biblioteki standardowej Go w tej sprawie (aczkolwiek nie zostało jasno określone w dokumentacji) w tym, że opakowania, które zapewnia wokół „rzeczy” dostarczanych przez system operacyjny - takich jak deskryptory plików i gniazda - są rozsądne „cienki”, a więc niezależnie od semantyki, którą implementują, jest implementowany przechodni przez kod standardowej biblioteki, działający w określonym systemie.

Na przykład POSIX wymaga, aby write(2)wywołania były atomowe względem siebie, gdy działają na zwykłych plikach lub dowiązaniach symbolicznych. Oznacza to, że ponieważ każde wywołanie Writerzeczy zawijających deskryptory plików lub gniazda w rzeczywistości powoduje pojedyncze wywołanie systemowe „zapis” systemu tagret, możesz zapoznać się z dokumentacją docelowego systemu operacyjnego i zorientować się, co się stanie.

Zauważ, że POSIX mówi tylko o obiektach systemu plików i jeśli os.Stdoutjest otwarty na terminal (lub pseudoterminal), potok lub cokolwiek innego, co obsługuje write(2)wywołanie systemowe, wyniki będą zależały od tego, jaki podsystem i / lub sterownik implement - na przykład dane z wielu jednoczesnych wywołań mogą być przeplatane lub jedno z wywołań lub oba mogą po prostu zawieść system operacyjny - mało prawdopodobne, ale nadal.

Wracając do Go, z tego, co zebrałem, następujące fakty są prawdziwe o typach Go stdlib, które zawijają deskryptory plików i gniazda:

  • Są bezpieczne do jednoczesnego użytku samodzielnie (mam na myśli na poziomie Go).
  • „Mapują” Writei Readwywołują 1-do-1 do obiektu bazowego - to znaczy, Writewywołanie nigdy nie jest dzielone na dwa lub więcej Readwywołań systemowych, a wywołanie nigdy nie zwraca danych „sklejonych” z wyników wielu wywołań systemowych. (Nawiasem mówiąc, ludzie czasami są potykani przez to proste zachowanie - na przykład zobacz to lub to jako przykłady).

Zasadniczo, gdy rozważymy to z faktem, fmt.*Print*możesz dzwonić Writedowolną liczbę razy podczas jednego połączenia, twoje przykłady, które używają os.Stdout, będą:

  • Nigdy nie skutkuj wyścigiem danych - chyba że przypisałeś zmiennej os.Stdoutjakąś niestandardową implementację, - ale
  • Dane faktycznie zapisane w bazowym FD będą mieszane w nieprzewidywalnej kolejności, która może zależeć od wielu czynników, w tym wersji i ustawień jądra systemu operacyjnego, wersji Go użytej do zbudowania programu, sprzętu i obciążenia systemu.

TL; DR

  • Wiele jednoczesnych wywołań fmt.Fprint*zapisu do tej samej wartości „modułu zapisującego” odracza ich współbieżność z implementacją (typem) elementu „zapisującego”.
  • Niemożliwe jest stworzenie wyścigu danych z obiektami „podobnymi do pliku” dostarczonymi przez bibliotekę Go standardową w konfiguracji, którą przedstawiłeś w swoim pytaniu.
  • Prawdziwym problemem nie będą wyścigi danych na poziomie programu Go, ale równoczesny dostęp do pojedynczego zasobu na poziomie systemu operacyjnego. I tam nie mówimy (zwykle) o wyścigach danych, ponieważ towarowe systemy operacyjne Go obsługują eksponowanie rzeczy, do których można „pisać”, jako abstrakcji, gdzie prawdziwy wyścig danych prawdopodobnie wskazywałby na błąd w jądrze lub sterowniku (i Wykrywacz wyścigów Go i tak nie będzie w stanie go wykryć, ponieważ ta pamięć nie byłaby własnością środowiska wykonawczego Go zasilającego proces).

Zasadniczo, w twoim przypadku, jeśli chcesz mieć pewność, że dane wytworzone przez określone wywołanie programu fmt.Fprint*wychodzą jako pojedynczy ciągły element do rzeczywistego odbiornika danych dostarczonego przez system operacyjny, musisz serializować te wywołania, ponieważ fmtpakiet nie zapewnia żadnych gwarancji dotyczących liczba wywołań Writedostarczonego „modułu zapisującego” dla funkcji, które eksportuje.
Serializacja może być zewnętrzna (jawna, czyli „weź blokadę, wywołaj fmt.Fprint*, zwolnij blokadę”) lub wewnętrzna - poprzez umieszczenie os.Stdoutw typie niestandardowym, który zarządzałby blokadą, i użycie go). A logskoro już o tym mowa , pakiet właśnie to robi i może być używany od razu jako "loggery", które dostarcza, włączając w to domyślny, pozwalający na blokowanie wysyłania "nagłówków dziennika" (takich jak znacznik czasu i nazwa pliku).