одновременная запись на stdout threadsafe?
приведенный ниже код не вызывает гонку данных
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)
}
Я пробовал несколько длин данных, с вариантом https://play.golang.org/p/29Cnwqj5K30
В этом посте говорится, что это не TS.
Это письмо действительно не отвечает на вопрос, или я не понял.
Пакетная документация для os и fmt мало упоминает об этом. Признаюсь, я не копался в исходном коде этих двух пакетов, чтобы найти дальнейшие объяснения, они кажутся мне слишком сложными.
Какие есть рекомендации и ссылки на них ?
Ответы
Я не уверен, что это можно считать окончательным ответом, но я постараюсь дать некоторое представление.
В F*
-функциях fmt
пакета просто утверждают , что они принимают значение типа , реализующий io.Writer
интерфейс и вызов Write
на него. Сами функции безопасны для одновременного использования - в том смысле, что можно вызывать любое количество fmt.Fwhaveter
одновременно: сам пакет подготовлен для этого, но поддержка интерфейса в Go ничего не говорит о реальном параллелизме типов.
Другими словами, реальная точка, в которой параллелизм может или не может быть разрешен, передается «писателю», которому выполняются функции fmt
записи. (Следует также иметь в виду, что fmt.*Print*
функциям разрешено вызывать Write
место назначения любое количество раз - в отличие от тех, которые предусмотрены стандартным пакетом log
.)
Итак, в основном у нас есть два случая:
- Пользовательские реализации
io.Writer
. - Стандартные реализации этого, такие как
*os.File
или оболочки вокруг сокетов, созданные функциямиnet
package.
Первый случай прост: что бы ни сделал разработчик.
Второй случай сложнее: как я понимаю, позиция стандартной библиотеки Go по этому поводу (хотя и не четко изложена в документации) в том смысле, что она предоставляет оболочки для «вещей», предоставляемых ОС, таких как файловые дескрипторы и сокеты, разумно «тонкий» и, следовательно, любая реализуемая ими семантика транзитивно реализуется кодом stdlib, работающим в конкретной системе.
Например, POSIX требует, чтобы write(2)вызовы были атомарными по отношению друг к другу, когда они работают с обычными файлами или символическими ссылками. Это означает, что, поскольку любой вызов Write
объектов, обертывающих файловые дескрипторы или сокеты, фактически приводит к единственному системному вызову «запись» системы tagret, вы можете обратиться к документации целевой ОС и получить представление о том, что произойдет.
Обратите внимание, что POSIX сообщает только об объектах файловой системы, и если os.Stdout
он открыт для терминала (или псевдотерминала), или для канала, или для чего-либо еще, что поддерживает write(2)
системный вызов, результаты будут зависеть от того, что соответствующая подсистема и / или драйвер реализация - например, данные из нескольких одновременных вызовов могут перемежаться, или один из вызовов, или оба, могут быть просто сбиты ОС - маловероятно, но все же.
Возвращаясь к Go, из того, что я понял, следующие факты верны о типах Go stdlib, которые обертывают файловые дескрипторы и сокеты:
- Они безопасны для одновременного использования сами по себе (я имею в виду на уровне Go).
- Они «сопоставляют»
Write
иRead
вызывают 1 к 1 базовому объекту, то естьWrite
вызов никогда не разбивается на два или более базовых системныхRead
вызова , и вызов никогда не возвращает данные, «склеенные» из результатов нескольких базовых системных вызовов. (Между прочим, людей иногда сбивает с толку такое поведение без излишеств - например, смотрите на то или это как на примеры.)
Итак, в основном, когда мы рассматриваем это с учетом факта, вы fmt.*Print*
можете свободно вызывать Write
любое количество раз за один вызов, ваши примеры, которые используют os.Stdout
, будут:
- Никогда не приводите к гонке данных - если вы не назначили переменной
os.Stdout
некоторую настраиваемую реализацию, - но - Данные, фактически записанные в базовый FD, будут смешиваться в непредсказуемом порядке, который может зависеть от многих факторов, включая версию и настройки ядра ОС, версию Go, использованную для создания программы, оборудование и нагрузку на систему.
TL; DR
- Множественные одновременные вызовы
fmt.Fprint*
записи в одно и то же значение «писателя» переносят их параллелизм на реализацию (тип) «писателя». - Невозможно получить гонку данных с «файловыми» объектами, предоставляемыми Go stdlib в настройке, которую вы представили в своем вопросе.
- Настоящая проблема будет заключаться не в гонке данных на уровне программы Go, а в одновременном доступе к одному ресурсу на уровне ОС. И там мы (обычно) не говорим о гонке данных, потому что стандартные ОС, поддерживаемые Go, раскрывают вещи, в которые можно «писать», как абстракции, где реальная гонка данных может указывать на ошибку в ядре или в драйвере (и Детектор гонки Go все равно не сможет ее обнаружить, так как эта память не будет принадлежать среде выполнения Go, управляющей процессом).
По сути, в вашем случае, если вам нужно быть уверенным, что данные, созданные каким-либо конкретным вызовом, fmt.Fprint*
выходят как единая непрерывная часть для фактического получателя данных, предоставляемого ОС, вам необходимо сериализовать эти вызовы, поскольку fmt
пакет не дает никаких гарантий относительно количество обращений к Write
предоставленному "писателю" для экспортируемых им функций.
Сериализация может быть внешней (явной, то есть «принять блокировку, вызвать fmt.Fprint*
, снять блокировку») или внутренней - путем заключения os.Stdout
в пользовательский тип, который будет управлять блокировкой, и его использования). И пока мы занимаемся этим, log
пакет делает именно это, и его можно сразу использовать в качестве «регистраторов», которые он предоставляет, в том числе и по умолчанию, позволяющих запретить вывод «заголовков журнала» (таких как отметка времени и имя файла).