czy zapis współbieżny na standardowym poziomie wątków jest bezpieczny?
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
Nie jestem pewien, czy kwalifikowałoby się to jako ostateczna odpowiedź, ale spróbuję przedstawić trochę wglądu.
Na F*
działanie funkcji z fmt
pakietu jedynie stwierdzić one przybierać wartości z typu wykonawczego io.Writer
interfejs i rozmowy Write
na jej temat. Same funkcje są bezpieczne do jednoczesnego użycia - w tym sensie, że można wywołać dowolną liczbę fmt.Fwhaveter
jednocześ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 fmt
zapisu. (Należy również pamiętać, że fmt.*Print*
funkcje mogą wywoływać Write
swoje 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.File
lub owijki wokół gniazd wytwarzanych przez funkcjenet
pakietu.
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 Write
rzeczy 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.Stdout
jest 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ą”
Write
iRead
wywołują 1-do-1 do obiektu bazowego - to znaczy,Write
wywołanie nigdy nie jest dzielone na dwa lub więcejRead
wywoł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ć Write
dowolną 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.Stdout
jakąś 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ż fmt
pakiet nie zapewnia żadnych gwarancji dotyczących liczba wywołań Write
dostarczonego „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.Stdout
w typie niestandardowym, który zarządzałby blokadą, i użycie go). A log
skoro 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).