.NET - การล็อกพจนานุกรมเทียบกับ ConcurrentDictionary


133

ฉันหาข้อมูลเกี่ยวกับConcurrentDictionaryประเภทไม่เพียงพอจึงคิดว่าจะถามเกี่ยวกับเรื่องนี้ที่นี่

ขณะนี้ฉันใช้ a Dictionaryเพื่อระงับผู้ใช้ทั้งหมดที่มีการเข้าถึงหลายเธรดอย่างต่อเนื่อง (จากเธรดพูลจึงไม่มีจำนวนเธรดที่แน่นอน) และมีการซิงโครไนซ์การเข้าถึง

ฉันเพิ่งพบว่ามีชุดของคอลเลกชันที่ปลอดภัยต่อเธรดใน. NET 4.0 และดูเหมือนว่าจะเป็นที่ชื่นชอบมาก ฉันสงสัยว่าตัวเลือก 'มีประสิทธิภาพและจัดการง่ายกว่า' จะเป็นอย่างไรเนื่องจากฉันมีตัวเลือกระหว่างการDictionaryเข้าถึงแบบปกติกับการเข้าถึงแบบซิงโครไนซ์หรือมีConcurrentDictionaryเธรดที่ปลอดภัยอยู่แล้ว

อ้างอิงถึง. NET 4.0 ConcurrentDictionary


คำตอบ:


148

คอลเลกชันที่ปลอดภัยต่อเธรดกับคอลเลกชันที่ไม่ใช่เธรดปลอดภัยสามารถดูได้ด้วยวิธีอื่น

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

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

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

คอลเลกชัน threadsafe รับประกันว่าโครงสร้างข้อมูลภายในถูกต้องตลอดเวลาแม้ว่าจะเข้าถึงได้จากหลายเธรดก็ตาม

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

อย่างไรก็ตามคอลเลกชันที่ปลอดภัยของเธรดไม่รับประกันว่าการดำเนินการตามลำดับบนเธรดทั้งหมดทำงานบน "สแน็ปช็อต" ของโครงสร้างข้อมูลภายในเดียวกันซึ่งหมายความว่าหากคุณมีโค้ดเช่นนี้:

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

คุณอาจได้รับ NullReferenceException เพราะ inbetween tree.Countและtree.First()ด้ายอื่นได้ออกมาเคลียร์โหนดที่เหลืออยู่ในต้นไม้ซึ่งหมายความว่าจะกลับมาFirst()null

สำหรับสถานการณ์นี้คุณต้องดูว่าคอลเล็กชันที่เป็นปัญหามีวิธีที่ปลอดภัยในการรับสิ่งที่คุณต้องการหรือไม่บางทีคุณอาจต้องเขียนโค้ดด้านบนใหม่หรือคุณอาจต้องล็อก


9
ทุกครั้งที่ฉันอ่านบทความนี้แม้ว่ามันจะเป็นความจริงทั้งหมด แต่เรากำลังเดินไปผิดทาง
scope_creep

19
ปัญหาหลักในแอปพลิเคชันที่เปิดใช้งานเธรดคือโครงสร้างข้อมูลที่ไม่แน่นอน หากคุณสามารถหลีกเลี่ยงสิ่งนั้นได้คุณจะช่วยตัวเองได้มาก
Lasse V.Karlsen

92
นี่เป็นคำอธิบายที่ดีเกี่ยวกับความปลอดภัยของเธรด แต่ฉันอดไม่ได้ที่จะรู้สึกว่านี่ไม่ได้ตอบคำถามของ OP สิ่งที่ OP ถาม (และสิ่งที่ฉันเจอในภายหลังจากคำถามนี้กำลังมองหา) คือความแตกต่างระหว่างการใช้มาตรฐานDictionaryและการจัดการการล็อกด้วยตัวเองเทียบกับการใช้ConcurrentDictionaryประเภทที่มีอยู่ใน. NET 4+ ฉันรู้สึกงุนงงเล็กน้อยที่ได้รับการยอมรับแล้ว
mclark1129

4
ความปลอดภัยของเกลียวเป็นมากกว่าการใช้คอลเลกชันที่เหมาะสม การใช้คอลเลกชันที่เหมาะสมเป็นการเริ่มต้นไม่ใช่สิ่งเดียวที่คุณต้องจัดการ ฉันเดาว่านั่นคือสิ่งที่ OP อยากรู้ ฉันเดาไม่ออกว่าทำไมถึงได้รับการยอมรับเป็นเวลานานแล้วที่ฉันเขียนสิ่งนี้
Lasse V.Karlsen

2
ฉันคิดว่าสิ่งที่ @ LasseV.Karlsen พยายามจะพูดก็คือ ... ความปลอดภัยของเธรดนั้นทำได้ง่าย แต่คุณต้องจัดหาระดับของความพร้อมกันในการจัดการกับความปลอดภัยนั้น ถามตัวเองว่า "ฉันสามารถดำเนินการเพิ่มเติมในเวลาเดียวกันในขณะที่รักษาเธรดออบเจ็กต์ให้ปลอดภัยได้หรือไม่" ลองพิจารณาตัวอย่างนี้: ลูกค้าสองรายต้องการคืนสินค้าที่แตกต่างกัน เมื่อคุณในฐานะผู้จัดการเดินทางไปเติมสต๊อกร้านค้าของคุณคุณไม่เพียงแค่เติมสินค้าทั้งสองรายการในทริปเดียวและยังจัดการคำขอของลูกค้าทั้งสองได้หรือคุณจะเดินทางทุกครั้งที่ลูกค้าส่งคืนสินค้า
Alexandru

69

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

ตัวอย่างเช่นถ้าคุณตรวจสอบก่อนว่ามีคีย์อยู่หรือไม่จากนั้นรับค่าที่ตรงกับคีย์ในภายหลังคีย์นั้นอาจไม่มีอยู่อีกต่อไปแม้จะมีเวอร์ชัน ConcurrentDictionary (เนื่องจากเธรดอื่นอาจลบคีย์ออกไปแล้ว) คุณยังคงต้องใช้การล็อกในกรณีนี้ (หรือดีกว่า: รวมการโทรทั้งสองโดยใช้TryGetValue )

ดังนั้นให้ใช้มัน แต่อย่าคิดว่าจะให้บัตรผ่านฟรีเพื่อเพิกเฉยต่อปัญหาการเกิดพร้อมกันทั้งหมด คุณยังคงต้องระวัง


4
โดยพื้นฐานแล้วคุณกำลังพูดว่าถ้าคุณไม่ใช้วัตถุอย่างถูกต้องมันจะไม่ทำงาน?
ChaosPandion

3
โดยทั่วไปคุณต้องใช้ TryAdd แทนประกอบด้วย -> เพิ่มทั่วไป
ChaosPandion

3
@ Bruno: ไม่หากคุณยังไม่ได้ล็อกเธรดอื่นอาจลบคีย์ระหว่างสองสาย
Mark Byers

13
ไม่ได้ตั้งใจที่จะฟังดูห้วน แต่TryGetValueเป็นส่วนหนึ่งDictionaryและเป็นแนวทางที่แนะนำมาโดยตลอด ที่สำคัญวิธีการ "พร้อมกัน" ที่ConcurrentDictionaryเปิดตัวเป็นและAddOrUpdate GetOrAddดังนั้นคำตอบที่ดี แต่สามารถเลือกตัวอย่างที่ดีกว่านี้ได้
Aaronaught

4
ถูกต้อง - ไม่เหมือนกับในฐานข้อมูลที่มีการทำธุรกรรมโครงสร้างการค้นหาในหน่วยความจำพร้อมกันส่วนใหญ่พลาดแนวคิดในการแยกพวกเขารับประกันเฉพาะโครงสร้างข้อมูลภายในที่ถูกต้อง
ช้าง

40

Internal ConcurrentDictionary ใช้การล็อกแยกกันสำหรับแต่ละถังแฮช ตราบใดที่คุณใช้เฉพาะ Add / TryGetValue และวิธีการที่คล้ายกันซึ่งใช้ได้กับรายการเดียวพจนานุกรมจะทำงานเป็นโครงสร้างข้อมูลที่แทบจะไม่มีการล็อกพร้อมกับประโยชน์ด้านประสิทธิภาพที่น่าสนใจ OTOH วิธีการแจงนับ (รวมถึงคุณสมบัติ Count) จะล็อกที่เก็บข้อมูลทั้งหมดในครั้งเดียวดังนั้นจึงแย่กว่าพจนานุกรมที่ซิงโครไนซ์ซึ่งมีประสิทธิภาพ

ฉันจะบอกว่าแค่ใช้ ConcurrentDictionary


15

ฉันคิดว่าเมธอด ConcurrentDictionary.GetOrAdd เป็นสิ่งที่สถานการณ์แบบมัลติเธรดส่วนใหญ่ต้องการ


1
ฉันจะใช้ TryGet ด้วยมิฉะนั้นคุณจะเสียความพยายามในการเริ่มต้นพารามิเตอร์ที่สองเป็น GetOrAdd ทุกครั้งที่มีคีย์อยู่แล้ว
Jorrit Schippers

7
GetOrAddมีGetOrAddเกินพิกัด(TKey, Func <TKey, TValue>)ดังนั้นพารามิเตอร์ที่สองจึงสามารถเริ่มต้นแบบ lazy ได้เฉพาะในกรณีที่ไม่มีคีย์ในพจนานุกรม นอกจากนี้TryGetValueยังใช้ในการดึงข้อมูลไม่แก้ไข การเรียกแต่ละวิธีในภายหลังอาจทำให้เธรดอื่นอาจเพิ่มหรือลบคีย์ระหว่างการเรียกทั้งสอง
sgnsajgon

นี่ควรเป็นคำตอบที่ทำเครื่องหมายไว้สำหรับคำถามแม้ว่าโพสต์ของ Lasse จะยอดเยี่ยม
Spivonious

13

คุณเคยเห็นReactive Extensionsสำหรับ. Net 3.5sp1 หรือไม่ ตาม Jon Skeet พวกเขาได้แบ็คพอร์ตกลุ่มของส่วนขยายแบบขนานและโครงสร้างข้อมูลพร้อมกันสำหรับ. Net3.5 sp1

มีชุดตัวอย่างสำหรับ. Net 4 Beta 2 ซึ่งอธิบายรายละเอียดที่ค่อนข้างดีเกี่ยวกับวิธีใช้ส่วนขยายแบบขนาน

ฉันเพิ่งใช้เวลาสัปดาห์ที่แล้วในการทดสอบ ConcurrentDictionary โดยใช้ 32 เธรดเพื่อดำเนินการ I / O ดูเหมือนว่าจะได้ผลตามที่โฆษณาไว้ซึ่งจะบ่งบอกถึงการทดสอบจำนวนมหาศาล

แก้ไข : .NET 4 ConcurrentDictionary และรูปแบบพร้อมกัน

Microsoft ได้เปิดตัว pdf ชื่อ Patterns of Paralell Programming มันคุ้มค่ากับการดาวน์โหลดจริงๆเนื่องจากอธิบายไว้ในรายละเอียดที่ดีจริงๆถึงรูปแบบที่เหมาะสมที่จะใช้สำหรับส่วนขยาย. Net 4 พร้อมกันและรูปแบบการต่อต้านที่ควรหลีกเลี่ยง นี่คือ


4

โดยทั่วไปคุณต้องการไปกับ ConcurrentDictionary ใหม่ ทันทีที่คุณต้องเขียนโค้ดน้อยลงเพื่อสร้างโปรแกรมเธรดที่ปลอดภัย


จะมีปัญหาอะไรหรือไม่หากฉันดำเนินการต่อด้วยการใช้พจนานุกรมพร้อมกัน
kbvishnu

1

ConcurrentDictionaryเป็นตัวเลือกที่ดีถ้ามันตอบสนองทุกความต้องการของด้ายความปลอดภัยของคุณ หากไม่เป็นเช่นนั้นกล่าวอีกนัยหนึ่งคือคุณกำลังทำอะไรที่ซับซ้อนเล็กน้อยDictionary+ ปกติlockอาจเป็นตัวเลือกที่ดีกว่า ตัวอย่างเช่นสมมติว่าคุณกำลังเพิ่มคำสั่งซื้อบางรายการลงในพจนานุกรมและคุณต้องการอัปเดตจำนวนคำสั่งซื้อทั้งหมด คุณสามารถเขียนโค้ดดังนี้:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

รหัสนี้ไม่ปลอดภัยสำหรับเธรด เธรดหลายเธรดที่อัปเดต_totalAmountฟิลด์อาจปล่อยให้อยู่ในสถานะที่เสียหาย ดังนั้นคุณอาจพยายามป้องกันโดยlock:

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

รหัสนี้ "ปลอดภัยกว่า" แต่ยังไม่ปลอดภัยต่อเธรด ไม่มีการรับประกันว่า_totalAmountจะสอดคล้องกับรายการในพจนานุกรม เธรดอื่นอาจพยายามอ่านค่าเหล่านี้เพื่ออัปเดตองค์ประกอบ UI:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

totalAmountอาจไม่ตรงกับการนับจำนวนของการสั่งซื้อในพจนานุกรม สถิติที่แสดงอาจผิดพลาด ณ จุดนี้คุณจะทราบว่าคุณต้องขยายการlockป้องกันเพื่อรวมการอัปเดตพจนานุกรม:

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

รหัสนี้ปลอดภัยอย่างสมบูรณ์ แต่ประโยชน์ทั้งหมดของการใช้ a ConcurrentDictionaryจะหายไป

  1. ประสิทธิภาพการทำงานแย่ลงกว่าการใช้แบบธรรมดาDictionaryเนื่องจากการล็อคภายในภายในConcurrentDictionaryตอนนี้สิ้นเปลืองและซ้ำซ้อน
  2. คุณต้องตรวจสอบโค้ดทั้งหมดของคุณสำหรับการใช้งานตัวแปรที่แชร์โดยไม่มีการป้องกัน
  3. คุณติดอยู่กับการใช้ API ที่น่าอึดอัดใจ ( TryAdd?, AddOrUpdate?)

ดังนั้นคำแนะนำของฉันคือ: เริ่มต้นด้วยDictionary+ lockและเก็บตัวเลือกในการอัปเกรดในภายหลังConcurrentDictionaryเป็นการเพิ่มประสิทธิภาพการทำงานหากตัวเลือกนี้ใช้งานได้จริง ในหลาย ๆ กรณีจะไม่เป็นเช่นนั้น


0

เราได้ใช้ ConcurrentDictionary สำหรับคอลเลกชันที่แคชซึ่งจะมีการเติมข้อมูลใหม่ทุกๆ 1 ชั่วโมงจากนั้นอ่านโดยเธรดไคลเอนต์หลายรายการซึ่งคล้ายกับวิธีแก้ปัญหาสำหรับเธรดตัวอย่างนี้ปลอดภัยหรือไม่ คำถาม.

เราพบว่าการเปลี่ยนเป็น  ReadOnlyDictionary  ช่วยเพิ่มประสิทธิภาพโดยรวม


ReadOnlyDictionary เป็นเพียงกระดาษห่อหุ้มรอบ ๆ IDictionary อื่น ใต้ฝากระโปรงใช้อะไรครับ?
Lu55

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