C ++ 11 đã giới thiệu một mô hình bộ nhớ tiêu chuẩn. Nó có nghĩa là gì? Và nó sẽ ảnh hưởng đến lập trình C ++ như thế nào?


1894

C ++ 11 đã giới thiệu một mô hình bộ nhớ được tiêu chuẩn hóa, nhưng điều đó chính xác có nghĩa là gì? Và nó sẽ ảnh hưởng đến lập trình C ++ như thế nào?

Bài viết này (của Gavin Clarke , người trích dẫn Herb Sutter ) nói rằng,

Mô hình bộ nhớ có nghĩa là mã C ++ hiện có một thư viện được tiêu chuẩn hóa để gọi bất kể ai đã tạo trình biên dịch và trên nền tảng nào nó đang chạy. Có một cách tiêu chuẩn để kiểm soát cách các luồng khác nhau nói chuyện với bộ nhớ của bộ xử lý.

"Khi bạn đang nói về việc chia [mã] qua các lõi khác nhau trong tiêu chuẩn, chúng tôi đang nói về mô hình bộ nhớ. Chúng tôi sẽ tối ưu hóa nó mà không phá vỡ các giả định sau đây mà mọi người sẽ đưa ra trong mã", Sutter nói.

Chà, tôi có thể ghi nhớ đoạn này và các đoạn tương tự có sẵn trực tuyến (vì tôi đã có mô hình bộ nhớ của riêng mình từ khi sinh ra: P) và thậm chí có thể đăng bài dưới dạng câu trả lời cho câu hỏi của người khác, nhưng thành thật mà nói, tôi không hiểu chính xác điều này.

Các lập trình viên C ++ đã từng phát triển các ứng dụng đa luồng ngay cả trước đây, vậy vấn đề là thế nào nếu đó là các luồng POSIX, hoặc các luồng Windows hoặc các luồng C ++ 11? Những lợi ích là gì? Tôi muốn hiểu các chi tiết cấp thấp.

Tôi cũng có cảm giác rằng mô hình bộ nhớ C ++ 11 bằng cách nào đó có liên quan đến hỗ trợ đa luồng C ++ 11, vì tôi thường thấy hai cái này cùng nhau. Nếu có, chính xác như thế nào? Tại sao chúng phải liên quan?

Vì tôi không biết các phần bên trong của đa luồng hoạt động như thế nào và mô hình bộ nhớ nói chung có ý nghĩa gì, xin vui lòng giúp tôi hiểu các khái niệm này. :-)


3
@cquilguy: Xây dựng ...
Nawaz

4
@cquilguy: Viết một blog sau đó ... và đề xuất cách khắc phục. Không có cách nào khác để làm cho quan điểm của bạn hợp lệ và hợp lý.
Nawaz

2
Tôi nhầm trang web đó là một nơi để hỏi Q và trao đổi ý kiến. Lỗi của tôi; đó là nơi phù hợp, nơi bạn không thể không đồng ý với Herb Sutter ngay cả khi anh ta mâu thuẫn một cách trắng trợn về thông số ném.
tò mò

5
@cquilguy: C ++ là những gì Standard nói, không phải những gì một anh chàng ngẫu nhiên trên internet nói. Vì vậy, có, phải có sự phù hợp với Tiêu chuẩn. C ++ KHÔNG phải là một triết lý mở, nơi bạn có thể nói về bất cứ điều gì không phù hợp với Tiêu chuẩn.
Nawaz

3
"Tôi đã chứng minh rằng không có chương trình C ++ nào có thể có hành vi được xác định rõ." . Yêu cầu cao, không có bằng chứng!
Nawaz

Câu trả lời:


2205

Đầu tiên, bạn phải học cách suy nghĩ như một Luật sư Ngôn ngữ.

Đặc tả C ++ không tham chiếu đến bất kỳ trình biên dịch, hệ điều hành hoặc CPU cụ thể nào. Nó làm cho tham chiếu đến một máy trừu tượng là một khái quát của các hệ thống thực tế. Trong thế giới Law Lawyer, công việc của lập trình viên là viết mã cho cỗ máy trừu tượng; công việc của trình biên dịch là hiện thực hóa mã đó trên một máy cụ thể. Bằng cách mã hóa cứng nhắc đến thông số kỹ thuật, bạn có thể chắc chắn rằng mã của bạn sẽ biên dịch và chạy mà không cần sửa đổi trên bất kỳ hệ thống nào có trình biên dịch C ++ tuân thủ, cho dù là ngày hôm nay hay 50 năm nữa.

Máy trừu tượng trong đặc tả C ++ 98 / C ++ 03 về cơ bản là đơn luồng. Vì vậy, không thể viết mã C ++ đa luồng "hoàn toàn di động" đối với thông số kỹ thuật. Thông số kỹ thuật thậm chí không nói bất cứ điều gì về tính nguyên tử của tải và lưu trữ bộ nhớ hoặc thứ tự tải và lưu trữ có thể xảy ra, không bao giờ bận tâm đến những thứ như mutexes.

Tất nhiên, bạn có thể viết mã đa luồng trong thực tế cho các hệ thống cụ thể - như pthreads hoặc Windows. Nhưng không có cách chuẩn để viết mã đa luồng cho C ++ 98 / C ++ 03.

Máy trừu tượng trong C ++ 11 được thiết kế đa luồng. Nó cũng có một mô hình bộ nhớ được xác định rõ ; đó là, nó nói những gì trình biên dịch có thể và có thể không làm khi truy cập bộ nhớ.

Hãy xem xét ví dụ sau, trong đó một cặp biến toàn cục được truy cập đồng thời bởi hai luồng:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

Chủ đề 2 có thể là gì?

Theo C ++ 98 / C ++ 03, đây thậm chí không phải là Hành vi không xác định; bản thân câu hỏi là vô nghĩa vì tiêu chuẩn không dự tính bất cứ điều gì gọi là "chủ đề".

Theo C ++ 11, kết quả là Hành vi không xác định, vì tải và lưu trữ không cần phải là nguyên tử nói chung. Điều mà có vẻ như không phải là một sự cải thiện ... Và bản thân nó thì không.

Nhưng với C ++ 11, bạn có thể viết điều này:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

Bây giờ mọi thứ trở nên thú vị hơn nhiều. Trước hết, hành vi ở đây được xác định . Bây giờ Thread 2 có thể in 0 0(nếu nó chạy trước Thread 1), 37 17(nếu nó chạy sau Thread 1) hoặc 0 17(nếu nó chạy sau Thread 1 gán cho x nhưng trước khi nó gán cho y).

Những gì nó không thể in là 37 0, bởi vì chế độ mặc định cho tải / lưu trữ nguyên tử trong C ++ 11 là để thực thi tính nhất quán tuần tự . Điều này chỉ có nghĩa là tất cả các tải và cửa hàng phải "như thể" chúng xảy ra theo thứ tự bạn đã viết chúng trong mỗi luồng, trong khi các thao tác giữa các luồng có thể được xen kẽ theo cách hệ thống thích. Vì vậy, hành vi mặc định của nguyên tử cung cấp cả nguyên tửtrật tự cho tải và lưu trữ.

Bây giờ, trên một CPU hiện đại, đảm bảo tính nhất quán tuần tự có thể tốn kém. Cụ thể, trình biên dịch có khả năng phát ra các rào cản bộ nhớ đầy đủ giữa mỗi lần truy cập ở đây. Nhưng nếu thuật toán của bạn có thể chịu được tải và lưu trữ ngoài đơn đặt hàng; tức là, nếu nó yêu cầu nguyên tử nhưng không đặt hàng; tức là, nếu nó có thể chịu đựng 37 0như đầu ra từ chương trình này, thì bạn có thể viết điều này:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

CPU càng hiện đại, điều này càng có khả năng nhanh hơn ví dụ trước.

Cuối cùng, nếu bạn chỉ cần giữ các tải và cửa hàng cụ thể theo thứ tự, bạn có thể viết:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

Điều này đưa chúng ta trở lại các tải và lưu trữ đã đặt hàng - vì vậy 37 0không còn là đầu ra có thể - nhưng nó làm như vậy với chi phí tối thiểu. (Trong ví dụ tầm thường này, kết quả giống như tính nhất quán tuần tự toàn diện; trong một chương trình lớn hơn, nó sẽ không như vậy.)

Tất nhiên, nếu các đầu ra duy nhất bạn muốn xem là 0 0hoặc 37 17, bạn chỉ có thể bọc một mutex xung quanh mã gốc. Nhưng nếu bạn đã đọc đến đây, tôi cá là bạn đã biết nó hoạt động như thế nào và câu trả lời này đã dài hơn tôi dự định :-).

Vì vậy, dòng dưới cùng. Mutexes rất tuyệt và C ++ 11 tiêu chuẩn hóa chúng. Nhưng đôi khi vì lý do hiệu suất mà bạn muốn các nguyên thủy cấp thấp hơn (ví dụ: mẫu khóa được kiểm tra hai lần cổ điển ). Tiêu chuẩn mới cung cấp các tiện ích cấp cao như mutexes và các biến điều kiện, và nó cũng cung cấp các tiện ích cấp thấp như các loại nguyên tử và các hương vị khác nhau của hàng rào bộ nhớ. Vì vậy, bây giờ bạn có thể viết các thói quen đồng thời hiệu suất cao, tinh vi hoàn toàn trong ngôn ngữ được chỉ định bởi tiêu chuẩn và bạn có thể chắc chắn rằng mã của bạn sẽ biên dịch và chạy không thay đổi trên cả hai hệ thống ngày nay và ngày mai.

Mặc dù thẳng thắn, trừ khi bạn là một chuyên gia và làm việc trên một số mã cấp thấp nghiêm trọng, có lẽ bạn nên bám vào các biến thể và biến điều kiện. Đó là những gì tôi dự định làm.

Để biết thêm về công cụ này, xem bài đăng blog này .


37
Câu trả lời hay, nhưng điều này thực sự đang cầu xin một số ví dụ thực tế về các nguyên thủy mới. Ngoài ra, tôi nghĩ rằng thứ tự bộ nhớ không có nguyên thủy cũng giống như trước C ++ 0x: không có gì đảm bảo.
John Ripley

5
@ John: Tôi biết, nhưng bản thân tôi vẫn đang học các nguyên thủy :-). Ngoài ra tôi nghĩ rằng họ đảm bảo truy cập byte là nguyên tử (mặc dù không được đặt hàng), đó là lý do tại sao tôi đã sử dụng "char" cho ví dụ của mình ... Nhưng tôi thậm chí không chắc chắn 100% về điều đó ... Nếu bạn muốn đề xuất bất kỳ điều gì tốt " hướng dẫn "tài liệu tham khảo Tôi sẽ thêm chúng vào câu trả lời của mình
Nemo

48
@Nawaz: Vâng! Truy cập bộ nhớ có thể được sắp xếp lại bởi trình biên dịch hoặc CPU. Hãy suy nghĩ về (ví dụ) bộ nhớ cache và tải đầu cơ. Thứ tự mà bộ nhớ hệ thống bị tấn công có thể không giống như những gì bạn đã mã hóa. Trình biên dịch và CPU sẽ đảm bảo sắp xếp lại như vậy không phá vỡ mã đơn luồng . Đối với mã đa luồng, "mô hình bộ nhớ" mô tả thứ tự sắp xếp lại có thể xảy ra và điều gì xảy ra nếu hai luồng đọc / ghi cùng một vị trí cùng một lúc và cách bạn kiểm soát cả hai. Đối với mã đơn luồng, mô hình bộ nhớ là không liên quan.
Nemo

26
@Nawaz, @Nemo - Một chi tiết nhỏ: mô hình bộ nhớ mới có liên quan trong mã đơn luồng trong khi nó chỉ định độ không xác định của các biểu thức nhất định, chẳng hạn như i = i++. Khái niệm cũ về điểm trình tự đã bị loại bỏ; tiêu chuẩn mới chỉ định điều tương tự bằng cách sử dụng mối quan hệ tuần tự trước đó chỉ là trường hợp đặc biệt của khái niệm liên chủ đề xảy ra trước khi xảy ra .
JulianD

17
@ AJG85: Mục 3.6.2 của dự thảo C ++ 0x spec nói: "Các biến có thời lượng lưu trữ tĩnh (3.7.1) hoặc thời gian lưu trữ luồng (3.7.2) sẽ không được khởi tạo (8.5) trước khi bắt đầu bất kỳ việc khởi tạo nào khác địa điểm." Vì x, y là toàn cục trong ví dụ này, chúng có thời lượng lưu trữ tĩnh và do đó sẽ không khởi tạo, tôi tin.
Nemo

345

Tôi sẽ chỉ đưa ra sự tương tự mà tôi hiểu các mô hình nhất quán bộ nhớ (hay gọi tắt là các mô hình bộ nhớ). Nó được lấy cảm hứng từ bài báo bán thời gian của Leslie Lamport "Thời gian, Đồng hồ và thứ tự các sự kiện trong một hệ thống phân tán" . Sự tương tự là apt và có ý nghĩa cơ bản, nhưng có thể là quá mức cần thiết cho nhiều người. Tuy nhiên, tôi hy vọng nó cung cấp một hình ảnh tinh thần (một hình ảnh đại diện) tạo điều kiện cho lý luận về các mô hình nhất quán bộ nhớ.

Chúng ta hãy xem lịch sử của tất cả các vị trí bộ nhớ trong sơ đồ không gian thời gian trong đó trục ngang biểu thị không gian địa chỉ (nghĩa là mỗi vị trí bộ nhớ được biểu thị bằng một điểm trên trục đó) và trục dọc biểu thị thời gian (chúng ta sẽ thấy rằng, nói chung, không có một khái niệm phổ quát về thời gian). Do đó, lịch sử của các giá trị được giữ bởi mỗi vị trí bộ nhớ được biểu thị bằng một cột dọc tại địa chỉ bộ nhớ đó. Mỗi thay đổi giá trị là do một trong các luồng viết một giá trị mới cho vị trí đó. Bằng một hình ảnh bộ nhớ , chúng tôi sẽ có nghĩa là tổng hợp / kết hợp các giá trị của tất cả các vị trí bộ nhớ có thể quan sát được tại một thời điểm cụ thể bởi một luồng cụ thể .

Trích dẫn từ "Một mồi về tính nhất quán của bộ nhớ và sự kết hợp bộ nhớ cache"

Mô hình bộ nhớ trực quan (và hạn chế nhất) là tính nhất quán tuần tự (SC) trong đó một thực thi đa luồng sẽ trông giống như một xen kẽ các thực thi tuần tự của từng luồng cấu thành, như thể các luồng được ghép theo thời gian trên bộ xử lý lõi đơn.

Thứ tự bộ nhớ toàn cầu đó có thể thay đổi từ một lần chạy chương trình này sang chương trình khác và có thể không được biết trước. Đặc điểm đặc trưng của SC là tập hợp các lát cắt ngang trong sơ đồ địa chỉ không gian-thời gian biểu thị các mặt phẳng của đồng thời (nghĩa là hình ảnh bộ nhớ). Trên một mặt phẳng nhất định, tất cả các sự kiện của nó (hoặc giá trị bộ nhớ) là đồng thời. Có một khái niệm về Thời gian tuyệt đối , trong đó tất cả các luồng đồng ý về các giá trị bộ nhớ đồng thời. Trong SC, tại mọi thời điểm tức thì, chỉ có một hình ảnh bộ nhớ được chia sẻ bởi tất cả các luồng. Đó là, tại mọi thời điểm, tất cả các bộ xử lý đều đồng ý với hình ảnh bộ nhớ (nghĩa là nội dung tổng hợp của bộ nhớ). Điều này không chỉ có nghĩa là tất cả các luồng đều xem cùng một chuỗi các giá trị cho tất cả các vị trí bộ nhớ mà còn cho tất cả các bộ xử lý quan sát giống nhaukết hợp các giá trị của tất cả các biến. Điều này giống như việc nói tất cả các hoạt động của bộ nhớ (trên tất cả các vị trí bộ nhớ) được quan sát theo cùng một thứ tự bởi tất cả các luồng.

Trong các mô hình bộ nhớ thư giãn, mỗi luồng sẽ cắt xén không gian-thời gian theo cách riêng của nó, hạn chế duy nhất là các lát của mỗi luồng sẽ không giao nhau vì tất cả các luồng phải đồng ý về lịch sử của từng vị trí bộ nhớ riêng lẻ (tất nhiên , lát của các chủ đề khác nhau có thể, và sẽ, chéo nhau). Không có cách phổ quát nào để cắt nó ra (không có thông tin đặc quyền về không gian-thời gian). Các lát cắt không phải là mặt phẳng (hoặc tuyến tính). Chúng có thể bị cong và đây là những gì có thể làm cho một luồng đọc các giá trị được viết bởi một luồng khác theo thứ tự chúng được viết. Lịch sử của các vị trí bộ nhớ khác nhau có thể trượt (hoặc bị kéo dài) tùy ý với nhau khi được xem bởi bất kỳ luồng cụ thể nào. Mỗi luồng sẽ có một ý nghĩa khác nhau về các sự kiện (hoặc, tương đương, các giá trị bộ nhớ) đồng thời. Tập hợp các sự kiện (hoặc giá trị bộ nhớ) đồng thời với một luồng không đồng thời với luồng khác. Do đó, trong một mô hình bộ nhớ thư giãn, tất cả các luồng vẫn quan sát cùng một lịch sử (nghĩa là chuỗi các giá trị) cho từng vị trí bộ nhớ. Nhưng họ có thể quan sát các hình ảnh bộ nhớ khác nhau (nghĩa là kết hợp các giá trị của tất cả các vị trí bộ nhớ). Ngay cả khi hai vị trí bộ nhớ khác nhau được ghi bởi cùng một chuỗi theo thứ tự, hai giá trị mới được ghi có thể được quan sát theo thứ tự khác nhau bởi các luồng khác.

[Ảnh từ Wikipedia] Hình ảnh từ Wikipedia

Những người đọc quen thuộc với Thuyết tương đối đặc biệt của Einstein sẽ chú ý đến những gì tôi đang ám chỉ. Dịch các từ của Minkowski sang lĩnh vực mô hình bộ nhớ: không gian địa chỉ và thời gian là bóng của không gian địa chỉ-thời gian. Trong trường hợp này, mỗi người quan sát (tức là luồng) sẽ chiếu các bóng của sự kiện (tức là lưu trữ / tải bộ nhớ) lên đường thế giới của riêng anh ta (tức là trục thời gian của anh ta) và mặt phẳng đồng thời của chính anh ta (trục không gian địa chỉ của anh ta) . Các luồng trong mô hình bộ nhớ C ++ 11 tương ứng với các quan sát viên đang di chuyển tương đối với nhau trong thuyết tương đối đặc biệt. Tính nhất quán tuần tự tương ứng với không gian thời gian Galilê (nghĩa là tất cả các nhà quan sát đều đồng ý về một thứ tự tuyệt đối của các sự kiện và ý thức về tính đồng thời toàn cầu).

Sự giống nhau giữa các mô hình bộ nhớ và tính tương đối đặc biệt xuất phát từ thực tế là cả hai đều xác định một tập các sự kiện được sắp xếp một phần, thường được gọi là tập hợp nhân quả. Một số sự kiện (nghĩa là lưu trữ bộ nhớ) có thể ảnh hưởng (nhưng không bị ảnh hưởng bởi) các sự kiện khác. Một luồng C ++ 11 (hoặc người quan sát trong vật lý) không nhiều hơn một chuỗi (tức là một tập hợp hoàn toàn theo thứ tự) các sự kiện (ví dụ, tải bộ nhớ và lưu trữ đến các địa chỉ khác nhau có thể).

Trong thuyết tương đối, một số trật tự được khôi phục thành bức tranh có vẻ hỗn loạn của các sự kiện được sắp xếp một phần, vì thứ tự tạm thời duy nhất mà tất cả các nhà quan sát đồng ý là thứ tự giữa các sự kiện Timelike (nghĩa là những sự kiện có thể kết nối được bởi bất kỳ hạt nào đi chậm hơn hơn tốc độ ánh sáng trong chân không). Chỉ các sự kiện liên quan đến thời gian được đặt hàng bất biến. Thời gian trong Vật lý, Craig Callender .

Trong mô hình bộ nhớ C ++ 11, một cơ chế tương tự (mô hình nhất quán phát hành) được sử dụng để thiết lập các quan hệ nhân quả cục bộ này .

Để cung cấp một định nghĩa về tính nhất quán của bộ nhớ và một động lực để từ bỏ SC, tôi sẽ trích dẫn từ "Một Primer về tính nhất quán của bộ nhớ và sự kết hợp bộ nhớ cache"

Đối với máy bộ nhớ dùng chung, mô hình nhất quán bộ nhớ xác định hành vi có thể nhìn thấy về mặt kiến ​​trúc của hệ thống bộ nhớ của nó. Tiêu chí về tính chính xác cho một hành vi phân vùng lõi của bộ xử lý duy nhất giữa một kết quả chính xác, mộtnhiều lựa chọn không chính xác ”. Điều này là do kiến ​​trúc của bộ xử lý yêu cầu việc thực thi một luồng biến đổi một trạng thái đầu vào nhất định thành một trạng thái đầu ra được xác định rõ, ngay cả trên lõi không theo thứ tự. Tuy nhiên, các mô hình nhất quán bộ nhớ dùng chung liên quan đến tải và lưu trữ của nhiều luồng và thường cho phép thực thi đúngtrong khi không cho phép nhiều (nhiều) không chính xác. Khả năng nhiều lần thực thi chính xác là do ISA cho phép nhiều luồng thực thi đồng thời, thường có nhiều sự xen kẽ pháp lý có thể có của các hướng dẫn từ các luồng khác nhau.

Các mô hình nhất quán bộ nhớ thư giãn hoặc yếu được thúc đẩy bởi thực tế là hầu hết các thứ tự bộ nhớ trong các mô hình mạnh là không cần thiết. Nếu một luồng cập nhật mười mục dữ liệu và sau đó là cờ đồng bộ hóa, các lập trình viên thường không quan tâm nếu các mục dữ liệu được cập nhật theo thứ tự đối với nhau mà chỉ có tất cả các mục dữ liệu được cập nhật trước khi cờ được cập nhật (thường được triển khai bằng hướng dẫn FENCE ). Các mô hình thư giãn tìm cách nắm bắt sự linh hoạt trật tự tăng lên này và chỉ bảo toàn các đơn hàng mà lập trình viên yêu cầuĐể có được hiệu suất cao hơn và tính chính xác của SC. Ví dụ, trong các kiến ​​trúc nhất định, bộ đệm ghi FIFO được sử dụng bởi mỗi lõi để giữ kết quả của các cửa hàng đã cam kết (đã nghỉ hưu) trước khi ghi kết quả vào bộ đệm. Tối ưu hóa này tăng cường hiệu suất nhưng vi phạm SC. Bộ đệm ghi ẩn độ trễ của việc phục vụ một cửa hàng bỏ lỡ. Bởi vì các cửa hàng là phổ biến, có thể tránh bị đình trệ trên hầu hết trong số họ là một lợi ích quan trọng. Đối với bộ xử lý lõi đơn, bộ đệm ghi có thể được ẩn đi về mặt kiến ​​trúc bằng cách đảm bảo rằng tải để giải quyết A trả về giá trị của cửa hàng gần đây nhất cho A ngay cả khi một hoặc nhiều cửa hàng cho A nằm trong bộ đệm ghi. Điều này thường được thực hiện bằng cách bỏ qua giá trị của cửa hàng gần đây nhất đến A để tải từ A, trong đó, hầu hết các lần gần đây nhất được xác định theo thứ tự chương trình, hoặc bằng cách trì hoãn tải A nếu lưu trữ vào A trong bộ đệm ghi. Khi nhiều lõi được sử dụng, mỗi lõi sẽ có bộ đệm ghi bỏ qua riêng. Không có bộ đệm ghi, phần cứng là SC, nhưng với bộ đệm ghi, thì không, làm cho bộ đệm ghi có thể nhìn thấy về mặt kiến ​​trúc trong bộ xử lý đa lõi.

Sắp xếp lại cửa hàng tại cửa hàng có thể xảy ra nếu một lõi có bộ đệm ghi không phải là FIFO cho phép các cửa hàng khởi hành theo một thứ tự khác với thứ tự mà chúng đã nhập. Điều này có thể xảy ra nếu cửa hàng thứ nhất bỏ lỡ trong bộ đệm trong khi lần truy cập thứ hai hoặc nếu cửa hàng thứ hai có thể kết hợp với cửa hàng trước đó (nghĩa là trước cửa hàng đầu tiên). Sắp xếp lại tải tải cũng có thể xảy ra trên các lõi được lên lịch động thực hiện các lệnh ngoài lệnh của chương trình. Điều đó có thể hoạt động giống như sắp xếp lại các cửa hàng trên một lõi khác (Bạn có thể đưa ra một ví dụ xen kẽ giữa hai luồng không?). Sắp xếp lại tải trước đó với cửa hàng sau (sắp xếp lại cửa hàng tải) có thể gây ra nhiều hành vi không chính xác, chẳng hạn như tải một giá trị sau khi phát hành khóa bảo vệ nó (nếu cửa hàng là hoạt động mở khóa).

Bởi vì sự kết hợp bộ nhớ cache và tính nhất quán của bộ nhớ đôi khi bị nhầm lẫn, nên cũng có hướng dẫn này:

Không giống như tính nhất quán, sự kết hợp bộ nhớ cache không hiển thị đối với phần mềm và không bắt buộc. Sự kết hợp tìm cách làm cho các bộ nhớ cache của một hệ thống bộ nhớ chia sẻ trở nên vô hình về mặt chức năng như các bộ đệm trong một hệ thống đơn lõi. Sự kết hợp chính xác đảm bảo rằng một lập trình viên có thể xác định liệu và nơi một hệ thống có bộ nhớ cache bằng cách phân tích kết quả của tải và lưu trữ. Điều này là do sự kết hợp chính xác đảm bảo rằng bộ đệm không bao giờ kích hoạt hành vi chức năng mới hoặc khác nhau (lập trình viên vẫn có thể suy ra cấu trúc bộ đệm có khả năng sử dụng thời gianthông tin). Mục đích chính của các giao thức kết hợp bộ đệm là duy trì bất biến một trình đọc nhiều trình đọc (SWMR) cho mọi vị trí bộ nhớ. Một sự khác biệt quan trọng giữa sự kết hợp và tính nhất quán là sự kết hợp được chỉ định trên cơ sở vị trí trên mỗi bộ nhớ , trong khi tính nhất quán được chỉ định đối với tất cả các vị trí bộ nhớ.

Tiếp tục với bức tranh tinh thần của chúng tôi, bất biến SWMR tương ứng với yêu cầu vật lý rằng có nhiều nhất một hạt nằm ở bất kỳ vị trí nào nhưng có thể có vô số người quan sát ở bất kỳ vị trí nào.


52
+1 cho sự tương tự với tính tương đối đặc biệt, tôi đã cố gắng tạo ra sự tương tự tương tự. Tôi thường thấy các lập trình viên điều tra mã luồng cố gắng diễn giải hành vi như các hoạt động trong các luồng khác nhau xảy ra xen kẽ với nhau theo một thứ tự cụ thể, và tôi phải nói với họ, không, với các hệ thống đa bộ xử lý khái niệm về tính đồng thời giữa các <s khác nhau > khung tham chiếu </ s> chủ đề bây giờ là vô nghĩa. So sánh với thuyết tương đối đặc biệt là một cách tốt để khiến họ tôn trọng sự phức tạp của vấn đề.
Pierre Lebeaupin

71
Vì vậy, bạn nên kết luận rằng vũ trụ là đa lõi?
Peter K

6
@PeterK: Chính xác :) Và đây là một hình ảnh rất đẹp về bức tranh thời gian này của nhà vật lý Brian Greene: youtube.com/watch?v=4BjGWLJNPcA&t=22m12s Đây là "Ảo ảnh về thời gian [Tài liệu đầy đủ]" vào phút 22 và 12 giây.
Ahmed Nassar

2
Có phải tôi hay anh ta đang chuyển từ mô hình bộ nhớ 1D (trục ngang) sang mô hình bộ nhớ 2D (các mặt phẳng của tính đồng thời). Tôi thấy điều này hơi khó hiểu nhưng có lẽ đó là vì tôi không phải là người bản ngữ ... Vẫn là một bài đọc rất thú vị.
Tạm biệt SE

Bạn đã quên một phần thiết yếu: " bằng cách phân tích kết quả của tải và lưu trữ " ... mà không sử dụng thông tin thời gian chính xác.
tò mò

115

Đây bây giờ là một câu hỏi nhiều năm tuổi, nhưng rất phổ biến, đáng nói đến một tài nguyên tuyệt vời để tìm hiểu về mô hình bộ nhớ C ++ 11. Tôi thấy không có điểm nào trong việc tóm tắt cuộc nói chuyện của anh ấy để đưa ra câu trả lời đầy đủ này, nhưng cho rằng đây là người thực sự viết tiêu chuẩn, tôi nghĩ rằng nó cũng đáng để xem cuộc nói chuyện.

Herb Sutter có cuộc nói chuyện dài ba giờ về mô hình bộ nhớ C ++ 11 có tiêu đề "nguyên tử <> Vũ khí", có sẵn trên trang Channel9 - phần 1phần 2 . Bài nói chuyện khá kỹ thuật và bao gồm các chủ đề sau:

  1. Tối ưu hóa, chủng tộc và mô hình bộ nhớ
  2. Đặt hàng - Cái gì: Mua và phát hành
  3. Đặt hàng - Cách thực hiện: Mutexes, Nguyên tử và / hoặc Hàng rào
  4. Các hạn chế khác đối với Trình biên dịch và Phần cứng
  5. Mã Gen & Hiệu suất: x86 / x64, IA64, POWER, ARM
  6. Nguyên tử thư giãn

Cuộc nói chuyện không nói chi tiết về API, mà là về lý luận, bối cảnh, dưới mui xe và hậu trường (bạn có biết ngữ nghĩa thoải mái chỉ được thêm vào tiêu chuẩn vì POWER và ARM không hỗ trợ tải đồng bộ hiệu quả?).


10
Cuộc nói chuyện đó thực sự tuyệt vời, hoàn toàn xứng đáng với 3 giờ bạn sẽ dành để xem nó.
ZunTzu

5
@ZunTzu: trên hầu hết các trình phát video, bạn có thể đặt tốc độ thành 1,25, 1,5 hoặc thậm chí gấp 2 lần so với ban đầu.
Christian Severin

4
@eran các bạn có tình cờ có slide không? liên kết trên kênh 9 trang thảo luận không hoạt động.
vận động viên

2
@athos Tôi không có chúng, xin lỗi. Hãy thử liên hệ với kênh 9, tôi không nghĩ việc xóa là có chủ ý (tôi đoán là họ đã nhận được liên kết từ Herb Sutter, được đăng như hiện tại và sau đó anh ta đã xóa các tệp; nhưng đó chỉ là suy đoán ...).
eran

75

Điều đó có nghĩa là tiêu chuẩn hiện xác định đa luồng và nó xác định những gì xảy ra trong ngữ cảnh của nhiều luồng. Tất nhiên, mọi người đã sử dụng các cách triển khai khác nhau, nhưng điều đó giống như hỏi tại sao chúng ta nên có một std::stringkhi tất cả chúng ta có thể sử dụng một stringlớp học tại nhà .

Khi bạn đang nói về các luồng POSIX hoặc các luồng Windows, thì đây chỉ là một ảo ảnh vì thực tế bạn đang nói về các luồng x86, vì đây là chức năng phần cứng để chạy đồng thời. Mô hình bộ nhớ C ++ 0x đảm bảo, cho dù bạn đang sử dụng x86, ARM hay MIPS hay bất kỳ thứ gì khác mà bạn có thể nghĩ ra.


28
Chủ đề Posix không bị giới hạn ở x86. Thật vậy, các hệ thống đầu tiên chúng được triển khai có lẽ không phải là các hệ thống x86. Chủ đề Posix độc lập với hệ thống và hợp lệ trên tất cả các nền tảng Posix. Điều đó cũng không thực sự đúng vì đó là một tài sản phần cứng vì các luồng Posix cũng có thể được thực hiện thông qua đa nhiệm hợp tác. Nhưng tất nhiên hầu hết các vấn đề luồng chỉ xuất hiện trên các triển khai luồng phần cứng (và một số thậm chí chỉ trên các hệ thống đa bộ xử lý / đa lõi).
celtschk

57

Đối với các ngôn ngữ không chỉ định mô hình bộ nhớ, bạn đang viết mã cho ngôn ngữ mô hình bộ nhớ được chỉ định bởi kiến ​​trúc bộ xử lý. Bộ xử lý có thể chọn sắp xếp lại các truy cập bộ nhớ để thực hiện. Vì vậy, nếu chương trình của bạn có các cuộc đua dữ liệu (một cuộc đua dữ liệu là khi nhiều lõi / siêu luồng có thể truy cập cùng một bộ nhớ) thì chương trình của bạn không phải là nền tảng chéo vì phụ thuộc vào mô hình bộ nhớ của bộ xử lý. Bạn có thể tham khảo hướng dẫn sử dụng phần mềm Intel hoặc AMD để tìm hiểu cách bộ xử lý có thể sắp xếp lại truy cập bộ nhớ.

Rất quan trọng, các khóa (và ngữ nghĩa đồng thời có khóa) thường được triển khai theo cách đa nền tảng ... Vì vậy, nếu bạn đang sử dụng các khóa tiêu chuẩn trong một chương trình đa luồng không có chủng tộc dữ liệu thì bạn không phải lo lắng về các mô hình bộ nhớ đa nền tảng .

Thật thú vị, các trình biên dịch của Microsoft cho C ++ đã thu được / phát hành ngữ nghĩa cho tính không ổn định, đó là một phần mở rộng C ++ để đối phó với việc thiếu mô hình bộ nhớ trong C ++ http://msdn.microsoft.com/en-us/l Library / 12a04hfd (v = vs .80) .aspx . Tuy nhiên, do Windows chỉ chạy trên x86 / x64, điều đó không có gì nhiều (các mô hình bộ nhớ Intel và AMD giúp việc triển khai ngữ nghĩa thu nhận / phát hành trong ngôn ngữ trở nên dễ dàng và hiệu quả).


2
Đúng là, khi câu trả lời được viết, Windows chỉ chạy trên x86 / x64, nhưng Windows chạy, tại một số thời điểm, trên IA64, MIPS, Alpha AXP64, PowerPC và ARM. Ngày nay, nó chạy trên các phiên bản khác nhau của ARM, bộ nhớ khá khác so với x86 và gần như không thể tha thứ.
Lorenzo Dematté

Liên kết đó có phần bị hỏng (nói "Tài liệu đã nghỉ hưu của Visual Studio 2005" ). Muốn cập nhật nó?
Peter Mortensen

3
Điều đó không đúng ngay cả khi câu trả lời được viết.
Ben

" Truy cập cùng một bộ nhớ " để truy cập theo cách xung đột
tò mò

27

Nếu bạn sử dụng mutexes để bảo vệ tất cả dữ liệu của mình, bạn thực sự không cần phải lo lắng. Mutexes luôn cung cấp đủ thứ tự và đảm bảo khả năng hiển thị.

Bây giờ, nếu bạn đã sử dụng các nguyên tử, hoặc các thuật toán không khóa, bạn cần suy nghĩ về mô hình bộ nhớ. Mô hình bộ nhớ mô tả chính xác khi các nguyên tử cung cấp bảo đảm trật tự và khả năng hiển thị và cung cấp hàng rào di động cho các bảo đảm được mã hóa bằng tay.

Trước đây, nguyên tử sẽ được thực hiện bằng cách sử dụng nội tại trình biên dịch, hoặc một số thư viện cấp cao hơn. Hàng rào sẽ được thực hiện bằng cách sử dụng các hướng dẫn dành riêng cho CPU (rào cản bộ nhớ).


19
Vấn đề trước đây là không có thứ gọi là mutex (về tiêu chuẩn C ++). Vì vậy, đảm bảo duy nhất bạn được cung cấp là bởi nhà sản xuất mutex, điều này vẫn ổn miễn là bạn không chuyển mã (vì những thay đổi nhỏ đối với bảo đảm là khó phát hiện). Bây giờ chúng tôi nhận được các đảm bảo được cung cấp bởi tiêu chuẩn có thể di động giữa các nền tảng.
Martin York

4
@Martin: trong mọi trường hợp, một điều là mô hình bộ nhớ và một điều nữa là các nguyên tử luồng và nguyên tử chạy trên mô hình bộ nhớ đó.
ninjalj

4
Ngoài ra, quan điểm của tôi chủ yếu là trước đây hầu như không có mô hình bộ nhớ ở cấp độ ngôn ngữ, nó tình cờ là mô hình bộ nhớ của CPU bên dưới. Bây giờ có một mô hình bộ nhớ là một phần của ngôn ngữ cốt lõi; OTOH, mutexes và tương tự luôn có thể được thực hiện như một thư viện.
ninjalj

3
Nó cũng có thể là một vấn đề thực sự cho những người đang cố gắng viết thư viện mutex. Khi CPU, bộ điều khiển bộ nhớ, nhân, trình biên dịch và "thư viện C" đều được thực hiện bởi các nhóm khác nhau và một số trong số đó không đồng ý về cách thức hoạt động của công cụ này, đôi khi là công cụ chúng tôi lập trình viên hệ thống phải làm để trình bày một mặt tiền đẹp đến mức ứng dụng không dễ chịu chút nào.
zwol

11
Thật không may, nó không đủ để bảo vệ cấu trúc dữ liệu của bạn bằng các đột biến đơn giản nếu không có mô hình bộ nhớ nhất quán trong ngôn ngữ của bạn. Có nhiều tối ưu hóa trình biên dịch khác nhau có ý nghĩa trong một ngữ cảnh luồng đơn nhưng khi nhiều luồng và lõi cpu hoạt động, việc sắp xếp lại các truy cập bộ nhớ và các tối ưu hóa khác có thể mang lại hành vi không xác định. Để biết thêm thông tin, hãy xem "Chủ đề không thể được triển khai như một thư viện" của Hans Boehm: citeseer.ist.psu.edu/viewdoc/
Kẻ

0

Các câu trả lời trên có được ở các khía cạnh cơ bản nhất của mô hình bộ nhớ C ++. Trong thực tế, hầu hết sử dụng std::atomic<>"chỉ hoạt động", ít nhất là cho đến khi lập trình viên tối ưu hóa quá mức (ví dụ: bằng cách cố gắng thư giãn quá nhiều thứ).

Có một nơi mà những sai lầm vẫn còn phổ biến: khóa trình tự . Có một cuộc thảo luận tuyệt vời và dễ đọc về các thách thức tại https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf . Khóa trình tự là hấp dẫn bởi vì người đọc tránh viết vào từ khóa. Đoạn mã sau dựa trên Hình 1 của báo cáo kỹ thuật ở trên và nó nêu bật những thách thức khi thực hiện khóa trình tự trong C ++:

atomic<uint64_t> seq; // seqlock representation
int data1, data2;     // this data will be protected by seq

T reader() {
    int r1, r2;
    unsigned seq0, seq1;
    while (true) {
        seq0 = seq;
        r1 = data1; // INCORRECT! Data Race!
        r2 = data2; // INCORRECT!
        seq1 = seq;

        // if the lock didn't change while I was reading, and
        // the lock wasn't held while I was reading, then my
        // reads should be valid
        if (seq0 == seq1 && !(seq0 & 1))
            break;
    }
    use(r1, r2);
}

void writer(int new_data1, int new_data2) {
    unsigned seq0 = seq;
    while (true) {
        if ((!(seq0 & 1)) && seq.compare_exchange_weak(seq0, seq0 + 1))
            break; // atomically moving the lock from even to odd is an acquire
    }
    data1 = new_data1;
    data2 = new_data2;
    seq = seq0 + 2; // release the lock by increasing its value to even
}

Đầu tiên là không trực quan, data1data2cần phải được atomic<>. Nếu chúng không phải là nguyên tử, thì chúng có thể được đọc (in reader()) cùng một lúc với chúng được viết (in writer()). Theo mô hình bộ nhớ C ++, đây là một cuộc đua ngay cả khi reader()không bao giờ thực sự sử dụng dữ liệu . Ngoài ra, nếu chúng không phải là nguyên tử, thì trình biên dịch có thể lưu trữ lần đọc đầu tiên của mỗi giá trị trong một thanh ghi. Rõ ràng là bạn sẽ không muốn điều đó ... bạn muốn đọc lại trong mỗi lần lặp của whilevòng lặp reader().

Nó cũng không đủ để làm cho họ atomic<>và truy cập chúng với memory_order_relaxed. Lý do cho điều này là các lần đọc seq (in reader()) chỉ có được ngữ nghĩa. Nói một cách đơn giản, nếu X và Y là truy cập bộ nhớ, X đứng trước Y, X không phải là thu hoặc phát hành và Y là thu nhận, thì trình biên dịch có thể sắp xếp lại Y trước X. Nếu Y là lần đọc thứ hai của seq và X là một dữ liệu đọc, việc sắp xếp lại như vậy sẽ phá vỡ việc thực hiện khóa.

Bài viết đưa ra một vài giải pháp. Một với hiệu suất tốt nhất hiện nay có lẽ là một trong đó sử dụng một atomic_thread_fencevới memory_order_relaxed trước khi đọc thứ hai của seqlock. Trong bài báo, đó là Hình 6. Tôi không sao chép mã ở đây, bởi vì bất kỳ ai đã đọc đến nay thực sự phải đọc bài báo. Nó chính xác và đầy đủ hơn bài này.

Vấn đề cuối cùng là nó có thể không tự nhiên để databiến các nguyên tử. Nếu bạn không thể trong mã của mình, thì bạn cần phải rất cẩn thận, bởi vì việc chuyển từ phi nguyên tử sang nguyên tử chỉ hợp pháp đối với các loại nguyên thủy. C ++ 20 được cho là thêm atomic_ref<>, điều này sẽ giúp giải quyết vấn đề này dễ dàng hơn.

Tóm lại: ngay cả khi bạn nghĩ rằng bạn hiểu mô hình bộ nhớ C ++, bạn nên rất cẩn thận trước khi thực hiện các khóa trình tự của riêng mình.


-2

C và C ++ từng được xác định bởi dấu vết thực hiện của một chương trình được hình thành tốt.

Bây giờ chúng được xác định một nửa bởi dấu vết thực thi của chương trình và nửa sau bởi nhiều thứ tự trên các đối tượng đồng bộ hóa.

Có nghĩa là các định nghĩa ngôn ngữ này hoàn toàn không có ý nghĩa vì không có phương pháp logic nào để trộn lẫn hai cách tiếp cận này. Cụ thể, sự phá hủy của một biến thể hoặc biến nguyên tử không được xác định rõ.


Tôi chia sẻ mong muốn mãnh liệt của bạn về cải tiến thiết kế ngôn ngữ, nhưng tôi nghĩ câu trả lời của bạn sẽ có giá trị hơn nếu nó tập trung vào một trường hợp đơn giản, trong đó bạn đã cho thấy rõ ràng và rõ ràng hành vi đó vi phạm các nguyên tắc thiết kế ngôn ngữ cụ thể như thế nào. Sau đó, tôi thực sự khuyên bạn, nếu bạn cho phép tôi, hãy đưa ra câu trả lời đó một lập luận rất tốt về mức độ phù hợp của từng điểm đó, bởi vì chúng sẽ trái ngược với sự liên quan của lợi ích năng suất vô hạn mà thiết kế C ++
Matias nhận thấy Haeussler

1
@MatiasHaeussler Tôi nghĩ bạn đã đọc sai câu trả lời của tôi; Tôi không phản đối định nghĩa của một tính năng C ++ cụ thể ở đây (tôi cũng có nhiều chỉ trích như vậy nhưng không phải ở đây). Tôi đang tranh luận ở đây rằng không có cấu trúc được xác định rõ trong C ++ (cũng như C). Toàn bộ ngữ nghĩa MT là một mớ hỗn độn, vì bạn không còn có ngữ nghĩa tuần tự nữa. (Tôi tin rằng Java MT bị hỏng nhưng ít hơn.) "Ví dụ đơn giản" sẽ gần như là bất kỳ chương trình MT nào. Nếu bạn không đồng ý, rất mong bạn trả lời câu hỏi của tôi về cách chứng minh tính đúng đắn của các chương trình MT C ++ .
tò mò

Thật thú vị, tôi nghĩ tôi hiểu nhiều hơn những gì bạn muốn nói sau khi đọc Questiong của bạn. Nếu tôi đúng, bạn đang đề cập đến việc không thể phát triển bằng chứng cho tính chính xác của chương trình C ++ MT . Trong trường hợp như vậy tôi sẽ nói rằng đối với tôi là một điều gì đó có tầm quan trọng rất lớn đối với tương lai của lập trình máy tính, đặc biệt là sự xuất hiện của sự thâm nhập nhân tạo. Nhưng tôi cũng sẽ chỉ ra rằng phần lớn mọi người đặt câu hỏi trong stack stack không phải là điều họ thậm chí không biết và thậm chí sau khi hiểu ý của bạn và trở nên quan tâm
Matias Haeussler

1
"Các câu hỏi về khả năng phá hủy của các chương trình máy tính nên được đăng trong stackoverflow hoặc trong stackexchange (nếu không, ở đâu)?" Cái này có vẻ là một cho meta stackoverflow, phải không?
Matias Haeussler

1
@MatiasHaeussler 1) C và C ++ về cơ bản chia sẻ "mô hình bộ nhớ" của các biến nguyên tử, mutexes và multithreading. 2) Sự liên quan về điều này là về lợi ích của việc có "mô hình bộ nhớ". Tôi nghĩ lợi ích bằng không vì mô hình là không có cơ sở.
tò mò
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.