2010-04-27 9 views
11

Tôi muốn giảm thiểu đồng bộ hóa và viết mã không có khóa khi có thể trong dự án của tôi. Khi hoàn toàn cần thiết tôi muốn thay thế spinlocks trọng lượng nhẹ được xây dựng từ các hoạt động nguyên tử cho pthread và win32 mutex khóa. Sự hiểu biết của tôi là đây là các cuộc gọi hệ thống bên dưới và có thể gây ra một chuyển đổi ngữ cảnh (có thể không cần thiết cho các phần rất quan trọng mà chỉ cần quay vài lần là thích hợp hơn).Spinlocks trọng lượng nhẹ được chế tạo từ các hoạt động nguyên tử GCC?

Các hoạt động nguyên tử tôi đề cập đến cũng là tài liệu ở đây: http://gcc.gnu.org/onlinedocs/gcc-4.4.1/gcc/Atomic-Builtins.html

Dưới đây là một ví dụ để minh họa cho những gì tôi đang nói về. Hãy tưởng tượng một cây RB với nhiều độc giả và nhà văn có thể. RBTree :: exist() là chỉ đọc và thread an toàn, RBTree :: insert() sẽ yêu cầu truy cập độc quyền bởi một nhà văn duy nhất (và không có độc giả) để được an toàn. Một số mã:

class IntSetTest 
{ 
private: 
    unsigned short lock; 
    RBTree<int>* myset; 

public: 
    // ... 

    void add_number(int n) 
    { 
     // Aquire once locked==false (atomic) 
     while (__sync_bool_compare_and_swap(&lock, 0, 0xffff) == false); 

     // Perform a thread-unsafe operation on the set 
     myset->insert(n); 

     // Unlock (atomic) 
     __sync_bool_compare_and_swap(&lock, 0xffff, 0); 
    } 

    bool check_number(int n) 
    { 
     // Increment once the lock is below 0xffff 
     u16 savedlock = lock; 
     while (savedlock == 0xffff || __sync_bool_compare_and_swap(&lock, savedlock, savedlock+1) == false) 
      savedlock = lock; 

     // Perform read-only operation  
     bool exists = tree->exists(n); 

     // Decrement 
     savedlock = lock; 
     while (__sync_bool_compare_and_swap(&lock, savedlock, savedlock-1) == false) 
      savedlock = lock; 

     return exists; 
    } 
}; 

(cho phép giả định nó không cần phải ngoại lệ an toàn)

Là mã này thực sự thread-an toàn không? Có bất kỳ ưu/khuyết điểm nào đối với ý tưởng này không? Lời khuyên nào? Việc sử dụng spinlocks như thế này là một ý tưởng tồi nếu các chủ đề không thực sự đồng thời?

Xin cảm ơn trước. ;)

+3

Câu trả lời tôi đưa ra trong một câu hỏi tương tự, http://stackoverflow.com/questions/1919135/critical-sections-that-spin-on-posix/1923218#1923218, có thể sẽ có liên quan ở đây. –

+0

Câu trả lời của bạn chắc chắn liên quan đến vấn đề sử dụng khóa spinlocks nói chung. Họ có vẻ như một ý tưởng tốt cho các máy smp trong trường hợp điển hình. Tình huống xấu nhất (một nhà văn ngừng chạy trong phần quan trọng) thậm chí với trường hợp có nhiều khả năng của hai luồng đồng thời cố gắng chèn cùng một lúc? Điều gì về trong một môi trường luồng lai mà các luồng người dùng được ánh xạ lên một số luồng hạt nhân bằng với số lượng bộ xử lý logic trên máy? Tình huống xấu nhất sẽ ít xảy ra hơn; Không? – Thomas

+0

Tôi không chắc chắn mức độ mà số lượng các chuỗi hạt nhân ảnh hưởng đến khả năng chạy vào các vấn đề hiệu suất. Có thể là người viết luồng đã sử dụng hết phần thời gian của nó giữa mục nhập và lối ra của khóa, điều này sẽ dẫn đến trường hợp vấn đề cho dù có bao nhiêu luồng hạt nhân. Vào thời điểm này, tôi sẽ lưu ý rằng hoạt động chèn RB-tree là O (log (n)), do đó cây càng lớn thì vấn đề này càng xảy ra. Ngoài ra, một cây lớn hơn có nhiều khả năng gây ra lỗi trang trong khi cập nhật, điều này cũng sẽ khiến cho trường hợp sự cố xảy ra nhiều hơn. Tôi muốn tránh spinlocks ở đây. –

Trả lời

4

Bạn cần một mã định danh volatile trên lock và tôi cũng sẽ làm cho nó là sig_atomic_t. Nếu không có sự volatile vòng loại, mã này:

u16 savedlock = lock; 
    while (savedlock == 0xffff || __sync_bool_compare_and_swap(&lock, savedlock, savedlock+1) == false) 
     savedlock = lock; 

có thể không đọc lại lock khi cập nhật savedlock trong cơ thể của vòng lặp while. Hãy xem xét trường hợp lock là 0xffff. Sau đó, savedlock sẽ là 0xffff trước khi kiểm tra điều kiện vòng lặp, do đó, điều kiện while sẽ ngắn mạch trước khi gọi __sync_bool_compare_and_swap. Vì __sync_bool_compare_and_swap không được gọi, trình biên dịch không gặp phải rào cản bộ nhớ, vì vậy có thể giả định hợp lý rằng giá trị của lock không thay đổi bên dưới bạn và tránh tải lại nó trong savedlock.

Re: sig_atomic_t, có thảo luận chi tiết here. Các cân nhắc tương tự áp dụng cho các trình xử lý tín hiệu cũng sẽ áp dụng cho các luồng.

Với những thay đổi này, tôi đoán rằng mã của bạn sẽ an toàn chỉ. Tôi vẫn sẽ khuyên bạn nên sử dụng mutexes, mặc dù, vì bạn thực sự không biết bao lâu RB-cây chèn của bạn sẽ mất trong trường hợp chung (mỗi bình luận trước đây của tôi theo câu hỏi).

+0

Điều này thật thú vị. Tôi đã đọc nhiều bài báo giải thích lý do tại sao dễ bay hơi là người bạn tốt nhất của chương trình đa luồng, và nhiều người giải thích tại sao dễ bay hơi không liên quan gì đến điều này và làm mọi thứ dễ bay hơi sẽ làm chậm chương trình. Trong ứng dụng của tôi, hơn một nửa dữ liệu có thể được truy cập bởi bất kỳ chủ đề nào và bất kỳ lúc nào. Chúng có thực sự dễ bay hơi không? Hoặc đây có phải là ngoại lệ bởi vì nó trong một vòng lặp chặt chẽ mà trình biên dịch có thể tối ưu hóa để chỉ kiểm tra khóa một lần? – Thomas

+0

Ví dụ: Hình ảnh một chức năng (không được gạch chân) được gọi, kiểm tra biến, sau đó trả về và được gọi lại nhanh chóng. Trong trường hợp này, sẽ dễ bay hơi không cần thiết vì trình biên dịch sẽ không thể tối ưu hóa mã trên nhiều cuộc gọi? Nhưng trong vòng lặp ở trên nó có thể nhận ra rằng khóa không bao giờ có thể thay đổi và tối ưu hóa nó ra? Vì vậy, dễ bay hơi không có gì để làm với bộ nhớ đệm, nó chỉ đơn giản là nói với trình biên dịch không để tối ưu hóa quyền truy cập vào bộ nhớ? Tôi nghĩ điều này có ý nghĩa với tôi. Vui lòng xác nhận hoặc làm rõ! :) – Thomas

+1

Tôi đã dành thời gian tìm kiếm các công trình dễ bay hơi như thế nào ...Tóm lại, những gì nó làm là để ngăn chặn tối ưu hóa truy cập bộ nhớ, và cũng ngăn chặn việc sắp xếp lại các hoạt động bộ nhớ liên quan đến các biến dễ bay hơi. (Các hoạt động bộ nhớ liên quan đến các biến không đủ điều kiện bay hơi có thể được sắp xếp lại xung quanh các biến liên quan đến biến động. Hơn nữa, ngay cả khi việc ghi xảy ra theo thứ tự, một CPU khác có thể nhận thấy các giá trị mới theo thứ tự khác.) đồng bộ hóa đã đọc _ trong trường hợp này_, bởi vì bạn cũng có các thói quen '__sync' cung cấp một rào cản bộ nhớ. –

1

Có thể đáng lưu ý rằng nếu bạn đang sử dụng các mutex của Win32, từ Vista trở đi, một nhóm luồng sẽ được cung cấp cho bạn. Tùy thuộc vào những gì bạn sử dụng cây RB cho, bạn có thể thay thế bằng điều đó.

Ngoài ra, điều bạn nên nhớ là các hoạt động nguyên tử không phải là đặc biệt nhanh. Microsoft cho biết họ là một vài trăm chu kỳ, mỗi chu kỳ. Thay vì cố gắng "bảo vệ" chức năng theo cách này, nó có thể sẽ hiệu quả hơn nhiều khi chỉ đơn giản là đồng bộ hóa các chủ đề, hoặc thay đổi theo cách tiếp cận hồ bơi SIMD/thread, hoặc chỉ sử dụng một mutex.

Nhưng, tất nhiên, không nhìn thấy mã của bạn, tôi thực sự không thể tạo thêm bất kỳ nhận xét nào. Vấn đề với đa luồng là bạn phải xem toàn bộ mô hình của ai đó để hiểu nó.

+0

Một điểm quan trọng khác là toàn bộ khía cạnh "nhẹ" này. Đây chỉ là một ví dụ, nhưng trong mã thực tế của tôi có thể trong một số trường hợp là hàng triệu các đối tượng này và tôi không nghĩ rằng nó sẽ là thực tế để tạo ra hàng triệu pthread hoặc win32 mutexes. Một int 16bit không dấu sẽ thực sự không gây ra bất kỳ chi phí bổ sung nào (do căn chỉnh). – Thomas

+0

Trên thực tế, nhóm chủ đề (http://msdn.microsoft.com/en-us/library/ms684957(VS.85).aspx) đã có sẵn từ Windows 2000. –

+0

Nó không thực tế để sử dụng hàng triệu hoạt động liên khóa hoặc. Tôi vẫn nghĩ rằng bạn cần phải thiết kế lại mô hình luồng của mình. Bạn dường như muốn thiết kế một lớp học có hiệu suất cao và hoàn toàn luồng không biết gì. @Billy Oneal - bạn nói đúng. Tôi đã không nhận thấy chức năng đó trước đây. – Puppy