อะไรคือวิธีที่ดีที่สุดในการติดตั้งพจนานุกรมเธรดที่ปลอดภัย


109

ฉันสามารถใช้พจนานุกรมเธรดปลอดภัยใน C # ได้โดยหามาจาก IDictionary และกำหนดอ็อบเจ็กต์ SyncRoot ส่วนตัว:

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public object SyncRoot
    {
        get { return syncRoot; }
    } 

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
    }

    // more IDictionary members...
}

จากนั้นฉันล็อควัตถุ SyncRoot นี้ตลอดผู้บริโภคของฉัน (หลายเธรด):

ตัวอย่าง:

lock (m_MySharedDictionary.SyncRoot)
{
    m_MySharedDictionary.Add(...);
}

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


3
คุณคิดว่ามันน่าเกลียดอะไร?
Asaf R

1
ฉันคิดว่าเขาอ้างถึงคำสั่งล็อคทั้งหมดที่เขามีตลอดรหัสของเขาภายในผู้บริโภคของคลาส SharedDictionary - เขาล็อครหัสการโทรทุกครั้งที่เขาเข้าถึงเมธอดบนวัตถุ SharedDictionary
Peter Meyer

แทนที่จะใช้วิธีการเพิ่มลองทำโดยกำหนดค่า ex- m_MySharedDictionary ["key1"] = "item1" นี่คือเธรดที่ปลอดภัย
testuser

คำตอบ:


43

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

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
        OnItemAdded(EventArgs.Empty);
    }

    public event EventHandler ItemAdded;

    protected virtual void OnItemAdded(EventArgs e)
    {
        EventHandler handler = ItemAdded;
        if (handler != null)
            handler(this, e);
    }

    // more IDictionary members...
}

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


@fryguybob: การแจงนับเป็นเหตุผลเดียวที่ทำให้ฉันเปิดเผยวัตถุซิงโครไนซ์ ตามแบบแผนฉันจะทำการล็อควัตถุนั้นก็ต่อเมื่อฉันแจกแจงผ่านคอลเลกชัน
.

1
หากพจนานุกรมของคุณมีขนาดไม่ใหญ่เกินไปคุณสามารถระบุในสำเนาและมีอยู่ในชั้นเรียน
friedguybob

2
พจนานุกรมของฉันไม่ใหญ่เกินไปและฉันคิดว่านั่นเป็นเคล็ดลับ สิ่งที่ฉันทำคือสร้างวิธีการสาธารณะใหม่ที่เรียกว่า CopyForEnum () ซึ่งส่งคืนอินสแตนซ์ใหม่ของพจนานุกรมพร้อมสำเนาของพจนานุกรมส่วนตัว จากนั้นจึงเรียกวิธีนี้สำหรับการแจงนับและ SyncRoot ถูกลบออก ขอบคุณ!
.

12
นอกจากนี้ยังไม่ใช่คลาส threadsafe โดยเนื้อแท้เนื่องจากการทำงานของพจนานุกรมมักจะเป็นแบบละเอียด ตรรกะเล็ก ๆ น้อย ๆ ตามบรรทัดของ if (dict.Contains (อะไรก็ได้)) {dict.Remove (อะไรก็ได้); dict.Add (อะไรก็ได้ค่าใหม่); } เป็นสภาพการแข่งขันที่รอให้เกิดขึ้นอย่างแน่นอน
แท่น

207

สุทธิ 4.0 ConcurrentDictionaryชั้นเรียนที่สนับสนุนเห็นพ้องด้วยการตั้งชื่อ


4
โปรดทำเครื่องหมายว่าเป็นคำตอบคุณไม่จำเป็นต้องมีคำสั่งที่กำหนดเองหาก Net มีวิธีแก้ไข
Alberto León

27
(โปรดจำไว้ว่าคำตอบอื่นเขียนไว้นานก่อนที่จะมี. NET 4.0 (เผยแพร่ในปี 2010))
Jeppe Stig Nielsen

1
น่าเสียดายที่ไม่ใช่โซลูชันที่ไม่มีการล็อกดังนั้นจึงไม่มีประโยชน์ในชุดประกอบที่ปลอดภัยของ SQL Server CLR คุณต้องการบางสิ่งเช่นที่อธิบายไว้ที่นี่: cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdfหรือบางทีการใช้งานนี้: github.com/hackcraft/Ariadne
Triynko

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

60

ความพยายามที่จะซิงโครไนซ์ภายในแทบจะไม่เพียงพออย่างแน่นอนเนื่องจากมีระดับนามธรรมต่ำเกินไป สมมติว่าคุณทำAddและContainsKeyดำเนินการทีละเธรดปลอดภัยดังนี้:

public void Add(TKey key, TValue value)
{
    lock (this.syncRoot)
    {
        this.innerDictionary.Add(key, value);
    }
}

public bool ContainsKey(TKey key)
{
    lock (this.syncRoot)
    {
        return this.innerDictionary.ContainsKey(key);
    }
}

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

if (!mySafeDictionary.ContainsKey(someKey))
{
    mySafeDictionary.Add(someKey, someValue);
}

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

ซึ่งหมายความว่าจะเขียนสถานการณ์ประเภทนี้ได้อย่างถูกต้องคุณต้องมีการล็อกนอกพจนานุกรมเช่น

lock (mySafeDictionary)
{
    if (!mySafeDictionary.ContainsKey(someKey))
    {
        mySafeDictionary.Add(someKey, someValue);
    }
}

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

  1. ใช้แบบปกติDictionary<TKey, TValue>และซิงโครไนซ์ภายนอกปิดล้อมการดำเนินการแบบผสมหรือ

  2. เขียนกระดาษห่อหุ้มเธรดใหม่ด้วยอินเทอร์เฟซที่แตกต่างกัน (กล่าวคือไม่ใช่IDictionary<T>) ที่รวมการดำเนินการเช่นAddIfNotContainedวิธีการดังนั้นคุณจึงไม่จำเป็นต้องรวมการดำเนินการจากมัน

(ฉันมักจะไปอันดับ 1 ด้วยตัวเอง)


10
เป็นสิ่งที่ควรค่าแก่การชี้ให้เห็นว่า. NET 4.0 จะรวมคอนเทนเนอร์ที่ปลอดภัยสำหรับเธรดจำนวนมากเช่นคอลเล็กชันและพจนานุกรมซึ่งมีอินเทอร์เฟซที่แตกต่างจากคอลเล็กชันมาตรฐาน (กล่าวคือพวกเขากำลังทำตัวเลือกที่ 2 ด้านบนให้คุณ)
Greg Beech

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

6

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

หากประสิทธิภาพการทำงานพิสูจน์ได้ว่าไม่ดีโดยใช้การล็อกมาตรฐานคอลเลกชันPower Threadingของ Wintellect จะมีประโยชน์มาก


5

มีปัญหาหลายประการเกี่ยวกับวิธีการใช้งานที่คุณอธิบาย

  1. คุณไม่ควรเปิดเผยวัตถุการซิงโครไนซ์ของคุณ การทำเช่นนั้นจะเป็นการเปิดตัวเองให้ผู้บริโภคได้หยิบจับของและทำการล็อคมันจากนั้นคุณก็จะปิ้ง
  2. คุณกำลังใช้อินเทอร์เฟซที่ปลอดภัยแบบไม่ใช้เธรดกับคลาสเธรดที่ปลอดภัย IMHO สิ่งนี้จะทำให้คุณต้องเสียเงิน

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


3

คุณไม่จำเป็นต้องล็อกคุณสมบัติ SyncRoot ในวัตถุผู้บริโภคของคุณ การล็อกที่คุณมีอยู่ในวิธีการของพจนานุกรมนั้นเพียงพอแล้ว

ทำอย่างละเอียด: สิ่งที่เกิดขึ้นคือพจนานุกรมของคุณถูกล็อกเป็นระยะเวลานานเกินความจำเป็น

สิ่งที่เกิดขึ้นในกรณีของคุณมีดังต่อไปนี้:

พูดว่าเธรด A ได้รับการล็อกบน SyncRoot มาก่อนเรียกไปที่ m_mySharedDictionary.Add จากนั้นเธรด B พยายามที่จะได้รับการล็อค แต่ถูกบล็อก ในความเป็นจริงเธรดอื่นทั้งหมดถูกบล็อก เธรด A ได้รับอนุญาตให้เรียกเข้าสู่วิธีการเพิ่ม ที่คำสั่งล็อกภายในวิธีการเพิ่มเธรด A ได้รับอนุญาตให้รับการล็อกอีกครั้งเนื่องจากเป็นเจ้าของอยู่แล้ว เมื่อออกจากบริบทการล็อกภายในเมธอดและนอกเมธอดเธรด A ได้คลายล็อกทั้งหมดเพื่อให้เธรดอื่นดำเนินการต่อ

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


1
ไม่เป็นความจริง ... หากคุณดำเนินการสองครั้งหลังจากดำเนินการอื่นที่ปลอดภัยต่อเธรดภายในนั่นไม่ได้หมายความว่าบล็อกโค้ดโดยรวมนั้นปลอดภัย ตัวอย่างเช่น if (! myDict.ContainsKey (someKey)) {myDict.Add (someKey, someValue); } จะไม่ปลอดภัยสำหรับเธรดแม้ว่ามันจะมีคีย์และแอดก็คือเธรดที่ปลอดภัย
Tim

ประเด็นของคุณถูกต้อง แต่ไม่ตรงกับคำตอบและคำถามของฉัน หากคุณดูคำถามมันไม่ได้พูดถึงการเรียกชื่อประกอบด้วยคีย์หรือคำตอบของฉัน คำตอบของฉันหมายถึงการได้รับการล็อค SyncRoot ซึ่งแสดงในตัวอย่างในคำถามเดิม ภายในบริบทของคำสั่ง lock การดำเนินการ thread-safe อย่างน้อยหนึ่งอย่างจะดำเนินการได้อย่างปลอดภัย
Peter Meyer

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

กลไกการล็อกภายนอกเป็นไปตามที่เขาแสดงในตัวอย่างของเขาในคำถาม: lock (m_MySharedDictionary.SyncRoot) {m_MySharedDictionary.Add (... ); } - การทำสิ่งต่อไปนี้จะปลอดภัยอย่างสมบูรณ์: lock (m_MySharedDictionary.SyncRoot) {if (! m_MySharedDictionary.Contains (... )) {m_MySharedDictionary.Add (... ); }} กล่าวอีกนัยหนึ่งกลไกการล็อกภายนอกคือคำสั่งล็อคที่ทำงานบนคุณสมบัติสาธารณะ SyncRoot
Peter Meyer

0

แค่คิดว่าทำไมไม่สร้างพจนานุกรมขึ้นมาใหม่? หากการอ่านเป็นการเขียนจำนวนมากการล็อกจะซิงโครไนซ์คำขอทั้งหมด

ตัวอย่าง

    private static readonly object Lock = new object();
    private static Dictionary<string, string> _dict = new Dictionary<string, string>();

    private string Fetch(string key)
    {
        lock (Lock)
        {
            string returnValue;
            if (_dict.TryGetValue(key, out returnValue))
                return returnValue;

            returnValue = "find the new value";
            _dict = new Dictionary<string, string>(_dict) { { key, returnValue } };

            return returnValue;
        }
    }

    public string GetValue(key)
    {
        string returnValue;

        return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key);
    }

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