là viết đồng thời trên stdout threadsafe?

Dec 09 2020

mã bên dưới không ném ra một cuộc đua dữ liệu

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)
}

Tôi đã thử nhiều độ dài dữ liệu, với biến thể https://play.golang.org/p/29Cnwqj5K30

Bài đăng này nói rằng nó không phải là TS.

Thư này không thực sự trả lời câu hỏi, hoặc tôi không hiểu.

Tài liệu gói của os và fmt không đề cập nhiều về điều này. Tôi thừa nhận rằng tôi đã không đào mã nguồn của hai gói đó để tìm giải thích thêm, chúng có vẻ quá phức tạp đối với tôi.

Các khuyến nghị và tài liệu tham khảo của họ là gì?

Trả lời

6 kostix Dec 09 2020 at 17:10

Tôi không chắc nó sẽ đủ điều kiện là một câu trả lời dứt khoát nhưng tôi sẽ cố gắng cung cấp một số thông tin chi tiết.

Các F*-functions của fmtgói chỉ nêu họ mất giá trị của một loại thực hiện io.Writergiao diện và gọi Writevào nó. Bản thân các chức năng này an toàn cho việc sử dụng đồng thời - theo nghĩa là có thể gọi bất kỳ số fmt.Fwhaveterđồng thời nào: bản thân gói đã được chuẩn bị cho điều đó, nhưng việc hỗ trợ giao diện trong Go không nói lên điều gì về kiểu thực sự đồng thời.

Nói cách khác, điểm thực tại nơi mà sự đồng thời có thể được phép hoặc có thể không được chuyển giao cho "người viết" mà các chức năng fmtghi. (Cũng nên nhớ rằng các fmt.*Print*hàm được phép gọi Writeđến đích của nó bất kỳ số lần nào - trái ngược với các hàm được cung cấp bởi gói cổ phiếu log.)

Vì vậy, về cơ bản chúng ta có hai trường hợp:

  • Triển khai tùy chỉnh của io.Writer.
  • Các triển khai cổ phiếu của nó, chẳng hạn như *os.Filehoặc bao bọc xung quanh các ổ cắm được tạo ra bởi các chức năng của netgói.

Trường hợp đầu tiên là trường hợp đơn giản: bất cứ điều gì người thực hiện đã làm.

Trường hợp thứ hai khó hơn: theo tôi hiểu, lập trường của thư viện tiêu chuẩn Go về điều này (mặc dù không được nêu rõ ràng trong tài liệu) trong đó các trình bao bọc mà nó cung cấp xung quanh "những thứ" được cung cấp bởi Hệ điều hành — chẳng hạn như trình mô tả tệp và ổ cắm — là hợp lý "thin", và do đó bất kỳ ngữ nghĩa nào mà chúng triển khai, đều được thực hiện chuyển tiếp bởi mã stdlib chạy trên một hệ thống cụ thể.

Ví dụ, POSIX yêu cầu rằng write(2)các cuộc gọi là nguyên tử đối với nhau khi chúng đang hoạt động trên các tệp thông thường hoặc các liên kết tượng trưng. Điều này có nghĩa là, vì bất kỳ lệnh gọi nào Writebao gồm các bộ mô tả tệp hoặc ổ cắm thực sự dẫn đến một cuộc gọi tổng hợp "ghi" duy nhất của hệ thống tagret, bạn có thể tham khảo tài liệu của hệ điều hành mục tiêu và biết được điều gì sẽ xảy ra.

Lưu ý rằng POSIX chỉ cho biết về các đối tượng hệ thống tệp và nếu os.Stdoutđược mở đến một thiết bị đầu cuối (hoặc một thiết bị đầu cuối giả) hoặc đến một đường ống hoặc bất kỳ thứ gì khác hỗ trợ cuộc gọi tổng hợp write(2), kết quả sẽ phụ thuộc vào hệ thống con có liên quan và / hoặc trình điều khiển thực hiện — ví dụ: dữ liệu từ nhiều cuộc gọi đồng thời có thể được xen kẽ, hoặc một trong các lệnh gọi, hoặc cả hai, có thể chỉ bị lỗi bởi Hệ điều hành — không chắc, nhưng vẫn có.

Quay trở lại Go, từ những gì tôi thu thập được, các sự kiện sau đây đúng về các loại Go stdlib bao bọc các bộ mô tả tệp và ổ cắm:

  • Chúng an toàn để sử dụng đồng thời (ý tôi là ở cấp độ cờ vây).
  • Chúng "ánh xạ" WriteReadgọi 1-to-1 đến đối tượng cơ bản — nghĩa là, một Writecuộc gọi không bao giờ được chia thành hai hoặc nhiều Readcuộc gọi hệ thống cơ bản và một cuộc gọi không bao giờ trả về dữ liệu "được dán" từ kết quả của nhiều cuộc gọi tổng hợp bên dưới. (Nhân tiện, đôi khi mọi người bị vấp bởi hành vi không kiểu cách này - ví dụ: xem điều này hoặc điều này làm ví dụ.)

Vì vậy, về cơ bản khi chúng tôi xem xét điều này với thực tế fmt.*Print*là bạn có thể tự do gọi Writebất kỳ số lần nào trên một cuộc gọi, ví dụ của bạn sử dụng os.Stdoutsẽ:

  • Không bao giờ dẫn đến một cuộc chạy đua dữ liệu - trừ khi bạn đã chỉ định biến os.Stdoutmột số triển khai tùy chỉnh, - nhưng
  • Dữ liệu thực sự được ghi vào FD bên dưới sẽ được trộn lẫn với nhau theo một thứ tự không thể đoán trước được, có thể phụ thuộc vào nhiều yếu tố bao gồm cài đặt và phiên bản hạt nhân hệ điều hành, phiên bản Go được sử dụng để xây dựng chương trình, phần cứng và tải trên hệ thống.

TL; DR

  • Nhiều lệnh gọi đồng thời để fmt.Fprint*ghi vào cùng một giá trị "người viết" làm trì hoãn sự đồng thời của chúng đối với việc triển khai (loại) của "người viết".
  • Không thể có cuộc chạy đua dữ liệu với các đối tượng "giống tệp" do Go stdlib cung cấp trong thiết lập mà bạn đã trình bày trong câu hỏi của mình.
  • Vấn đề thực sự sẽ không phải là với các cuộc đua dữ liệu ở cấp chương trình Go mà là với việc truy cập đồng thời vào một tài nguyên duy nhất xảy ra ở cấp hệ điều hành. Và ở đó, chúng tôi không (thường) nói về các cuộc đua dữ liệu bởi vì hàng hóa OSes Go hỗ trợ hiển thị những thứ mà người ta có thể "ghi vào" dưới dạng các nội dung trừu tượng, trong đó một cuộc chạy đua dữ liệu thực có thể chỉ ra lỗi trong hạt nhân hoặc trong trình điều khiển (và Trình dò ​​cuộc đua của cờ vây sẽ không thể phát hiện ra nó vì bộ nhớ đó sẽ không thuộc sở hữu của thời gian chạy cờ vây cung cấp năng lượng cho quá trình).

Về cơ bản, trong trường hợp của bạn, nếu bạn cần đảm bảo dữ liệu được tạo ra bởi bất kỳ cuộc gọi cụ thể nào fmt.Fprint*xuất hiện dưới dạng một phần tiếp giáp duy nhất với bộ thu dữ liệu thực tế do HĐH cung cấp, bạn cần phải tuần tự hóa các cuộc gọi này vì fmtgói không cung cấp đảm bảo nào về số lượng lệnh gọi đến Write"nhà văn" được cung cấp cho các chức năng mà nó xuất.
Việc tuần tự hóa có thể là bên ngoài (rõ ràng, đó là "lấy khóa, gọi fmt.Fprint*, mở khóa") hoặc bên trong - bằng cách gói os.Stdoutloại tùy chỉnh sẽ quản lý khóa và sử dụng nó). Và trong khi chúng ta đang ở đó, loggói thực hiện điều đó và có thể được sử dụng ngay lập tức làm "trình ghi nhật ký" mà nó cung cấp, bao gồm cả trình ghi mặc định, cho phép ngăn xuất ra "tiêu đề nhật ký" (chẳng hạn như dấu thời gian và tên của tệp).