одновременная запись на stdout threadsafe?

Dec 09 2020

приведенный ниже код не вызывает гонку данных

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 мало упоминает об этом. Признаюсь, я не копался в исходном коде этих двух пакетов, чтобы найти дальнейшие объяснения, они кажутся мне слишком сложными.

Какие есть рекомендации и ссылки на них ?

Ответы

6 kostix Dec 09 2020 at 17:10

Я не уверен, что это можно считать окончательным ответом, но я постараюсь дать некоторое представление.

В F*-функциях fmtпакета просто утверждают , что они принимают значение типа , реализующий io.Writerинтерфейс и вызов Writeна него. Сами функции безопасны для одновременного использования - в том смысле, что можно вызывать любое количество fmt.Fwhaveterодновременно: сам пакет подготовлен для этого, но поддержка интерфейса в Go ничего не говорит о реальном параллелизме типов.

Другими словами, реальная точка, в которой параллелизм может или не может быть разрешен, передается «писателю», которому выполняются функции fmtзаписи. (Следует также иметь в виду, что fmt.*Print*функциям разрешено вызывать Writeместо назначения любое количество раз - в отличие от тех, которые предусмотрены стандартным пакетом log.)

Итак, в основном у нас есть два случая:

  • Пользовательские реализации io.Writer.
  • Стандартные реализации этого, такие как *os.Fileили оболочки вокруг сокетов, созданные функциями netpackage.

Первый случай прост: что бы ни сделал разработчик.

Второй случай сложнее: как я понимаю, позиция стандартной библиотеки 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пакет делает именно это, и его можно сразу использовать в качестве «регистраторов», которые он предоставляет, в том числе и по умолчанию, позволяющих запретить вывод «заголовков журнала» (таких как отметка времени и имя файла).