Cout có được đồng bộ hóa / luồng an toàn không?


112

Nói chung, tôi giả định rằng các luồng không được đồng bộ hóa, người dùng có thể thực hiện khóa phù hợp. Tuy nhiên, những thứ như coutcó được đối xử đặc biệt trong thư viện tiêu chuẩn không?

Đó là, nếu nhiều luồng đang ghi coutchúng có thể làm hỏng coutđối tượng không? Tôi hiểu rằng ngay cả khi được đồng bộ hóa, bạn vẫn nhận được đầu ra xen kẽ ngẫu nhiên, nhưng việc xen kẽ đó có được đảm bảo không. Đó là, nó có an toàn để sử dụng couttừ nhiều chủ đề không?

Nhà cung cấp này có phụ thuộc không? Gcc làm gì?


Quan trọng : Vui lòng cung cấp một số loại tham chiếu cho câu trả lời của bạn nếu bạn nói "có" vì tôi cần một số loại bằng chứng về điều này.

Mối quan tâm của tôi cũng không phải là về các lệnh gọi hệ thống cơ bản, những lệnh đó ổn, mà là các luồng thêm một lớp đệm ở trên cùng.


2
Điều này phụ thuộc vào nhà cung cấp. C ++ (trước C ++ 0x) không có khái niệm về nhiều luồng.
Sven

2
Còn c ++ 0x thì sao? Nó định nghĩa một mô hình bộ nhớ và một luồng là gì, vì vậy có lẽ những thứ này nhỏ giọt trong đầu ra?
rubenvb

2
Có nhà cung cấp nào làm cho nó an toàn không?
edA-qa mort-hay-y

Có ai có liên kết đến tiêu chuẩn đề xuất C ++ 2011 mới nhất không?
edA-qa mort-hay-y

4
Theo một nghĩa nào đó, đây là nơi printftỏa sáng khi đầu ra hoàn chỉnh được ghi stdoutvào một lần chụp; khi sử dụng std::coutmỗi liên kết của chuỗi biểu thức sẽ được xuất ra riêng rẽ stdout; ở giữa chúng có thể có một số ghi luồng khác stdoutdo đó thứ tự của đầu ra cuối cùng bị rối loạn.
Legends2k

Câu trả lời:


106

Tiêu chuẩn C ++ 03 không nói bất cứ điều gì về nó. Khi bạn không có gì đảm bảo về độ an toàn của sợi chỉ, bạn nên coi nó là sợi chỉ không an toàn.

Điều quan tâm đặc biệt ở đây là thực tế coutđược đệm. Ngay cả khi các lệnh gọi tới write(hoặc bất cứ điều gì thực hiện được hiệu ứng đó trong việc triển khai cụ thể đó) được đảm bảo là loại trừ lẫn nhau, bộ đệm có thể được chia sẻ bởi các luồng khác nhau. Điều này sẽ nhanh chóng dẫn đến tình trạng bên trong của luồng bị hỏng.

Và ngay cả khi quyền truy cập vào bộ đệm được đảm bảo là an toàn theo luồng, bạn nghĩ điều gì sẽ xảy ra trong đoạn mã này?

// in one thread
cout << "The operation took " << result << " seconds.";

// in another thread
cout << "Hello world! Hello " << name << "!";

Bạn có thể muốn mỗi dòng ở đây hoạt động loại trừ lẫn nhau. Nhưng làm thế nào một triển khai có thể đảm bảo điều đó?

Trong C ++ 11, chúng tôi có một số đảm bảo. FDIS cho biết những điều sau đây trong §27.4.1 [iostream.objects.overview]:

Truy cập đồng thời vào các chức năng đầu vào và đầu ra (§27.7.2.1) và đầu ra (§27.7.3.1) chuẩn được đồng bộ hóa (§27.5.3.4) của đối tượng iostream được đồng bộ hóa hoặc một luồng C tiêu chuẩn theo nhiều luồng sẽ không dẫn đến chạy đua dữ liệu (§ 1.10). [Lưu ý: Người dùng vẫn phải đồng bộ hóa việc sử dụng đồng thời các đối tượng và luồng này theo nhiều luồng nếu họ muốn tránh các ký tự xen kẽ. - ghi chú cuối]

Vì vậy, bạn sẽ không nhận được các luồng bị hỏng, nhưng bạn vẫn cần phải đồng bộ hóa chúng theo cách thủ công nếu không muốn đầu ra trở thành rác.


2
Về mặt kỹ thuật đúng với C ++ 98 / C ++ 03, nhưng tôi nghĩ mọi người đều biết điều đó. Nhưng điều này không trả lời được hai câu hỏi thú vị: Còn C ++ 0x thì sao? Các triển khai điển hình thực sự làm gì ?
Nemo

1
@ edA-qa mort-ora-y: Không, bạn nhầm rồi. C ++ 11 xác định rõ ràng rằng các đối tượng dòng tiêu chuẩn có thể được đồng bộ hóa và giữ lại hành vi được xác định rõ ràng, không phải là chúng theo mặc định.
ildjarn

12
@ildjarn - Không, @ edA-qa mort-ora-y đúng. Miễn cout.sync_with_stdio()là đúng, việc sử dụng coutđể xuất các ký tự từ nhiều luồng mà không cần đồng bộ hóa bổ sung được xác định rõ ràng, nhưng chỉ ở cấp độ của từng byte riêng lẻ. Vì vậy, cout << "ab";và được cout << "cd"thực thi trong các luồng khác nhau có thể xuất ra acdb, chẳng hạn, nhưng có thể không gây ra Hành vi không xác định.
JohannesD

4
@JohannesD: Chúng tôi đồng ý ở đó - nó được đồng bộ hóa với API C bên dưới. Quan điểm của tôi là nó không được "đồng bộ hóa" theo cách hữu ích, tức là người ta vẫn cần đồng bộ hóa thủ công nếu họ không muốn dữ liệu rác.
ildjarn

2
@ildjarn, tôi ổn với dữ liệu rác, điều đó tôi hiểu. Tôi chỉ quan tâm đến điều kiện chạy đua dữ liệu, điều này dường như đã rõ ràng.
edA-qa mort-hay-y,

16

Đâ là một câu hỏi tuyệt vời.

Đầu tiên, C ++ 98 / C ++ 03 không có khái niệm "luồng". Vì vậy, trong thế giới đó, câu hỏi là vô nghĩa.

Còn C ++ 0x thì sao? Hãy xem câu trả lời của Martinho (mà tôi thừa nhận đã làm tôi ngạc nhiên).

Làm thế nào về các triển khai cụ thể trước C ++ 0x? Ví dụ: đây là mã nguồn basic_streambuf<...>:sputccủa GCC 4.5.2 (tiêu đề "streambuf"):

 int_type
 sputc(char_type __c)
 {
   int_type __ret;
   if (__builtin_expect(this->pptr() < this->epptr(), true)) {
       *this->pptr() = __c;
        this->pbump(1);
        __ret = traits_type::to_int_type(__c);
      }
    else
        __ret = this->overflow(traits_type::to_int_type(__c));
    return __ret;
 }

Rõ ràng, điều này thực hiện không khóa. Và cũng không xsputn. Và đây chắc chắn là loại streambuf mà cout sử dụng.

Theo như tôi có thể nói, libstdc ++ không thực hiện khóa xung quanh bất kỳ hoạt động luồng nào. Và tôi sẽ không mong đợi bất kỳ điều gì, vì điều đó sẽ chậm.

Vì vậy, với việc triển khai này, rõ ràng là có thể đầu ra của hai luồng làm hỏng lẫn nhau ( không chỉ xen kẽ).

Mã này có thể làm hỏng chính cấu trúc dữ liệu không? Câu trả lời phụ thuộc vào các tương tác có thể có của các chức năng này; Ví dụ: điều gì sẽ xảy ra nếu một luồng cố gắng xóa bộ đệm trong khi luồng khác cố gắng gọi xsputnhoặc bất cứ điều gì. Nó có thể phụ thuộc vào cách trình biên dịch và CPU của bạn quyết định sắp xếp lại các lần tải và lưu trữ bộ nhớ; nó sẽ cần một phân tích cẩn thận để chắc chắn. Nó cũng phụ thuộc vào những gì CPU của bạn làm nếu hai luồng cố gắng sửa đổi cùng một vị trí đồng thời.

Nói cách khác, ngay cả khi nó hoạt động tốt trong môi trường hiện tại của bạn, nó có thể bị hỏng khi bạn cập nhật bất kỳ thời gian chạy, trình biên dịch hoặc CPU nào của mình.

Tóm tắt điều hành: "Tôi sẽ không". Xây dựng một lớp ghi nhật ký thực hiện khóa thích hợp hoặc chuyển sang C ++ 0x.

Là một giải pháp thay thế yếu, bạn có thể đặt cout thành không đệm. Có khả năng (mặc dù không được đảm bảo) sẽ bỏ qua tất cả logic liên quan đến bộ đệm và gọi writetrực tiếp. Mặc dù điều đó có thể rất chậm.


1
Câu trả lời tốt, nhưng hãy xem phản hồi của Martinho cho thấy rằng C ++ 11 thực sự xác định đồng bộ hóa cho cout.
edA-qa mort-hay-y

7

Tiêu chuẩn C ++ không chỉ định việc ghi vào luồng có an toàn theo luồng hay không, nhưng thường thì không.

www.techrepublic.com/article/use-stl-streams-for-easy-c-plus-plus-thread-safe-logging

và cả: Các luồng đầu ra tiêu chuẩn trong chuỗi C ++ có an toàn không (cout, cerr, clog)?

CẬP NHẬT

Vui lòng xem câu trả lời của @Martinho Fernandes để biết về tiêu chuẩn C ++ 11 mới nói gì về điều này.


3
Tôi đoán vì C ++ 11 bây giờ là tiêu chuẩn nên câu trả lời này thực sự sai.
edA-qa mort-ora-y

6

Như các câu trả lời khác đã đề cập, điều này chắc chắn là dành riêng cho nhà cung cấp vì tiêu chuẩn C ++ không đề cập đến phân luồng (điều này thay đổi trong C ++ 0x).

GCC không hứa hẹn nhiều về an toàn luồng và I / O. Nhưng tài liệu cho những gì nó hứa là ở đây:

điều quan trọng có lẽ là:

Loại __basic_file chỉ đơn giản là một tập hợp các trình bao bọc nhỏ xung quanh lớp C stdio (một lần nữa, hãy xem liên kết trong Cấu trúc). Chúng tôi không tự khóa mình, mà chỉ chuyển qua các lệnh gọi fopen, fwrite, v.v.

Vì vậy, đối với 3.0, câu hỏi "đa luồng có an toàn cho I / O không" phải được trả lời bằng "thư viện C của nền tảng của bạn có an toàn luồng cho I / O không?" Một số là theo mặc định, một số thì không; nhiều cung cấp nhiều triển khai của thư viện C với các đánh đổi khác nhau về tính an toàn và hiệu quả của luồng. Bạn, lập trình viên, luôn phải quan tâm đến nhiều luồng.

(Ví dụ: tiêu chuẩn POSIX yêu cầu các hoạt động C stdio FILE * là nguyên tử. Các thư viện C tuân thủ POSIX (ví dụ: trên Solaris và GNU / Linux) có một mutex bên trong để tuần tự hóa các hoạt động trên FILE * s. Tuy nhiên, bạn vẫn cần để không làm những điều ngu ngốc như gọi fclose (fs) trong một luồng, sau đó là truy cập fs trong luồng khác.)

Vì vậy, nếu thư viện C của nền tảng của bạn là an toàn luồng, thì các hoạt động I / O dòng fstream của bạn sẽ an toàn luồng ở mức thấp nhất. Đối với các hoạt động cấp cao hơn, chẳng hạn như thao tác dữ liệu chứa trong các lớp định dạng luồng (ví dụ: thiết lập lệnh gọi lại bên trong std :: ofstream), bạn cần phải bảo vệ các truy cập đó giống như bất kỳ tài nguyên chia sẻ quan trọng nào khác.

Tôi không biết liệu có điều gì đã thay đổi sin khung thời gian 3.0 được đề cập hay không.

iostreamsBạn có thể tìm thấy tài liệu an toàn luồng của MSVC tại đây: http://msdn.microsoft.com/en-us/library/c9ceah3b.aspx :

Một đối tượng duy nhất là luồng an toàn để đọc từ nhiều luồng. Ví dụ, cho một đối tượng A, sẽ an toàn khi đọc A từ luồng 1 và từ luồng 2 đồng thời.

Nếu một đối tượng đang được ghi vào một luồng, thì tất cả các lần đọc và ghi vào đối tượng đó trên cùng một luồng hoặc các luồng khác phải được bảo vệ. Ví dụ, cho một đối tượng A, nếu luồng 1 đang ghi vào A, thì luồng 2 phải được ngăn không cho đọc hoặc ghi vào A.

Thật an toàn khi đọc và ghi vào một phiên bản của một kiểu ngay cả khi một luồng khác đang đọc hoặc ghi vào một phiên bản khác của cùng một kiểu. Ví dụ, các đối tượng A và B đã cho cùng kiểu, sẽ an toàn nếu A đang được ghi trong luồng 1 và B đang được đọc trong luồng 2.

...

Các lớp iostream

Các lớp iostream tuân theo các quy tắc tương tự như các lớp khác, với một ngoại lệ. Sẽ an toàn khi ghi vào một đối tượng từ nhiều luồng. Ví dụ: luồng 1 có thể ghi vào cout cùng lúc với luồng 2. Tuy nhiên, điều này có thể dẫn đến kết quả đầu ra từ hai luồng được trộn lẫn với nhau.

Lưu ý: Đọc từ bộ đệm luồng không được coi là thao tác đọc. Nó nên được coi là một hoạt động ghi, vì điều này thay đổi trạng thái của lớp.

Lưu ý rằng thông tin đó dành cho phiên bản mới nhất của MSVC (hiện tại cho VS 2010 / MSVC 10 / cl.exe16.x). Bạn có thể chọn thông tin cho các phiên bản MSVC cũ hơn bằng cách sử dụng điều khiển thả xuống trên trang (và thông tin khác với các phiên bản cũ hơn).


1
"Tôi không biết liệu có điều gì đã thay đổi khung thời gian 3.0 được đề cập hay không." Nó chắc chắn đã làm. Trong vài năm qua, việc triển khai các luồng g ++ đã thực hiện bộ đệm của riêng nó.
Nemo
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.