Đầ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ử và 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 0
như đầ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 0
khô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 0
hoặ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 .