Tại sao dễ bay hơi không được coi là hữu ích trong lập trình C hoặc C ++ đa luồng?


165

Như đã trình bày trong câu trả lời này tôi mới đăng, tôi dường như bối rối về tiện ích (hoặc thiếu nó) volatiletrong bối cảnh lập trình đa luồng.

Hiểu biết của tôi là thế này: bất cứ khi nào một biến có thể được thay đổi ngoài luồng kiểm soát của một đoạn mã truy cập vào nó, biến đó sẽ được khai báo volatile. Trình xử lý tín hiệu, thanh ghi I / O và các biến được sửa đổi bởi một luồng khác đều tạo thành các tình huống như vậy.

Vì vậy, nếu bạn có int toàn cầu foofoođược đọc bởi một luồng và được đặt nguyên tử bởi một luồng khác (có thể sử dụng một lệnh máy thích hợp), thì luồng đọc sẽ thấy tình huống này giống như cách nó nhìn thấy một biến được điều chỉnh bởi trình xử lý tín hiệu hoặc được sửa đổi bởi một điều kiện phần cứng bên ngoài và do đó foonên được khai báo volatile(hoặc, đối với các tình huống đa luồng, được truy cập với tải có hàng rào bộ nhớ, có lẽ là một giải pháp tốt hơn).

Làm thế nào và ở đâu tôi sai?


7
Tất cả các biến động đều nói rằng trình biên dịch không nên lưu trữ quyền truy cập vào một biến dễ bay hơi. Nó không nói gì về việc xâu chuỗi truy cập như vậy. Điều này đã được thảo luận ở đây Tôi không biết bao nhiêu lần và tôi không nghĩ câu hỏi này sẽ thêm bất cứ điều gì vào những cuộc thảo luận đó.

4
Và một lần nữa, một câu hỏi không xứng đáng, và đã được hỏi ở đây nhiều lần trước khi được nêu lên. Bạn sẽ vui lòng ngừng làm điều này.

14
@neil Tôi đã tìm kiếm các câu hỏi khác và tìm thấy một câu hỏi, nhưng bất kỳ lời giải thích nào tôi thấy bằng cách nào đó đã không kích hoạt những gì tôi cần để thực sự hiểu tại sao tôi sai. Câu hỏi này đã gợi ra một câu trả lời như vậy.
Michael Ekstrand

1
Để có một nghiên cứu chuyên sâu về những gì CPU làm với dữ liệu (thông qua bộ nhớ cache của họ), hãy xem: rdrop.com/users/paulmck/scalability/apers/whymb.2010,06,0c.pdf
Sassafras_wot

1
@cquilguy Đó là ý của tôi với "không phải trường hợp trong C", trong đó nó có thể được sử dụng để ghi vào các thanh ghi phần cứng, v.v. và không được sử dụng cho đa luồng như nó thường được sử dụng trong Java.
Monstieur

Câu trả lời:


213

Vấn đề với volatiletrong bối cảnh đa luồng là nó không cung cấp tất cả các đảm bảo mà chúng ta cần. Nó có một vài thuộc tính chúng ta cần, nhưng không phải tất cả chúng, vì vậy chúng ta không thể chỉ dựa vào volatile một mình .

Tuy nhiên, các nguyên thủy chúng ta phải sử dụng cho các thuộc tính còn lại cũng cung cấp các thuộc tính đó volatile, vì vậy nó thực sự không cần thiết.

Để truy cập an toàn luồng vào dữ liệu được chia sẻ, chúng tôi cần đảm bảo rằng:

  • việc đọc / ghi thực sự xảy ra (rằng trình biên dịch sẽ không lưu trữ giá trị trong một thanh ghi thay vào đó và trì hoãn cập nhật bộ nhớ chính cho đến sau này)
  • rằng không có sự sắp xếp lại diễn ra. Giả sử rằng chúng ta sử dụng một volatilebiến làm cờ để cho biết liệu một số dữ liệu đã sẵn sàng để đọc hay chưa. Trong mã của chúng tôi, chúng tôi chỉ đơn giản là đặt cờ sau khi chuẩn bị dữ liệu, vì vậy tất cả ngoại hình tốt. Nhưng nếu các hướng dẫn được sắp xếp lại để cờ được đặt đầu tiên thì sao?

volatilekhông đảm bảo điểm đầu tiên. Nó cũng đảm bảo rằng không có sự sắp xếp lại xảy ra giữa các lần đọc / ghi dễ bay hơi khác nhau . Tất cả các volatiletruy cập bộ nhớ sẽ xảy ra theo thứ tự được chỉ định. Đó là tất cả những gì chúng ta cần cho những gì volatileđược dự định: thao tác các thanh ghi I / O hoặc phần cứng được ánh xạ bộ nhớ, nhưng nó không giúp chúng ta trong mã đa luồng trong đó volatileđối tượng thường chỉ được sử dụng để đồng bộ hóa truy cập vào dữ liệu không bay hơi. Những truy cập vẫn có thể được sắp xếp lại liên quan đến volatilenhững người truy cập .

Giải pháp để ngăn chặn sắp xếp lại là sử dụng hàng rào bộ nhớ , cho biết cả trình biên dịch và CPU không có quyền truy cập bộ nhớ có thể được sắp xếp lại theo điểm này . Đặt các rào cản như vậy xung quanh truy cập biến dễ bay hơi của chúng tôi đảm bảo rằng ngay cả các truy cập không dễ bay hơi sẽ không được sắp xếp lại theo thứ tự dễ bay hơi, cho phép chúng tôi viết mã an toàn cho chuỗi.

Tuy nhiên, các rào cản bộ nhớ cũng đảm bảo rằng tất cả các lần đọc / ghi đang chờ xử lý được thực thi khi đạt đến rào cản, do đó, nó mang lại cho chúng ta mọi thứ chúng ta cần một cách hiệu quả, volatilekhông cần thiết. Chúng tôi chỉ có thể loại bỏ volatilehoàn toàn vòng loại.

Kể từ C ++ 11, các biến nguyên tử ( std::atomic<T>) cung cấp cho chúng ta tất cả các đảm bảo có liên quan.


5
@jbcreix: Bạn đang hỏi về "nó" nào? Rào cản dễ bay hơi hay bộ nhớ? Trong mọi trường hợp, câu trả lời là khá nhiều như nhau. Cả hai đều phải làm việc ở cả trình biên dịch và mức CPU, vì chúng mô tả hành vi có thể quan sát được của chương trình --- vì vậy họ phải đảm bảo rằng CPU không sắp xếp lại mọi thứ, thay đổi hành vi mà chúng đảm bảo. Nhưng hiện tại bạn không thể viết đồng bộ hóa luồng di động, vì các rào cản bộ nhớ không phải là một phần của C ++ tiêu chuẩn (vì vậy chúng không thể mang theo được) và volatilekhông đủ mạnh để trở nên hữu ích.
jalf

4
Một ví dụ MSDN thực hiện điều này và tuyên bố rằng các hướng dẫn không thể được sắp xếp lại qua quyền truy cập dễ bay hơi: msdn.microsoft.com/en-us/l
Library / 12a04hfd (v = vs.80) .aspx

27
@OJW: Nhưng trình biên dịch của Microsoft xác định lại volatilelà một rào cản bộ nhớ đầy đủ (ngăn chặn sắp xếp lại). Đó không phải là một phần của tiêu chuẩn, vì vậy bạn không thể dựa vào hành vi này trong mã di động.
jalf

4
@Skizz: không, đó là nơi phần "ma thuật trình biên dịch" của phương trình xuất hiện. Một rào cản bộ nhớ phải được hiểu bởi cả CPU và trình biên dịch. Nếu trình biên dịch hiểu ngữ nghĩa của một rào cản bộ nhớ, thì nó biết để tránh các thủ thuật như thế (cũng như sắp xếp lại các lần đọc / ghi trên hàng rào). Và may mắn thay, trình biên dịch không hiểu ngữ nghĩa của một rào cản bộ nhớ, vì vậy cuối cùng, tất cả đều hoạt động. :)
jalf

13
@Skizz: Bản thân các chủ đề luôn là một tiện ích mở rộng phụ thuộc vào nền tảng trước C ++ 11 và C11. Theo hiểu biết của tôi, mọi môi trường C và C ++ cung cấp tiện ích mở rộng luồng cũng cung cấp tiện ích mở rộng "hàng rào bộ nhớ". Bất kể, volatileluôn luôn vô dụng cho lập trình đa luồng. (Ngoại trừ trong Visual Studio, nơi dễ bay hơi phần mở rộng hàng rào bộ nhớ.)
Nemo

49

Bạn cũng có thể xem xét điều này từ Tài liệu hạt nhân Linux .

Các lập trình viên C thường lấy biến động để có nghĩa là biến có thể được thay đổi bên ngoài luồng thực thi hiện tại; kết quả là, đôi khi họ bị cám dỗ sử dụng nó trong mã hạt nhân khi các cấu trúc dữ liệu được chia sẻ đang được sử dụng. Nói cách khác, chúng đã được biết là coi các loại dễ bay hơi như một loại biến nguyên tử dễ dàng, mà chúng không phải là. Việc sử dụng biến động trong mã kernel hầu như không bao giờ đúng; tài liệu này mô tả lý do tại sao.

Điểm mấu chốt để hiểu về sự biến động là mục đích của nó là triệt tiêu tối ưu hóa, điều gần như không bao giờ là điều người ta thực sự muốn làm. Trong kernel, người ta phải bảo vệ các cấu trúc dữ liệu được chia sẻ trước sự truy cập đồng thời không mong muốn, đây là một nhiệm vụ khác. Quá trình bảo vệ chống lại sự tương tranh không mong muốn cũng sẽ tránh được hầu hết các vấn đề liên quan đến tối ưu hóa theo cách hiệu quả hơn.

Giống như dễ bay hơi, các nguyên hàm hạt nhân giúp truy cập đồng thời vào dữ liệu an toàn (spinlocks, mutexes, rào cản bộ nhớ, v.v.) được thiết kế để ngăn chặn tối ưu hóa không mong muốn. Nếu chúng đang được sử dụng đúng cách, sẽ không có nhu cầu sử dụng dễ bay hơi. Nếu không ổn định vẫn cần thiết, gần như chắc chắn có một lỗi trong mã ở đâu đó. Trong mã hạt nhân được viết đúng, dễ bay hơi chỉ có thể phục vụ để làm chậm mọi thứ.

Hãy xem xét một khối mã hạt nhân điển hình:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

Nếu tất cả các mã tuân theo các quy tắc khóa, giá trị của shared_data không thể thay đổi bất ngờ trong khi giữ khóa_lock. Bất kỳ mã nào khác có thể muốn phát với dữ liệu đó sẽ chờ trên khóa. Các nguyên thủy spinlock hoạt động như các rào cản bộ nhớ - chúng được viết rõ ràng để làm như vậy - có nghĩa là các truy cập dữ liệu sẽ không được tối ưu hóa trên chúng. Vì vậy, trình biên dịch có thể nghĩ rằng nó biết những gì sẽ có trong shared_data, nhưng lệnh gọi spin_lock (), vì nó hoạt động như một rào cản bộ nhớ, sẽ buộc nó quên bất cứ điều gì nó biết. Sẽ không có vấn đề tối ưu hóa với việc truy cập vào dữ liệu đó.

Nếu shared_data được khai báo là không ổn định, việc khóa vẫn sẽ là cần thiết. Nhưng trình biên dịch cũng sẽ bị ngăn chặn tối ưu hóa quyền truy cập vào shared_data trong phần quan trọng, khi chúng tôi biết rằng không ai khác có thể làm việc với nó. Trong khi khóa được giữ, shared_data không biến động. Khi xử lý dữ liệu chia sẻ, khóa thích hợp làm cho biến động không cần thiết - và có thể gây hại.

Lớp lưu trữ dễ bay hơi ban đầu có nghĩa là cho các thanh ghi I / O được ánh xạ bộ nhớ. Trong kernel, các truy cập đăng ký cũng nên được bảo vệ bởi các khóa, nhưng người ta cũng không muốn trình biên dịch "tối ưu hóa" các truy cập đăng ký trong một phần quan trọng. Nhưng, trong nhân, việc truy cập bộ nhớ I / O luôn được thực hiện thông qua các hàm truy cập; truy cập bộ nhớ I / O trực tiếp thông qua con trỏ được tán thành và không hoạt động trên tất cả các kiến ​​trúc. Những người truy cập được viết để ngăn chặn tối ưu hóa không mong muốn, vì vậy, một lần nữa, biến động là không cần thiết.

Một tình huống khác mà người ta có thể muốn sử dụng dễ bay hơi là khi bộ xử lý đang bận chờ đợi giá trị của một biến. Cách đúng đắn để thực hiện chờ đợi bận rộn là:

while (my_variable != what_i_want)
    cpu_relax();

Cuộc gọi cpu_relax () có thể giảm mức tiêu thụ năng lượng của CPU hoặc mang lại bộ xử lý sinh đôi siêu phân luồng; nó cũng xảy ra để phục vụ như một rào cản bộ nhớ, vì vậy, một lần nữa, biến động là không cần thiết. Tất nhiên, bận rộn chờ đợi nói chung là một hành động chống đối xã hội bắt đầu.

Vẫn còn một vài tình huống hiếm hoi mà sự biến động có ý nghĩa trong nhân:

  • Các hàm truy cập được đề cập ở trên có thể sử dụng không ổn định trên các kiến ​​trúc nơi truy cập bộ nhớ I / O trực tiếp không hoạt động. Về cơ bản, mỗi cuộc gọi của người truy cập trở thành một phần quan trọng nhỏ và đảm bảo rằng việc truy cập diễn ra như mong đợi của lập trình viên.

  • Mã lắp ráp nội tuyến thay đổi bộ nhớ, nhưng không có tác dụng phụ có thể nhìn thấy khác, có nguy cơ bị GCC xóa. Thêm từ khóa dễ bay hơi vào các câu lệnh asm sẽ ngăn chặn việc loại bỏ này.

  • Biến jiffies đặc biệt ở chỗ nó có thể có giá trị khác nhau mỗi lần được tham chiếu, nhưng nó có thể được đọc mà không cần khóa đặc biệt. Vì vậy, jiffies có thể không ổn định, nhưng việc bổ sung các biến khác của loại này được tán thành mạnh mẽ. Jiffies được coi là một vấn đề "di sản ngu ngốc" (lời của Linus) trong vấn đề này; sửa chữa nó sẽ là rắc rối nhiều hơn nó là giá trị.

  • Con trỏ tới cấu trúc dữ liệu trong bộ nhớ kết hợp có thể được sửa đổi bởi các thiết bị I / O đôi khi có thể biến động một cách hợp pháp. Bộ đệm vòng được sử dụng bởi bộ điều hợp mạng, trong đó bộ điều hợp thay đổi con trỏ để chỉ ra mô tả nào đã được xử lý, là một ví dụ về loại tình huống này.

Đối với hầu hết các mã, không có bất kỳ biện minh nào ở trên cho việc áp dụng dễ bay hơi. Do đó, việc sử dụng dễ bay hơi có thể được coi là một lỗi và sẽ mang lại sự xem xét kỹ lưỡng hơn cho mã. Các nhà phát triển bị cám dỗ sử dụng biến động nên lùi lại một bước và suy nghĩ về những gì họ đang thực sự cố gắng thực hiện.



1
Spin_lock () trông giống như một lệnh gọi hàm thông thường. Điều đặc biệt là trình biên dịch sẽ xử lý nó đặc biệt để mã được tạo sẽ "quên" mọi giá trị của shared_data đã được đọc trước spin_lock () và được lưu trữ trong một thanh ghi để giá trị phải được đọc lại trong do_s Something_on () sau spin_lock ()?
Syncopated

1
@underscore_d Quan điểm của tôi là tôi không thể nói từ tên hàm spin_lock () rằng nó có gì đặc biệt. Tôi không biết những gì trong đó. Đặc biệt, tôi không biết những gì trong quá trình triển khai ngăn trình biên dịch tối ưu hóa các lần đọc tiếp theo.
đảo phách

1
Syncopated có một điểm tốt. Điều này về cơ bản có nghĩa là lập trình viên nên biết việc thực hiện bên trong các "chức năng đặc biệt" đó hoặc ít nhất là được thông báo rất rõ về hành vi của họ. Điều này đặt ra các câu hỏi bổ sung, chẳng hạn như - các hàm đặc biệt này có được chuẩn hóa và đảm bảo để hoạt động theo cùng một cách trên tất cả các kiến ​​trúc và tất cả các trình biên dịch không? Có một danh sách các chức năng như vậy có sẵn hay ít nhất là có một quy ước sử dụng nhận xét mã để báo hiệu cho các nhà phát triển rằng chức năng được đề cập bảo vệ mã khỏi bị "tối ưu hóa"?
JustAMartin

1
@Tuntable: Một tĩnh riêng có thể được chạm bởi bất kỳ mã nào, thông qua một con trỏ. Và địa chỉ của nó đang được thực hiện. Có lẽ phân tích dataflow có khả năng chứng minh rằng con trỏ không bao giờ thoát, nhưng nói chung đó là một vấn đề rất khó khăn, siêu tuyến trong kích thước chương trình. Nếu bạn có một cách để đảm bảo rằng không có bí danh nào tồn tại, thì việc di chuyển truy cập qua khóa xoay thực sự sẽ ổn. Nhưng nếu không có bí danh tồn tại, thì volatilecũng vô nghĩa. Trong mọi trường hợp, hành vi "gọi đến một chức năng mà cơ thể không thể nhìn thấy" sẽ chính xác.
Ben Voigt

11

Tôi không nghĩ bạn sai - dễ bay hơi là cần thiết để đảm bảo rằng luồng A sẽ thấy giá trị thay đổi, nếu giá trị bị thay đổi bởi một thứ khác ngoài luồng A. Theo tôi hiểu, về cơ bản, biến động là một cách để nói với trình biên dịch "không lưu trữ biến này trong một thanh ghi, thay vào đó hãy đảm bảo luôn đọc / ghi nó từ bộ nhớ RAM trên mỗi lần truy cập".

Sự nhầm lẫn là do không ổn định không đủ để thực hiện một số điều. Đặc biệt, các hệ thống hiện đại sử dụng nhiều cấp bộ nhớ đệm, CPU đa lõi hiện đại thực hiện một số tối ưu hóa lạ mắt trong thời gian chạy và các trình biên dịch hiện đại thực hiện một số tối ưu hóa lạ mắt vào thời gian biên dịch và tất cả đều có thể dẫn đến các hiệu ứng phụ khác nhau xuất hiện trong một thứ tự từ thứ tự bạn mong đợi nếu bạn chỉ nhìn vào mã nguồn.

Vì vậy, dễ bay hơi là tốt, miễn là bạn nhớ rằng những thay đổi 'được quan sát' trong biến dễ bay hơi có thể không xảy ra vào thời điểm chính xác mà bạn nghĩ chúng sẽ xảy ra. Cụ thể, đừng cố sử dụng các biến dễ bay hơi như một cách để đồng bộ hóa hoặc đặt hàng các hoạt động giữa các luồng, bởi vì nó không hoạt động đáng tin cậy.

Cá nhân, cách sử dụng chính (duy nhất?) Của tôi cho cờ dễ bay hơi là dưới dạng boolean "PleaseGoAwayNow". Nếu tôi có một luồng công nhân lặp liên tục, tôi sẽ kiểm tra boolean dễ bay hơi trên mỗi lần lặp của vòng lặp và thoát nếu boolean là đúng. Sau đó, luồng chính có thể dọn sạch luồng công nhân một cách an toàn bằng cách đặt boolean thành true và sau đó gọi pthread_join () để đợi cho đến khi luồng worker biến mất.


2
Cờ Boolean của bạn có thể không an toàn. Làm thế nào để bạn đảm bảo rằng công nhân hoàn thành nhiệm vụ của mình và cờ vẫn ở trong phạm vi cho đến khi nó được đọc (nếu nó được đọc)? Đó là một công việc cho các tín hiệu. Dễ bay hơi là tốt để thực hiện spinlocks đơn giản nếu không có mutex, vì an toàn bí danh có nghĩa là trình biên dịch giả định mutex_lock(và mọi chức năng thư viện khác) có thể thay đổi trạng thái của biến cờ.
Potatoswatter

6
Rõ ràng là nó chỉ hoạt động nếu bản chất của thói quen của luồng công nhân là nó được đảm bảo để kiểm tra boolean định kỳ. Cờ dễ bay hơi được đảm bảo duy trì trong phạm vi vì trình tự tắt luồng luôn xảy ra trước khi đối tượng giữ boolean dễ bay hơi bị phá hủy và trình tự tắt luồng gọi pthread_join () sau khi đặt bool. pthread_join () sẽ chặn cho đến khi luồng worker đã biến mất. Tín hiệu có vấn đề riêng của họ, đặc biệt khi được sử dụng cùng với đa luồng.
Jeremy Friesner

2
Chuỗi công nhân không được đảm bảo để hoàn thành công việc của nó trước khi boolean là đúng - trên thực tế, nó gần như chắc chắn sẽ ở giữa một đơn vị công việc khi bool được đặt thành true. Nhưng nó không thành vấn đề khi luồng công nhân hoàn thành đơn vị công việc của nó, bởi vì luồng chính sẽ không làm gì ngoại trừ việc chặn bên trong pthread_join () cho đến khi luồng công nhân thoát ra, trong mọi trường hợp. Vì vậy, chuỗi tắt máy được sắp xếp hợp lý - bool dễ bay hơi (và mọi dữ liệu được chia sẻ khác) sẽ không được giải phóng cho đến khi pthread_join () trở lại và pthread_join () sẽ không quay lại cho đến khi hết chuỗi worker.
Jeremy Friesner

10
@Jeremy, bạn đúng trong thực tế nhưng về mặt lý thuyết nó vẫn có thể bị phá vỡ. Trên hệ thống hai lõi, một lõi liên tục thực thi luồng công nhân của bạn. Các lõi khác đặt bool thành đúng. Tuy nhiên, không có gì đảm bảo lõi của luồng công nhân sẽ nhìn thấy sự thay đổi đó, tức là nó có thể không bao giờ dừng lại mặc dù nó lặp đi lặp lại kiểm tra bool. Hành vi này được cho phép bởi các mô hình bộ nhớ c ++ 0x, java và c #. Trong thực tế, điều này sẽ không bao giờ xảy ra vì luồng bận rộn rất có thể chèn một rào cản bộ nhớ ở đâu đó, sau đó nó sẽ thấy sự thay đổi đối với bool.
deft_code

4
Sử dụng hệ thống POSIX, sử dụng chính sách lập lịch thời gian thực SCHED_FIFO, mức độ ưu tiên tĩnh cao hơn các quy trình / luồng khác trong hệ thống, đủ lõi, là điều hoàn toàn có thể. Trong Linux, bạn có thể chỉ định rằng quy trình thời gian thực có thể sử dụng 100% thời gian của CPU. Họ sẽ không bao giờ chuyển đổi ngữ cảnh nếu không có luồng / tiến trình ưu tiên cao hơn và không bao giờ chặn bởi I / O. Nhưng vấn đề là C / C ++ volatilekhông có nghĩa là để thực thi ngữ nghĩa chia sẻ / đồng bộ hóa dữ liệu phù hợp. Tôi thấy việc tìm kiếm các trường hợp đặc biệt để chứng minh rằng mã không chính xác đôi khi có thể hoạt động là bài tập vô dụng.
FooF

7

volatilelà hữu ích (mặc dù không đủ) để thực hiện cấu trúc cơ bản của một mutex spinlock, nhưng một khi bạn đã có nó (hoặc một cái gì đó vượt trội), bạn không cần cái khác volatile.

Cách điển hình của lập trình đa luồng không phải là bảo vệ mọi biến được chia sẻ ở cấp độ máy, mà là giới thiệu các biến bảo vệ hướng dẫn luồng chương trình. Thay vì volatile bool my_shared_flag;bạn nên có

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

Điều này không chỉ gói gọn "phần cứng", về cơ bản là cần thiết: C không bao gồm các hoạt động nguyên tử cần thiết để thực hiện một mutex; nó chỉ volatilephải đảm bảo thêm về các hoạt động thông thường .

Bây giờ bạn có một cái gì đó như thế này:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag không cần phải biến động, mặc dù không thể điều khiển được, bởi vì

  1. Một chủ đề khác có quyền truy cập vào nó.
  2. Có nghĩa là một tham chiếu đến nó phải được thực hiện đôi khi (với &toán tử).
    • (Hoặc một tham chiếu đã được đưa đến một cấu trúc có chứa)
  3. pthread_mutex_lock là một chức năng thư viện.
  4. Có nghĩa là trình biên dịch không thể biết nếu pthread_mutex_lockbằng cách nào đó có được tham chiếu đó.
  5. Có nghĩa là trình biên dịch phải giả định rằng pthread_mutex_locksửa đổi cờ chia sẻ !
  6. Vì vậy, biến phải được tải lại từ bộ nhớ. volatile, trong khi có ý nghĩa trong bối cảnh này, là không liên quan.

6

Hiểu biết của bạn thực sự là sai.

Thuộc tính, mà các biến dễ bay hơi có, là "đọc từ và ghi vào biến này là một phần của hành vi có thể nhận thấy của chương trình". Điều đó có nghĩa là chương trình này hoạt động (được cung cấp phần cứng phù hợp):

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

Vấn đề là, đây không phải là tài sản chúng tôi muốn từ bất cứ thứ gì an toàn.

Ví dụ: bộ đếm an toàn luồng sẽ chỉ là (mã giống như linux-kernel, không biết tương đương c ++ 0x):

atomic_t counter;

...
atomic_inc(&counter);

Đây là nguyên tử, không có rào cản bộ nhớ. Bạn nên thêm chúng nếu cần thiết. Việc thêm tính dễ bay hơi có thể sẽ không giúp ích gì, vì nó sẽ không liên quan đến quyền truy cập vào mã gần đó (ví dụ: nối thêm một phần tử vào danh sách mà bộ đếm đang đếm). Chắc chắn, bạn không cần phải xem bộ đếm tăng lên bên ngoài chương trình của bạn và tối ưu hóa vẫn là mong muốn, ví dụ.

atomic_inc(&counter);
atomic_inc(&counter);

vẫn có thể được tối ưu hóa để

atomically {
  counter+=2;
}

nếu trình tối ưu hóa đủ thông minh (nó không thay đổi ngữ nghĩa của mã).


6

Để dữ liệu của bạn nhất quán trong môi trường đồng thời, bạn cần có hai điều kiện để áp dụng:

1) Tính nguyên tử tức là nếu tôi đọc hoặc ghi một số dữ liệu vào bộ nhớ thì dữ liệu đó sẽ được đọc / ghi trong một lần và không thể bị gián đoạn hoặc tranh cãi do ví dụ như chuyển đổi ngữ cảnh

2) Tính nhất quán tức là thứ tự các op đọc / ghi phải được xem là giống nhau giữa nhiều môi trường đồng thời - có thể là các luồng, máy, v.v.

dễ bay hơi không phù hợp với cả hai điều trên - hoặc đặc biệt hơn, tiêu chuẩn c hoặc c ++ về cách thức biến động nên bao gồm cả những điều trên.

Thực tế thậm chí còn tệ hơn khi một số trình biên dịch (như trình biên dịch Itanium intel) đã cố gắng thực hiện một số yếu tố của hành vi an toàn truy cập đồng thời (nghĩa là bằng cách đảm bảo hàng rào bộ nhớ) tuy nhiên không có sự thống nhất giữa các triển khai trình biên dịch và hơn nữa, tiêu chuẩn không yêu cầu điều này của việc thực hiện ở nơi đầu tiên.

Đánh dấu một biến là dễ bay hơi sẽ chỉ có nghĩa là bạn đang buộc giá trị được tuôn ra và từ bộ nhớ mỗi lần, trong nhiều trường hợp chỉ làm chậm mã của bạn vì về cơ bản bạn đã làm giảm hiệu suất bộ đệm của bạn.

c # và java AFAIK thực hiện khắc phục điều này bằng cách tuân thủ dễ bay hơi theo 1) và 2) tuy nhiên điều tương tự không thể nói đối với trình biên dịch c / c ++ vì vậy về cơ bản hãy làm với nó khi bạn thấy phù hợp.

Đối với một số thảo luận sâu hơn (mặc dù không thiên vị) về chủ đề này, hãy đọc


3
+1 - tính nguyên tử được đảm bảo là một phần khác của những gì tôi đã thiếu. Tôi đã giả định rằng việc tải một int là nguyên tử, do đó việc không ổn định ngăn chặn việc đặt hàng lại đã cung cấp giải pháp đầy đủ ở phía đọc. Tôi nghĩ rằng đó là một giả định đúng đắn trên hầu hết các kiến ​​trúc, nhưng nó không phải là một sự đảm bảo.
Michael Ekstrand

Khi nào cá nhân đọc và ghi vào bộ nhớ bị gián đoạn và không nguyên tử? Có lợi ích gì không?
batbrat

5

Câu hỏi thường gặp comp.programming.threads có lời giải thích kinh điển của Dave Butenhof:

Câu hỏi 56: Tại sao tôi không cần phải khai báo các biến được chia sẻ TÌNH TRẠNG?

Tuy nhiên, tôi lo ngại về các trường hợp cả trình biên dịch và thư viện luồng hoàn thành các thông số kỹ thuật tương ứng của chúng. Một trình biên dịch C tuân thủ có thể phân bổ toàn cầu một số biến được chia sẻ (không biến đổi) cho một thanh ghi được lưu và khôi phục khi CPU được truyền từ luồng này sang luồng khác. Mỗi luồng sẽ có giá trị riêng của nó cho biến được chia sẻ này, đây không phải là giá trị chúng ta muốn từ một biến được chia sẻ.

Theo một nghĩa nào đó thì điều này là đúng, nếu trình biên dịch biết đủ về phạm vi tương ứng của biến và các hàm pthread_cond_wait (hoặc pthread_mutex_lock). Trong thực tế, hầu hết các trình biên dịch sẽ không cố gắng giữ các bản sao đăng ký dữ liệu toàn cầu qua một cuộc gọi đến một chức năng bên ngoài, bởi vì quá khó để biết liệu thói quen có thể có quyền truy cập vào địa chỉ của dữ liệu hay không.

Vì vậy, đúng, một trình biên dịch tuân thủ nghiêm ngặt (nhưng rất tích cực) với ANSI C có thể không hoạt động với nhiều luồng mà không có biến động. Nhưng ai đó đã sửa nó tốt hơn. Bởi vì bất kỳ HỆ THỐNG nào (thực tế là sự kết hợp giữa kernel, thư viện và trình biên dịch C) không cung cấp đảm bảo tính liên kết bộ nhớ POSIX không CONFORM theo tiêu chuẩn POSIX. Giai đoạn = Stage. Hệ thống CANNOT yêu cầu bạn sử dụng biến động trên các biến được chia sẻ cho hành vi chính xác, bởi vì POSIX chỉ yêu cầu các chức năng đồng bộ hóa POSIX là cần thiết.

Vì vậy, nếu chương trình của bạn bị hỏng vì bạn không sử dụng dễ bay hơi, đó là BUG. Nó có thể không phải là một lỗi trong C, hoặc một lỗi trong thư viện chủ đề, hoặc một lỗi trong kernel. Nhưng đó là lỗi HỆ THỐNG và một hoặc nhiều thành phần trong số đó sẽ phải hoạt động để khắc phục.

Bạn không muốn sử dụng dễ bay hơi, bởi vì, trên bất kỳ hệ thống nào có sự khác biệt, nó sẽ đắt hơn rất nhiều so với một biến không biến đổi thích hợp. . thực sự làm bạn chậm lại.)

/ --- [Dave Butenhof] ----------------------- [butenhof@zko.dec.com] --- \
| Tổng công ty Thiết bị kỹ thuật số 110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [Sống tốt hơn thông qua đồng thời] ---------------- /

Ông Butenhof bao quát phần lớn nền tảng trong bài đăng này :

Việc sử dụng "dễ bay hơi" là không đủ để đảm bảo khả năng hiển thị hoặc đồng bộ hóa bộ nhớ phù hợp giữa các luồng. Việc sử dụng một mutex là đủ, và, ngoại trừ bằng cách sử dụng các giải pháp thay thế mã máy không di động khác nhau, (hoặc hàm ý tinh tế hơn của các quy tắc bộ nhớ POSIX thường khó áp dụng hơn, như đã giải thích trong bài viết trước của tôi), a mutex là CẦN THIẾT.

Do đó, như Bryan giải thích, việc sử dụng các biến động hoàn thành không có gì ngoài việc ngăn chặn trình biên dịch thực hiện các tối ưu hóa hữu ích và mong muốn, không cung cấp bất kỳ trợ giúp nào trong việc làm cho mã "luồng an toàn". Tất nhiên, bạn được hoan nghênh tuyên bố bất cứ điều gì bạn muốn là "dễ bay hơi" - rốt cuộc đó là thuộc tính lưu trữ ANSI C hợp pháp. Chỉ không mong đợi nó giải quyết bất kỳ vấn đề đồng bộ hóa chủ đề cho bạn.

Tất cả đều áp dụng như nhau cho C ++.


Liên kết bị hỏng; nó dường như không còn chỉ ra những gì bạn muốn trích dẫn. Không có văn bản, đó là một câu trả lời vô nghĩa.
jww

3

Đây là tất cả những gì "dễ bay hơi" đang làm: "Này trình biên dịch, biến này có thể thay đổi TẠI MỌI MOMENT (trên bất kỳ đồng hồ nào) ngay cả khi KHÔNG CÓ HƯỚNG DẪN ĐỊA PHƯƠNG nào hoạt động trên nó. KHÔNG lưu trữ giá trị này trong một thanh ghi."

Đó là CNTT. Nó báo cho trình biên dịch rằng giá trị của bạn là, tốt, không ổn định - giá trị này có thể bị thay đổi bất cứ lúc nào bởi logic bên ngoài (luồng khác, quá trình khác, Kernel, v.v.). Nó tồn tại ít nhiều chỉ để triệt tiêu tối ưu hóa trình biên dịch sẽ âm thầm lưu trữ một giá trị trong một thanh ghi mà nó vốn không an toàn đối với bộ đệm EVER.

Bạn có thể gặp các bài báo như "Tiến sĩ Dobbs" có độ biến động như một số thuốc chữa bách bệnh cho lập trình đa luồng. Cách tiếp cận của ông không hoàn toàn không có công, nhưng nó có lỗ hổng cơ bản là khiến người dùng của đối tượng chịu trách nhiệm về an toàn luồng của nó, có xu hướng có các vấn đề tương tự như các vi phạm đóng gói khác.


3

Theo tiêu chuẩn C cũ của tôi, thì Cái gì tạo thành quyền truy cập vào một đối tượng có loại đủ điều kiện dễ bay hơi được xác định là triển khai . Vì vậy, người viết trình biên dịch C có thể đã chọn "truy cập an toàn luồng" có nghĩa là "truy cập an toàn luồng trong môi trường nhiều quy trình" . Nhưng họ đã không làm thế.

Thay vào đó, các hoạt động cần thiết để làm cho một luồng phần quan trọng an toàn trong môi trường bộ nhớ chia sẻ đa tiến trình đa lõi đã được thêm vào dưới dạng các tính năng xác định thực hiện mới. Và, được giải phóng khỏi yêu cầu "dễ bay hơi" sẽ cung cấp quyền truy cập và thứ tự truy cập nguyên tử trong môi trường đa quy trình, các nhà văn biên dịch ưu tiên giảm mã so với ngữ nghĩa "dễ bay hơi" phụ thuộc vào triển khai lịch sử.

Điều này có nghĩa là những thứ như semaphores "dễ bay hơi" xung quanh các phần mã quan trọng, không hoạt động trên phần cứng mới với trình biên dịch mới, có thể đã từng làm việc với các trình biên dịch cũ trên phần cứng cũ và các ví dụ cũ đôi khi không sai, chỉ là cũ.


Các ví dụ cũ yêu cầu chương trình được xử lý bởi các trình biên dịch chất lượng phù hợp với lập trình cấp thấp. Thật không may, các trình biên dịch "hiện đại" đã lấy thực tế là Tiêu chuẩn không yêu cầu họ xử lý "dễ bay hơi" theo cách hữu ích như một dấu hiệu cho thấy mã sẽ yêu cầu họ làm như vậy bị hỏng, thay vì nhận ra rằng Tiêu chuẩn không Nỗ lực cấm các triển khai tuân thủ nhưng chất lượng thấp đến mức vô dụng, nhưng không bằng bất kỳ cách nào bỏ qua các trình biên dịch chất lượng thấp nhưng tuân thủ đã trở nên phổ biến
supercat

Trên hầu hết các nền tảng, sẽ khá dễ dàng để nhận ra những gì volatilecần làm để cho phép một người viết một HĐH theo cách phụ thuộc vào phần cứng nhưng không phụ thuộc vào trình biên dịch. Yêu cầu các lập trình viên sử dụng các tính năng phụ thuộc vào triển khai thay vì thực hiện volatilecông việc theo yêu cầu làm suy yếu mục đích của việc có một tiêu chuẩn.
supercat
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.