Sự khác biệt giữa mutex và phần quan trọng là gì?


134

Hãy giải thích từ Linux, quan điểm của Windows?

Tôi đang lập trình trong C #, liệu hai thuật ngữ này có làm nên sự khác biệt. Xin vui lòng gửi càng nhiều càng tốt, với các ví dụ và như vậy ....

Cảm ơn

Câu trả lời:


232

Đối với Windows, các phần quan trọng có trọng lượng nhẹ hơn so với mutexes.

Mutexes có thể được chia sẻ giữa các tiến trình, nhưng luôn dẫn đến một cuộc gọi hệ thống đến kernel có một số chi phí.

Các phần quan trọng chỉ có thể được sử dụng trong một quy trình, nhưng có lợi thế là chúng chỉ chuyển sang chế độ kernel trong trường hợp tranh chấp - Mua lại không được kiểm soát, nên là trường hợp phổ biến, cực kỳ nhanh. Trong trường hợp tranh chấp, họ nhập kernel để chờ một số nguyên thủy đồng bộ hóa (như một sự kiện hoặc semaphore).

Tôi đã viết một ứng dụng mẫu nhanh so sánh thời gian giữa hai người họ. Trên hệ thống của tôi cho 1.000.000 lượt mua lại và phát hành không được giám sát, một mutex mất hơn một giây. Một phần quan trọng mất ~ 50 ms cho 1.000.000 lượt mua.

Đây là mã kiểm tra, tôi đã chạy mã này và nhận được kết quả tương tự nếu mutex là thứ nhất hoặc thứ hai, vì vậy chúng tôi không thấy bất kỳ hiệu ứng nào khác.

HANDLE mutex = CreateMutex(NULL, FALSE, NULL);
CRITICAL_SECTION critSec;
InitializeCriticalSection(&critSec);

LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
LARGE_INTEGER start, end;

// Force code into memory, so we don't see any effects of paging.
EnterCriticalSection(&critSec);
LeaveCriticalSection(&critSec);
QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    EnterCriticalSection(&critSec);
    LeaveCriticalSection(&critSec);
}

QueryPerformanceCounter(&end);

int totalTimeCS = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
WaitForSingleObject(mutex, INFINITE);
ReleaseMutex(mutex);

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    WaitForSingleObject(mutex, INFINITE);
    ReleaseMutex(mutex);
}

QueryPerformanceCounter(&end);

int totalTime = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

printf("Mutex: %d CritSec: %d\n", totalTime, totalTimeCS);

1
Không chắc điều này có liên quan hay không (vì tôi chưa biên dịch và thử mã của bạn), nhưng tôi đã thấy rằng việc gọi WaitForSingleObject bằng INFINITE dẫn đến hiệu suất kém. Vượt qua nó một giá trị hết thời gian là 1 rồi lặp lại trong khi kiểm tra sự trở lại của nó đã tạo ra một sự khác biệt lớn trong hiệu suất của một số mã của tôi. Điều này chủ yếu là trong bối cảnh chờ đợi một xử lý quá trình bên ngoài, tuy nhiên ... Không phải là một mutex. YMMV. Tôi muốn biết làm thế nào mutex thực hiện với sửa đổi đó. Sự khác biệt thời gian kết quả từ thử nghiệm này có vẻ lớn hơn so với dự kiến.
Troy Howard

5
@TroyHoward về cơ bản bạn không chỉ quay khóa vào thời điểm đó?
dss539

Những lý do cho sự khác biệt này là chủ yếu là lịch sử. Không khó để thực hiện khóa nhanh như CriticalSection trong trường hợp không được bảo vệ (một vài hướng dẫn nguyên tử, không có tòa nhà), nhưng vẫn hoạt động trên các quy trình (với một phần bộ nhớ dùng chung). Xem ví dụ Linux Futexes .
đăng lại

2
@TroyHoward thử buộc CPU của bạn chạy 100% mọi lúc và xem INFINITE có hoạt động tốt hơn không. Chiến lược năng lượng có thể mất tới 40ms trên máy của tôi (Dell XPS-8700) để thu thập dữ liệu trở lại tốc độ tối đa sau khi nó quyết định giảm tốc độ, điều này có thể không xảy ra nếu bạn ngủ hoặc chỉ chờ một phần nghìn giây.
Stevens Miller

Tôi không chắc tôi hiểu những gì đang được chứng minh ở đây. Nói chung, việc vào một phần quan trọng đòi hỏi phải đạt được một số loại semaphore. Bạn có nói rằng đằng sau hậu trường, O / S có một cách hiệu quả để thực hiện hành vi phần quan trọng này mà không yêu cầu các trường hợp đột biến?
SN

89

Từ góc độ lý thuyết, một phần quan trọng là một đoạn mã không được chạy bởi nhiều luồng cùng một lúc vì mã truy cập các tài nguyên được chia sẻ.

Một mutex là một thuật toán (và đôi khi là tên của cấu trúc dữ liệu) được sử dụng để bảo vệ các phần quan trọng.

SemaphoresMàn hình là những triển khai phổ biến của một mutex.

Trong thực tế có nhiều triển khai mutex có sẵn trong các cửa sổ. Chúng chủ yếu khác nhau do hậu quả của việc thực hiện bởi mức độ khóa, phạm vi, chi phí và hiệu suất của chúng dưới các mức độ tranh chấp khác nhau. Xem CLR Inside Out - Sử dụng đồng thời cho khả năng mở rộng cho biểu đồ chi phí của các triển khai mutex khác nhau.

Nguyên thủy đồng bộ hóa có sẵn.

Câu lock(object)lệnh được triển khai bằng cách sử dụng Monitor- xem MSDN để tham khảo.

Trong những năm qua, nhiều nghiên cứu được thực hiện về đồng bộ hóa không chặn . Mục tiêu là để thực hiện các thuật toán theo cách không khóa hoặc chờ miễn phí. Trong các thuật toán như vậy, một quy trình giúp các quy trình khác hoàn thành công việc của họ để quy trình cuối cùng có thể hoàn thành công việc của mình. Do đó, một quy trình có thể hoàn thành công việc của nó ngay cả khi các quy trình khác, đã cố gắng thực hiện một số công việc, treo. Usinig khóa, họ sẽ không phát hành khóa của họ và ngăn chặn các quá trình khác tiếp tục.


Nhìn thấy câu trả lời được chấp nhận, tôi đã nghĩ có lẽ tôi đã nhớ khái niệm các phần quan trọng sai, cho đến khi tôi thấy rằng Quan điểm lý thuyết mà bạn đã viết. :)
Anirudh Ramanathan

2
Thực hành lập trình khóa miễn phí giống như Shangri La, ngoại trừ nó tồn tại. Bài viết của Keir Fraser (PDF) khám phá điều này khá thú vị (quay trở lại năm 2004). Và chúng tôi vẫn đang vật lộn với nó vào năm 2012. Chúng tôi hút.
Tim Post

22

Ngoài các câu trả lời khác, các chi tiết sau đây dành riêng cho các phần quan trọng trên windows:

  • trong trường hợp không có sự tranh chấp, có được một phần quan trọng cũng đơn giản như một InterlockedCompareExchangehoạt động
  • cấu trúc phần quan trọng giữ chỗ cho một mutex. Nó ban đầu chưa được phân bổ
  • nếu có sự tranh chấp giữa các luồng cho một phần quan trọng, mutex sẽ được phân bổ và sử dụng. Hiệu suất của phần quan trọng sẽ giảm xuống so với phần của mutex
  • nếu bạn dự đoán sự tranh chấp cao, bạn có thể phân bổ phần quan trọng chỉ định số vòng quay.
  • nếu có sự tranh chấp trên một phần quan trọng với số vòng quay, thì luồng cố gắng để có được phần quan trọng sẽ quay (bận-chờ) cho nhiều chu kỳ bộ xử lý. Điều này có thể dẫn đến hiệu suất tốt hơn so với ngủ, vì số chu kỳ để thực hiện chuyển đổi ngữ cảnh sang luồng khác có thể cao hơn nhiều so với số chu kỳ được thực hiện bởi luồng sở hữu để giải phóng mutex
  • nếu số vòng quay hết hạn, mutex sẽ được phân bổ
  • Khi luồng sở hữu giải phóng phần quan trọng, cần phải kiểm tra xem mutex có được phân bổ không, nếu có thì nó sẽ thiết lập mutex để giải phóng một chuỗi chờ

Trong linux, tôi nghĩ rằng họ có một "khóa spin" phục vụ mục đích tương tự như phần quan trọng với số lần quay.


Thật không may, phần quan trọng của Window liên quan đến việc thực hiện một thao tác CAS ở chế độ kernel , đắt hơn nhiều so với thao tác lồng vào nhau thực tế. Ngoài ra, các phần quan trọng của Windows có thể có số vòng quay liên quan đến chúng.
Hứa hẹn

2
Điều đó chắc chắn không đúng. CAS có thể được thực hiện với cmpxchg trong chế độ người dùng.
Michael

Tôi nghĩ rằng số vòng quay mặc định là 0 nếu bạn gọi là LaunchizeCriticalSection - bạn phải gọi InitializeCriticalSectionAndSpinCount nếu bạn muốn áp dụng số vòng quay. Bạn có một tài liệu tham khảo cho điều đó?
1800 THÔNG TIN

18

Phần quan trọng và Mutex không phải là hệ điều hành cụ thể, các khái niệm của chúng về đa luồng / đa xử lý.

Phần quan trọng Là một đoạn mã chỉ phải tự chạy ở bất kỳ thời điểm nào (ví dụ: có 5 luồng chạy đồng thời và một hàm gọi là "then_section_feft" cập nhật một mảng ... bạn không muốn có tất cả 5 luồng cập nhật mảng cùng một lúc. Vì vậy, khi chương trình đang chạy tới_s_s_s_bức (), không có luồng nào trong số các luồng khác phải chạy tới tệp_s_s_số_fection của chúng.

mutex * Mutex là một cách để triển khai mã phần quan trọng (nghĩ về nó giống như một mã thông báo ... chủ đề phải có quyền sở hữu nó để chạy tới phần phê bình)


2
Ngoài ra, mutexes có thể được chia sẻ qua các quá trình.
cấu hình

14

Một mutex là một đối tượng mà một luồng có thể có được, ngăn chặn các luồng khác có được nó. Đó là tư vấn, không bắt buộc; một luồng có thể sử dụng tài nguyên mà mutex đại diện mà không cần lấy nó.

Phần quan trọng là độ dài mã được đảm bảo bởi hệ điều hành để không bị can thiệp. Trong mã giả, nó sẽ giống như:

StartCriticalSection();
    DoSomethingImportant();
    DoSomeOtherImportantThing();
EndCriticalSection();

1
Tôi nghĩ rằng người đăng đã nói về các nguyên thủy đồng bộ hóa chế độ người dùng, như một đối tượng phần quan trọng win32, chỉ cung cấp loại trừ lẫn nhau. Tôi không biết về Linux, nhưng nhân Windows có các khu vực quan trọng hoạt động như bạn mô tả - không bị gián đoạn.
Michael

1
Tôi không biết lý do tại sao bạn bị hạ cấp. Có khái niệm về một phần quan trọng, mà bạn đã mô tả chính xác, khác với đối tượng nhân Windows được gọi là CriticalSection, là một loại mutex. Tôi tin rằng OP đã hỏi về định nghĩa sau.
Adam Rosenfield

Ít nhất tôi đã bị nhầm lẫn bởi thẻ bất khả tri ngôn ngữ. Nhưng trong mọi trường hợp, đây là những gì chúng ta nhận được khi Microsoft đặt tên cho việc triển khai của họ giống như lớp cơ sở của họ. Thực hành mã hóa xấu!
Mikko Rantanen

Chà, anh ấy đã hỏi càng nhiều chi tiết càng tốt, và đặc biệt nói Windows và Linux để âm thanh như các khái niệm là tốt. +1 - cũng không hiểu -1: /
Jason Coco

14

Windows 'nhanh' bằng với lựa chọn quan trọng trong Linux sẽ là một Futex , viết tắt của mutex không gian người dùng nhanh. Sự khác biệt giữa Futex và mutex là với Futex, kernel chỉ tham gia khi cần phân xử trọng tài, vì vậy bạn tiết kiệm chi phí nói chuyện với kernel mỗi khi bộ đếm nguyên tử được sửa đổi. Điều đó .. có thể tiết kiệm một lượng đáng kể thời gian đàm phán khóa trong một số ứng dụng.

Một Futex cũng có thể được chia sẻ giữa các quy trình, sử dụng các phương tiện bạn sẽ sử dụng để chia sẻ một mutex.

Thật không may, Futexes có thể rất khó thực hiện (PDF). (Cập nhật năm 2018, chúng không đáng sợ như năm 2009).

Ngoài ra, nó khá giống nhau trên cả hai nền tảng. Bạn đang tạo các bản cập nhật nguyên tử, mã thông báo cho một cấu trúc được chia sẻ theo cách mà (hy vọng) không gây ra chết đói. Những gì còn lại chỉ đơn giản là phương pháp để thực hiện điều đó.


6

Trong Windows, một phần quan trọng là cục bộ trong quy trình của bạn. Một mutex có thể được chia sẻ / truy cập trên các quy trình. Về cơ bản, các phần quan trọng là rẻ hơn nhiều. Không thể nhận xét cụ thể về Linux, nhưng trên một số hệ thống, chúng chỉ là bí danh cho cùng một thứ.


6

Chỉ cần thêm 2 xu của tôi, các Phần quan trọng được xác định là cấu trúc và các thao tác trên chúng được thực hiện trong ngữ cảnh chế độ người dùng.

ntdll! _RTL_CRITICS_SECTION
   + 0x000 DebugInfo: Ptr32 _RTL_CRITICAL_SECTION_DEBUG
   + Khóa 0x004: Int4B
   + Đệ quy 0x008: Int4B
   + 0x00c Sở hữuThread: Ptr32 Void
   + 0x010 LockSemaphore: Vô hiệu Ptr32
   + Số lần quay 0x014: Uint4B

Trong đó mutex là các đối tượng kernel (ExMutantObjectType) được tạo trong thư mục đối tượng Windows. Hoạt động Mutex chủ yếu được thực hiện trong chế độ kernel. Chẳng hạn, khi tạo Mutex, cuối cùng bạn gọi nt! NtCreateMutant trong kernel.


Điều gì xảy ra khi một chương trình khởi tạo và sử dụng một đối tượng Mutex, gặp sự cố? Đối tượng Mutex có được tự động giải quyết không? Không, tôi sẽ nói. Đúng?
Ankur

6
Các đối tượng hạt nhân có số tham chiếu. Đóng một tay cầm cho một đối tượng làm giảm số tham chiếu và khi nó đạt đến 0 thì đối tượng được giải phóng. Khi một quá trình gặp sự cố, tất cả các tay cầm của nó sẽ tự động đóng lại, do đó, một mutex mà chỉ quá trình đó có một tay cầm sẽ tự động được xử lý.
Michael

Và đây là lý do tại sao các đối tượng Phần quan trọng bị ràng buộc quá trình, mặt khác, các đột biến có thể được chia sẻ qua các quy trình.
Sisir

2

Câu trả lời tuyệt vời từ Michael. Tôi đã thêm một thử nghiệm thứ ba cho lớp mutex được giới thiệu trong C ++ 11. Kết quả là hơi thú vị và vẫn hỗ trợ chứng thực ban đầu của anh ấy về các đối tượng CRITICAL_SECTION cho các quy trình đơn lẻ.

mutex m;
HANDLE mutex = CreateMutex(NULL, FALSE, NULL);
CRITICAL_SECTION critSec;
InitializeCriticalSection(&critSec);

LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
LARGE_INTEGER start, end;

// Force code into memory, so we don't see any effects of paging.
EnterCriticalSection(&critSec);
LeaveCriticalSection(&critSec);
QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    EnterCriticalSection(&critSec);
    LeaveCriticalSection(&critSec);
}

QueryPerformanceCounter(&end);

int totalTimeCS = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
WaitForSingleObject(mutex, INFINITE);
ReleaseMutex(mutex);

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    WaitForSingleObject(mutex, INFINITE);
    ReleaseMutex(mutex);
}

QueryPerformanceCounter(&end);

int totalTime = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);

// Force code into memory, so we don't see any effects of paging.
m.lock();
m.unlock();

QueryPerformanceCounter(&start);
for (int i = 0; i < 1000000; i++)
{
    m.lock();
    m.unlock();
}

QueryPerformanceCounter(&end);

int totalTimeM = (int)((end.QuadPart - start.QuadPart) * 1000 / freq.QuadPart);


printf("C++ Mutex: %d Mutex: %d CritSec: %d\n", totalTimeM, totalTime, totalTimeCS);

Kết quả của tôi là 217, 473 và 19 (lưu ý rằng tỷ lệ số lần của tôi trong hai lần gần nhất tương đương với Michael, nhưng máy của tôi trẻ hơn anh ấy ít nhất bốn tuổi, vì vậy bạn có thể thấy bằng chứng về tốc độ tăng từ năm 2009 đến 2013 , khi XPS-8700 ra đời). Lớp mutex mới nhanh gấp đôi so với mutex Windows, nhưng vẫn chưa bằng một phần mười tốc độ của đối tượng Windows CRITICAL_SECTION. Lưu ý rằng tôi chỉ thử nghiệm mutex không đệ quy. Các đối tượng CRITICS_SECTION được đệ quy (một luồng có thể nhập chúng liên tục, miễn là nó để lại cùng một số lần).


0

Các hàm AC được gọi là reentrant nếu nó chỉ sử dụng các tham số thực tế của nó.

Các hàm reentrant có thể được gọi bởi nhiều luồng cùng một lúc.

Ví dụ về hàm reentrant:

int reentrant_function (int a, int b)
{
   int c;

   c = a + b;

   return c;
}

Ví dụ về hàm không reentrant:

int result;

void non_reentrant_function (int a, int b)
{
   int c;

   c = a + b;

   result = c;

}

Thư viện tiêu chuẩn C strtok () không được phát hành lại và không thể được sử dụng bởi 2 hoặc nhiều luồng cùng một lúc.

Một số SDK nền tảng đi kèm với phiên bản reentrant của strtok () được gọi là strtok_r ();

Enrico Migliore

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.