list :: hành vi đa luồng rỗng ()?


9

Tôi có một danh sách mà tôi muốn các chủ đề khác nhau để lấy các yếu tố từ đó. Để tránh bị khóa mutex bảo vệ danh sách khi nó trống, tôi kiểm tra empty()trước khi khóa.

Không sao nếu cuộc gọi list::empty()không đúng 100%. Tôi chỉ muốn tránh sự cố hoặc làm gián đoạn đồng thời list::push()list::pop()các cuộc gọi.

Tôi có an toàn không khi cho rằng VC ++ và Gnu GCC đôi khi sẽ chỉ bị empty()sai và không có gì tệ hơn?

if(list.empty() == false){ // unprotected by mutex, okay if incorrect sometimes
    mutex.lock();
    if(list.empty() == false){ // check again while locked to be certain
         element = list.back();
         list.pop_back();
    }
    mutex.unlock();
}

1
Không, bạn không thể giả định điều này. Bạn có thể sử dụng một thùng chứa đồng thời như đồng thời
Panagiotis Kanavos

2
@Fureeish Đây nên là một câu trả lời. Tôi sẽ thêm rằng std::list::sizeđã đảm bảo độ phức tạp thời gian không đổi, về cơ bản có nghĩa là kích thước (số lượng nút) cần được lưu trữ trong một biến riêng biệt; chúng ta hãy gọi nó size_. std::list::emptysau đó có khả năng trả về một cái gì đó như size_ == 0, và việc đọc và ghi đồng thời size_sẽ gây ra cuộc đua dữ liệu, do đó, UB.
Daniel Langr

@DanielLangr "Thời gian không đổi" được đo như thế nào? Là trên một cuộc gọi chức năng duy nhất hoặc chương trình hoàn chỉnh?
tò mò

1
@cquilguy: DanielLangr đã trả lời câu hỏi của bạn bằng cách "độc lập với số lượng nút danh sách", đó là định nghĩa chính xác của O (1) có nghĩa là mọi cuộc gọi được thực hiện trong thời gian ngắn hơn một số thời gian không đổi, bất kể số lượng phần tử. vi.wikipedia.org/wiki/Big_O_notation#Orders_of_common_fifts Tùy chọn khác (cho đến khi C ++ 11) có nghĩa là tuyến tính = O (n), kích thước đó sẽ phải tính các yếu tố (danh sách được liên kết), thậm chí còn tệ hơn đối với sự tương tranh (cuộc đua dữ liệu rõ ràng hơn so với đọc / ghi không nguyên tử trên bộ đếm).
firda

1
@cquilguy: Lấy ví dụ của riêng bạn với dV, độ phức tạp thời gian là cùng một phép toán - giới hạn. Tất cả những điều này được định nghĩa đệ quy hoặc ở dạng "Tồn tại C sao cho f (N) <C với mọi N" - đó là định nghĩa của O (1) (đối với / mọi CTNH đều tồn tại hằng số C như vậy rằng thuật toán kết thúc ít hơn thời gian C trên bất kỳ đầu vào nào). Trung bình khấu hao có nghĩa là trung bình , có nghĩa là một số đầu vào có thể mất nhiều thời gian hơn để xử lý (ví dụ: tái băm / phân bổ lại cần thiết), nhưng trung bình vẫn không đổi (giả sử tất cả các đầu vào có thể).
firda

Câu trả lời:


10

Không sao nếu cuộc gọi list::empty()không đúng 100%.

Không, nó không ổn. Nếu bạn kiểm tra xem danh sách có trống bên ngoài một số cơ chế đồng bộ hóa (khóa mutex) không thì bạn có một cuộc đua dữ liệu. Có một cuộc đua dữ liệu có nghĩa là bạn có hành vi không xác định. Có hành vi không xác định có nghĩa là chúng tôi không còn có thể lý do về chương trình và bất kỳ đầu ra nào bạn nhận được là "chính xác".

Nếu bạn coi trọng sự tỉnh táo của mình, bạn sẽ thực hiện cú đánh hiệu suất và khóa mutex trước khi kiểm tra. Điều đó nói rằng, danh sách thậm chí có thể không phải là container chính xác cho bạn. Nếu bạn có thể cho chúng tôi biết chính xác những gì bạn đang làm với nó, chúng tôi có thể đề xuất một container tốt hơn.


Quan điểm cá nhân, gọi list::empty()là một hành động đọc không liên quan gìrace-condition
Ngọc Khánh Nguyễn

3
@ NgọcKhánhNguy giá Nếu họ đang thêm các yếu tố vào danh sách thì điều đó chắc chắn gây ra cuộc đua dữ liệu khi bạn đang viết và đọc kích thước cùng một lúc.
NathanOliver

6
@ NgọcKhánhNguy Điều đó là sai. Một điều kiện cuộc đua là read-writehoặc write-write. Nếu bạn không tin rằng tôi, cung cấp cho phần tiêu chuẩn trên chủng tộc dữ liệu đọc
NathanOliver

1
@ NgọcKhánhNguy giá: Bởi vì cả ghi và đọc đều không được đảm bảo là nguyên tử, do đó có thể được thực thi đồng thời, do đó việc đọc có thể bị lỗi hoàn toàn (gọi là đọc rách). Hãy tưởng tượng ghi thay đổi 0x00FF thành 0x0100 trong MCU 8 bit nhỏ về cuối, bắt đầu bằng cách viết lại 0xFF thấp thành 0x00 và giờ đây đọc chính xác bằng 0, đọc cả hai byte (viết luồng bị chậm hoặc bị treo), tiếp tục viết bằng cách cập nhật byte cao thành 0x01, nhưng luồng đọc đã có giá trị sai (không phải 0x00FF, cũng không phải 0x0100 mà là 0x0000 bất ngờ).
firda

1
@ NgọcKhánhNguy giá Có thể trên một số kiến ​​trúc nhưng máy ảo C ++ không đảm bảo như vậy. Ngay cả khi phần cứng của bạn đã làm, trình biên dịch sẽ tối ưu hóa mã theo cách mà bạn sẽ không bao giờ thấy thay đổi, trừ khi có đồng bộ hóa luồng, nó có thể cho rằng nó chỉ chạy một luồng duy nhất và tối ưu hóa theo đó.
NathanOliver

6

Có một phần đọc và viết (rất có thể là cho sizethành viên của std::list, nếu chúng ta cho rằng nó được đặt tên như vậy) không được đồng bộ hóa trong thuốc thử với nhau . Tưởng tượng rằng một luồng gọi empty()(ở bên ngoài của bạn if()) trong khi luồng khác nhập vào bên trong if()và thực thi pop_back(). Sau đó, bạn đang đọc một biến, có thể, đang được sửa đổi. Đây là hành vi không xác định.


2

Như một ví dụ về cách mọi thứ có thể đi sai:

Một trình biên dịch đủ thông minh có thể thấy rằng mutex.lock()không thể thay đổi list.empty()giá trị trả về và do đó bỏ qua ifhoàn toàn kiểm tra bên trong , cuối cùng dẫn đến pop_backmột danh sách có phần tử cuối cùng bị loại bỏ sau phần đầu tiên if.

Tại sao nó có thể làm điều đó? Không có sự đồng bộ hóa list.empty(), do đó, nếu nó được thay đổi đồng thời sẽ tạo thành một cuộc đua dữ liệu. Tiêu chuẩn nói rằng các chương trình sẽ không có các cuộc đua dữ liệu, vì vậy trình biên dịch sẽ coi đó là điều hiển nhiên (nếu không nó có thể thực hiện hầu như không tối ưu hóa gì). Do đó, nó có thể giả định một phối cảnh đơn luồng trên không đồng bộ list.empty()và kết luận rằng nó phải không đổi.

Đây chỉ là một trong một số tối ưu hóa (hoặc hành vi phần cứng) có thể phá vỡ mã của bạn.


Trình biên dịch hiện tại thậm chí dường như không muốn tối ưu hóa a.load()+a.load()...
tò mò

1
@cquilguy Làm thế nào muốn tối ưu hóa? Bạn yêu cầu sự thống nhất tuần tự đầy đủ ở đó, vì vậy bạn sẽ nhận được điều đó ...
Max Langhof

@MaxLanghof Bạn không nghĩ việc tối ưu hóa a.load()*2là hiển nhiên? Thậm chí không a.load(rel)+b.load(rel)-a.load(rel)được tối ưu hóa. Không có gì là. Tại sao bạn mong đợi các khóa (mà về bản chất hầu hết có tính nhất quán seq) sẽ được tối ưu hóa hơn?
tò mò

@cquilguy Vì thứ tự bộ nhớ của các truy cập phi nguyên tử (ở đây trước và sau khóa) và nguyên tử hoàn toàn khác nhau? Tôi không hy vọng khóa sẽ được tối ưu hóa "nhiều hơn", tôi hy vọng rằng các truy cập không đồng bộ sẽ được tối ưu hóa nhiều hơn các truy cập nhất quán liên tục. Sự hiện diện của khóa là không liên quan đến quan điểm của tôi. Và không có, trình biên dịch không được phép tối ưu hóa a.load() + a.load()để 2 * a.load(). Hãy hỏi một câu hỏi về điều này nếu bạn muốn biết thêm.
Max Langhof

@MaxLanghof Tôi không biết bạn đang cố nói gì. Khóa về cơ bản là nhất quán tuần tự. Tại sao việc thực hiện sẽ cố gắng thực hiện tối ưu hóa trên một số nguyên thủy luồng (khóa) mà không phải một số khác (nguyên tử)? Bạn có mong đợi các truy cập phi nguyên tử được tối ưu hóa xung quanh việc sử dụng nguyên tử không?
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.