อะไรคือความแตกต่างระหว่าง mutex และส่วนวิกฤต?


137

กรุณาอธิบายจาก Linux, Windows มุมมอง?

ฉันกำลังเขียนโปรแกรมในภาษา C # คำศัพท์ทั้งสองนี้จะสร้างความแตกต่างได้หรือไม่ กรุณาโพสต์ให้มากที่สุดพร้อมตัวอย่างและเช่น ....

ขอบคุณ

คำตอบ:


234

สำหรับ Windows ส่วนที่สำคัญจะมีน้ำหนักเบากว่า mutexes

Mutexes สามารถใช้ร่วมกันระหว่างกระบวนการได้ แต่จะส่งผลให้ระบบเรียกไปยังเคอร์เนลซึ่งมีค่าใช้จ่ายอยู่บ้าง

ส่วนที่สำคัญสามารถใช้ได้ภายในกระบวนการเดียวเท่านั้น แต่มีข้อได้เปรียบที่พวกเขาเปลี่ยนไปใช้โหมดเคอร์เนลในกรณีที่มีการโต้แย้งเท่านั้น - การได้มาโดยไม่ต้องมีการควบคุมซึ่งควรเป็นกรณีทั่วไปนั้นรวดเร็วอย่างไม่น่าเชื่อ ในกรณีของความขัดแย้งพวกเขาเข้าสู่เคอร์เนลเพื่อรอการซิงโครไนซ์แบบดั้งเดิม (เช่นเหตุการณ์หรือสัญญาณ)

ฉันเขียนแอปตัวอย่างสั้น ๆ ที่เปรียบเทียบเวลาระหว่างทั้งสองแอป ในระบบของฉันสำหรับการรับและเผยแพร่ที่ไม่มีการควบคุม 1,000,000 รายการ mutex จะใช้เวลามากกว่าหนึ่งวินาที ส่วนที่สำคัญใช้เวลาประมาณ 50 ms สำหรับการได้มา 1,000,000 ครั้ง

นี่คือรหัสทดสอบฉันรันสิ่งนี้และได้ผลลัพธ์ที่คล้ายกันหาก mutex เป็นครั้งแรกหรือครั้งที่สองดังนั้นเราจึงไม่เห็นเอฟเฟกต์อื่น ๆ

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
ไม่แน่ใจว่าเกี่ยวข้องหรือไม่ (เนื่องจากฉันยังไม่ได้คอมไพล์และลองใช้โค้ดของคุณ) แต่ฉันพบว่าการเรียก WaitForSingleObject ด้วย INFINITE ทำให้ประสิทธิภาพไม่ดี การส่งค่าการหมดเวลาเป็น 1 จากนั้นการวนซ้ำในขณะที่ตรวจสอบว่ามันส่งคืนได้สร้างความแตกต่างอย่างมากในประสิทธิภาพของโค้ดของฉัน ส่วนใหญ่อยู่ในบริบทของการรอการจัดการกระบวนการภายนอกอย่างไรก็ตาม ... ไม่ใช่ mutex YMMV. ฉันสนใจที่จะดูว่า mutex ทำงานอย่างไรกับการดัดแปลงนั้น ผลต่างของเวลาที่ได้จากการทดสอบนี้ดูเหมือนจะใหญ่กว่าที่ควรจะเป็น
Troy Howard

5
@TroyHoward โดยพื้นฐานแล้วคุณไม่ได้หมุนล็อคที่จุดนั้นเหรอ?
dss539

สาเหตุของความแตกต่างนี้เป็นไปได้ทางประวัติศาสตร์เป็นหลัก ไม่ยากที่จะใช้การล็อกที่เร็วเท่ากับ CriticalSection ในกรณีที่ไม่มีการแก้ไข (คำสั่งเกี่ยวกับอะตอมไม่กี่คำสั่งไม่มี syscalls) แต่ทำงานข้ามกระบวนการได้ (ด้วยหน่วยความจำที่ใช้ร่วมกัน) ดูเช่นfutexes ลินุกซ์
regnarg

2
@ ทรอยลองบังคับให้ CPU ของคุณทำงานที่ 100% ตลอดเวลาและดูว่า INFINITE ทำงานได้ดีขึ้นหรือไม่ กลยุทธ์การใช้พลังงานอาจใช้เวลานานถึง 40 มิลลิวินาทีในเครื่องของฉัน (Dell XPS-8700) ในการรวบรวมข้อมูลกลับไปที่ความเร็วเต็มที่หลังจากตัดสินใจที่จะทำงานช้าลงซึ่งอาจไม่ได้ผลหากคุณนอนหลับหรือรอเพียงมิลลิวินาที
Stevens Miller

ฉันไม่แน่ใจว่าฉันเข้าใจสิ่งที่กำลังแสดงอยู่ที่นี่ โดยทั่วไปการเข้าสู่ส่วนวิกฤตจำเป็นต้องได้รับสัญญาณบางชนิด คุณกำลังบอกว่าเบื้องหลัง O / S มีวิธีที่มีประสิทธิภาพในการนำพฤติกรรมส่วนที่สำคัญนี้ไปใช้โดยไม่ต้องใช้ mutexes?
SN

89

จากมุมมองทางทฤษฎีส่วนที่สำคัญคือส่วนของโค้ดที่ต้องไม่รันโดยหลายเธรดพร้อมกันเนื่องจากโค้ดเข้าถึงทรัพยากรที่ใช้ร่วมกัน

mutexเป็นอัลกอริทึม (และบางครั้งชื่อของโครงสร้างข้อมูล) ที่ใช้ในการป้องกันส่วนที่สำคัญ

SemaphoresและMonitorsเป็นการนำ mutex ไปใช้งานทั่วไป

ในทางปฏิบัติมีการใช้งาน mutex มากมายใน windows ส่วนใหญ่แตกต่างกันเนื่องจากการนำไปใช้งานตามระดับการล็อกขอบเขตค่าใช้จ่ายและประสิทธิภาพภายใต้การช่วงชิงระดับที่แตกต่างกัน ดูCLR Inside Out - การใช้ความพร้อมกันสำหรับความสามารถในการปรับขนาดสำหรับแผนภูมิต้นทุนของการใช้งาน mutex ที่แตกต่างกัน

Primitives การซิงโครไนซ์ที่พร้อมใช้งาน

lock(object)คำสั่งจะดำเนินการโดยใช้Monitor- ดูMSDNสำหรับการอ้างอิง

ในปีที่ผ่านมาการวิจัยมากจะทำในไม่ปิดกั้นการประสาน เป้าหมายคือการนำอัลกอริทึมไปใช้ด้วยวิธีที่ไม่ต้องล็อกหรือไม่ต้องรอ ในอัลกอริทึมดังกล่าวกระบวนการจะช่วยให้กระบวนการอื่น ๆ ทำงานให้เสร็จเพื่อให้กระบวนการทำงานเสร็จสิ้นในที่สุด ด้วยเหตุนี้กระบวนการสามารถทำงานให้เสร็จสิ้นได้แม้ในขณะที่กระบวนการอื่น ๆ ที่พยายามทำงานบางอย่างก็หยุดทำงาน Usinig ล็อกพวกเขาจะไม่คลายล็อกและป้องกันไม่ให้กระบวนการอื่น ๆ ดำเนินต่อไป


เมื่อเห็นคำตอบที่ยอมรับฉันคิดว่าบางทีฉันอาจจำแนวคิดของส่วนที่สำคัญผิดจนกระทั่งฉันเห็นว่ามุมมองเชิงทฤษฎีที่คุณเขียน :)
Anirudh Ramanathan

2
การเขียนโปรแกรมแบบไม่มีการล็อกในทางปฏิบัติก็เหมือนกับแชงกรีลายกเว้นจะมีอยู่ Keir เฟรเซอร์กระดาษ (PDF) สำรวจนี้ค่อนข้างน่าสนใจ (จะกลับไปที่ 2004) และเรายังคงดิ้นรนกับมันในปี 2012 เราห่วย
Tim Post

22

นอกเหนือจากคำตอบอื่น ๆ แล้วรายละเอียดต่อไปนี้เป็นข้อมูลเฉพาะของส่วนที่สำคัญบน windows:

  • ในกรณีที่ไม่มีการโต้แย้งการได้มาซึ่งส่วนที่สำคัญนั้นง่ายพอ ๆ กับการInterlockedCompareExchangeดำเนินการ
  • โครงสร้างส่วนที่สำคัญมีพื้นที่สำหรับ mutex ตอนแรกยังไม่ได้จัดสรร
  • หากมีความขัดแย้งระหว่างเธรดสำหรับส่วนที่สำคัญ mutex จะถูกจัดสรรและใช้ ประสิทธิภาพของส่วนที่สำคัญจะลดลงเป็นของ mutex
  • หากคุณคาดว่าจะมีการโต้แย้งสูงคุณสามารถจัดสรรส่วนวิกฤตที่ระบุจำนวนสปินได้
  • หากมีความขัดแย้งในส่วนวิกฤตที่มีจำนวนสปินเธรดที่พยายามรับส่วนวิกฤตจะหมุน (ไม่ว่างรอ) สำหรับรอบตัวประมวลผลจำนวนมากนั้น สิ่งนี้สามารถส่งผลให้มีประสิทธิภาพที่ดีกว่าการนอนหลับเนื่องจากจำนวนรอบในการสลับบริบทไปยังเธรดอื่นอาจสูงกว่าจำนวนรอบที่เธรดที่เป็นเจ้าของเพื่อคลาย mutex
  • หากการนับสปินหมดอายุ mutex จะถูกจัดสรร
  • เมื่อเธรดที่เป็นเจ้าของเผยแพร่ส่วนสำคัญจำเป็นต้องตรวจสอบว่ามีการจัดสรร mutex หรือไม่หากเป็นเช่นนั้นจะตั้งค่า mutex เพื่อปล่อยเธรดที่รอ

ในลินุกซ์ฉันคิดว่าพวกเขามี "สปินล็อค" ที่ทำหน้าที่คล้ายกันกับส่วนวิกฤตที่มีการนับสปิน


น่าเสียดายที่ส่วนสำคัญของ Window เกี่ยวข้องกับการดำเนินการ CAS ในโหมดเคอร์เนลซึ่งมีราคาแพงกว่าการดำเนินการเชื่อมต่อกันอย่างมาก นอกจากนี้ส่วนที่สำคัญของ Windows สามารถมีจำนวนสปินที่เกี่ยวข้องได้
สัญญา

2
นั่นไม่เป็นความจริงอย่างแน่นอน CAS สามารถทำได้ด้วย cmpxchg ในโหมดผู้ใช้
Michael

ฉันคิดว่าจำนวนสปินเริ่มต้นเป็นศูนย์ถ้าคุณเรียก InitializeCriticalSection - คุณต้องเรียกใช้ InitializeCriticalSectionAndSpinCount หากคุณต้องการใช้การนับสปิน คุณมีข้อมูลอ้างอิงสำหรับสิ่งนั้นหรือไม่?
1800 ข้อมูล

18

Critical Section และ Mutex ไม่ใช่ระบบปฏิบัติการที่เฉพาะเจาะจง แต่เป็นแนวคิดของการประมวลผลแบบหลายเธรด / มัลติโปรเซสเซอร์

Critical Section คือโค้ดส่วนหนึ่งที่ต้องรันด้วยตัวเองในช่วงเวลาใดเวลาหนึ่งเท่านั้น (ตัวอย่างเช่นมี 5 เธรดที่ทำงานพร้อมกันและฟังก์ชันที่เรียกว่า "Critical_section_function" ซึ่งจะอัปเดตอาร์เรย์ ... คุณไม่ต้องการให้ทั้ง 5 เธรด การอัปเดตอาร์เรย์ในครั้งเดียวดังนั้นเมื่อโปรแกรมกำลังเรียกใช้ Critical_section_function () เธรดอื่น ๆ จะต้องรันฟังก์ชัน Critical_section_function ไม่ได้

mutex * Mutex เป็นวิธีการใช้งานโค้ดส่วนที่สำคัญ (คิดว่ามันเหมือนโทเค็น ... เธรดจะต้องมีมันอยู่ในครอบครองเพื่อเรียกใช้ Critical_section_code)


2
นอกจากนี้ mutexes ยังสามารถใช้ร่วมกันระหว่างกระบวนการต่างๆ
Configurator

15

mutex เป็นอ็อบเจ็กต์ที่เธรดสามารถรับได้เพื่อป้องกันไม่ให้เธรดอื่นได้รับมัน เป็นที่ปรึกษาไม่บังคับ เธรดสามารถใช้ทรัพยากรที่ mutex แสดงโดยไม่ต้องรับมัน

ส่วนที่สำคัญคือความยาวของรหัสที่ระบบปฏิบัติการรับประกันว่าจะไม่ถูกแทรกแซง ในรหัสหลอกจะเป็นดังนี้:

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

2
ฉันคิดว่าผู้โพสต์กำลังพูดถึง primitives การซิงโครไนซ์โหมดผู้ใช้เช่นออบเจ็กต์ส่วนสำคัญของ win32 ซึ่งให้การยกเว้นซึ่งกันและกัน ฉันไม่รู้เกี่ยวกับ Linux แต่เคอร์เนลของ Windows มีขอบเขตที่สำคัญซึ่งทำงานเหมือนที่คุณอธิบาย - ไม่ถูกขัดจังหวะ
Michael

2
ฉันไม่รู้ว่าทำไมคุณถึงถูกโหวตลง มีแนวคิดของส่วนที่สำคัญซึ่งคุณได้อธิบายอย่างถูกต้องซึ่งแตกต่างจากวัตถุเคอร์เนลของ Windows ที่เรียกว่า CriticalSection ซึ่งเป็นประเภทของ mutex ฉันเชื่อว่า OP กำลังถามเกี่ยวกับคำจำกัดความหลัง
Adam Rosenfield

1
อย่างน้อยฉันก็สับสนกับแท็กที่ไม่เข้าใจเรื่องภาษา แต่ไม่ว่าในกรณีใดนี่คือสิ่งที่เราได้รับจากการที่ Microsoft ตั้งชื่อการใช้งานเหมือนกับคลาสพื้นฐาน ฝึกเขียนโค้ดไม่ดี!
Mikko Rantanen

เขาขอรายละเอียดให้มากที่สุดเท่าที่จะเป็นไปได้และกล่าวเฉพาะ Windows และ Linux ดังนั้นดูเหมือนว่าแนวคิดจะดี +1 - ไม่เข้าใจ -1 อย่างใดอย่างหนึ่ง: /
Jason Coco

14

Windows ที่ 'เร็ว' เท่ากับการเลือกที่สำคัญใน Linux จะเป็นfutexซึ่งย่อมาจาก fast user space mutex ความแตกต่างระหว่าง futex และ mutex คือเมื่อใช้ futex เคอร์เนลจะมีส่วนเกี่ยวข้องเมื่อจำเป็นต้องใช้อนุญาโตตุลาการเท่านั้นดังนั้นคุณจึงประหยัดค่าใช้จ่ายในการพูดคุยกับเคอร์เนลทุกครั้งที่มีการแก้ไขตัวนับอะตอม ว่า .. สามารถบันทึกอย่างมีนัยสำคัญจำนวนเวลาล็อคการเจรจาต่อรองในบางโปรแกรม

นอกจากนี้ยังสามารถแชร์ futex ระหว่างกระบวนการต่างๆได้โดยใช้วิธีที่คุณจะใช้ในการแบ่งปัน mutex

น่าเสียดายที่ futexes สามารถใช้งานได้ (PDF) เป็นเรื่องยุ่งยากมาก (อัปเดตปี 2018 ไม่น่ากลัวเท่าปี 2009)

นอกเหนือจากนั้นมันก็เหมือนกันในทั้งสองแพลตฟอร์ม คุณกำลังทำการอัปเดตอะตอมที่ขับเคลื่อนด้วยโทเค็นไปยังโครงสร้างที่ใช้ร่วมกันในลักษณะที่ (หวังว่า) จะไม่ทำให้เกิดความอดอยาก สิ่งที่เหลืออยู่เป็นเพียงวิธีการทำสิ่งนั้นให้สำเร็จ


6

ใน Windows ส่วนที่สำคัญจะอยู่ในกระบวนการของคุณ mutex สามารถแชร์ / เข้าถึงระหว่างกระบวนการต่างๆ โดยทั่วไปส่วนที่สำคัญมีราคาถูกกว่ามาก ไม่สามารถแสดงความคิดเห็นเกี่ยวกับ Linux โดยเฉพาะ แต่ในบางระบบพวกเขาเป็นเพียงนามแฝงสำหรับสิ่งเดียวกัน


6

เพียงเพื่อเพิ่ม 2 เซ็นต์ของฉันส่วนที่สำคัญจะถูกกำหนดเป็นโครงสร้างและการดำเนินการกับพวกเขาจะดำเนินการในบริบทโหมดผู้ใช้

ntdll! _RTL_CRITICAL_SECTION
   + 0x000 DebugInfo: Ptr32 _RTL_CRITICAL_SECTION_DEBUG
   + 0x004 LockCount: Int4B
   + 0x008 RecursionCount: Int4B
   + 0x00c เป็นเจ้าของกระทู้: Ptr32 Void
   + 0x010 LockSemaphore: Ptr32 Void
   + 0x014 SpinCount: Uint4B

ในขณะที่ mutex เป็นเคอร์เนลอ็อบเจ็กต์ (ExMutantObjectType) ที่สร้างในไดเร็กทอรีอ็อบเจ็กต์ Windows การดำเนินการ Mutex ส่วนใหญ่จะใช้ในโหมดเคอร์เนล ตัวอย่างเช่นเมื่อสร้าง Mutex คุณจะเรียก nt! NtCreateMutant ในเคอร์เนล


จะเกิดอะไรขึ้นเมื่อโปรแกรมที่เริ่มต้นและใช้วัตถุ Mutex ขัดข้อง วัตถุ Mutex ถูกยกเลิกการจัดสรรโดยอัตโนมัติหรือไม่ ไม่ฉันจะบอกว่า ขวา?
อังกูร์

6
เคอร์เนลอ็อบเจ็กต์มีจำนวนอ้างอิง การปิดจุดจับกับวัตถุจะลดจำนวนการอ้างอิงและเมื่อถึง 0 วัตถุจะถูกปลดปล่อย เมื่อกระบวนการล้มเหลวจุดจับทั้งหมดจะถูกปิดโดยอัตโนมัติดังนั้น mutex ที่มีเพียงกระบวนการนั้นเท่านั้นที่มีจุดจับที่จะถูกจัดสรรโดยอัตโนมัติ
Michael

และนี่คือเหตุผลว่าทำไมอ็อบเจ็กต์ส่วนที่สำคัญจึงถูกผูกมัดในทางกลับกัน mutexes สามารถแชร์ข้ามกระบวนการได้
Sisir

3

คำตอบที่ดีจาก Michael ฉันได้เพิ่มการทดสอบที่สามสำหรับคลาส mutex ที่แนะนำใน C ++ 11 ผลลัพธ์ค่อนข้างน่าสนใจและยังคงสนับสนุนการรับรอง CRITICAL_SECTION ออบเจ็กต์สำหรับกระบวนการเดียว

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);

ผลลัพธ์ของฉันคือ 217, 473 และ 19 (โปรดทราบว่าอัตราส่วนของเวลาของฉันสำหรับสองครั้งล่าสุดนั้นเทียบได้กับของ Michael แต่เครื่องของฉันมีอายุน้อยกว่าของเขาอย่างน้อยสี่ปีดังนั้นคุณจะเห็นหลักฐานของความเร็วที่เพิ่มขึ้นระหว่างปี 2009 ถึง 2013 , เมื่อ XPS-8700 ออกมา) คลาส mutex ใหม่เร็วกว่า Windows mutex สองเท่า แต่ยังน้อยกว่าหนึ่งในสิบของความเร็วของวัตถุ Windows CRITICAL_SECTION โปรดทราบว่าฉันทดสอบเฉพาะ mutex แบบไม่เรียกซ้ำเท่านั้น CRITICAL_SECTION ออบเจ็กต์แบบวนซ้ำ (หนึ่งเธรดสามารถป้อนซ้ำได้หากออกจากจำนวนครั้งเท่ากัน)


0

ฟังก์ชัน AC เรียกว่า reentrant หากใช้พารามิเตอร์จริงเท่านั้น

ฟังก์ชัน Reentrant สามารถเรียกใช้โดยหลายเธรดในเวลาเดียวกันได้

ตัวอย่างฟังก์ชัน reentrant:

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

   c = a + b;

   return c;
}

ตัวอย่างฟังก์ชัน non reentrant:

int result;

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

   c = a + b;

   result = c;

}

ไลบรารีมาตรฐาน C strtok () ไม่ได้ reentrant และไม่สามารถใช้โดย 2 เธรดหรือมากกว่าในเวลาเดียวกัน

SDK บางแพลตฟอร์มมาพร้อมกับ strtok () เวอร์ชัน reentrant ที่เรียกว่า strtok_r ();

Enrico Migliore

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.