เหตุใดล็อค (นี่) {…} ไม่ดี


484

MSDN เอกสารกล่าวว่า

public class SomeObject
{
  public void SomeOperation()
  {
    lock(this)
    {
      //Access instance variables
    }
  }
}

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

คำตอบ:


508

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

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

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

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

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

เรียกใช้รหัส C # ต่อไปนี้เป็นตัวอย่าง

public class Person
{
    public int Age { get; set;  }
    public string Name { get; set; }

    public void LockThis()
    {
        lock (this)
        {
            System.Threading.Thread.Sleep(10000);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var nancy = new Person {Name = "Nancy Drew", Age = 15};
        var a = new Thread(nancy.LockThis);
        a.Start();
        var b = new Thread(Timewarp);
        b.Start(nancy);
        Thread.Sleep(10);
        var anotherNancy = new Person { Name = "Nancy Drew", Age = 50 };
        var c = new Thread(NameChange);
        c.Start(anotherNancy);
        a.Join();
        Console.ReadLine();
    }

    static void Timewarp(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // A lock does not make the object read-only.
        lock (person.Name)
        {
            while (person.Age <= 23)
            {
                // There will be a lock on 'person' due to the LockThis method running in another thread
                if (Monitor.TryEnter(person, 10) == false)
                {
                    Console.WriteLine("'this' person is locked!");
                }
                else Monitor.Exit(person);
                person.Age++;
                if(person.Age == 18)
                {
                    // Changing the 'person.Name' value doesn't change the lock...
                    person.Name = "Nancy Smith";
                }
                Console.WriteLine("{0} is {1} years old.", person.Name, person.Age);
            }
        }
    }

    static void NameChange(object subject)
    {
        var person = subject as Person;
        if (person == null) throw new ArgumentNullException("subject");
        // You should avoid locking on strings, since they are immutable.
        if (Monitor.TryEnter(person.Name, 30) == false)
        {
            Console.WriteLine("Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string \"Nancy Drew\".");
        }
        else Monitor.Exit(person.Name);

        if (Monitor.TryEnter("Nancy Drew", 30) == false)
        {
            Console.WriteLine("Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!");
        }
        else Monitor.Exit("Nancy Drew");
        if (Monitor.TryEnter(person.Name, 10000))
        {
            string oldName = person.Name;
            person.Name = "Nancy Callahan";
            Console.WriteLine("Name changed from '{0}' to '{1}'.", oldName, person.Name);
        }
        else Monitor.Exit(person.Name);
    }
}

เอาต์พุตคอนโซล

'this' person is locked!
Nancy Drew is 16 years old.
'this' person is locked!
Nancy Drew is 17 years old.
Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string "Nancy Drew".
'this' person is locked!
Nancy Smith is 18 years old.
'this' person is locked!
Nancy Smith is 19 years old.
'this' person is locked!
Nancy Smith is 20 years old.
Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!
'this' person is locked!
Nancy Smith is 21 years old.
'this' person is locked!
Nancy Smith is 22 years old.
'this' person is locked!
Nancy Smith is 23 years old.
'this' person is locked!
Nancy Smith is 24 years old.
Name changed from 'Nancy Drew' to 'Nancy Callahan'.

2
ในฐานะที่ฉัน grok: (1)แนนซี่อยู่ใน thread1 พร้อมล็อค (นี้) (2) SAME Nancy อยู่ในกลุ่มอายุ 2 ในขณะที่ยังคงถูกล็อกใน thread1 - การพิสูจน์ว่าวัตถุที่ถูกล็อคไม่ได้เป็นแบบอ่านอย่างเดียว นอกจากนี้(2a)ในขณะที่อยู่ในเธรด 2 วัตถุ Nancy นี้จะถูกล็อคในชื่อ (3)สร้างวัตถุที่แตกต่างกันที่มีชื่อเดียวกัน (4)ผ่านเข้าไปใน thread3 และพยายามล็อคด้วยชื่อ (จบขนาดใหญ่)แต่ "สตริงไม่เปลี่ยนรูป" หมายถึงวัตถุใด ๆ ที่อ้างอิงสตริง "Nancy Drew" กำลังดูอินสแตนซ์ของสตริงเดียวกันในหน่วยความจำ ดังนั้น object2 จึงไม่สามารถล็อคสตริงได้เมื่อ object1 ถูกล็อคด้วยค่าเดียวกัน
Radarbob

การใช้ตัวแปรมาตรฐานแทนที่จะlock(this)เป็นคำแนะนำมาตรฐาน สิ่งสำคัญคือให้สังเกตว่าการทำเช่นนั้นโดยทั่วไปจะทำให้เป็นไปไม่ได้ที่โค้ดภายนอกจะทำให้เกิดการล็อคที่เกี่ยวข้องกับวัตถุระหว่างการเรียกเมธอด นี้อาจหรือไม่อาจจะเป็นสิ่งที่ดี มีอันตรายบางอย่างในการอนุญาตให้โค้ดภายนอกเก็บการล็อคสำหรับช่วงเวลาที่กำหนดเองและโดยทั่วไปคลาสควรได้รับการออกแบบเพื่อให้การใช้งานดังกล่าวไม่จำเป็น แต่ไม่มีทางเลือกที่ใช้งานได้จริงเสมอไป ในฐานะที่เป็นตัวอย่างง่ายๆเว้นแต่การดำเนินการจัดเก็บToArrayหรือToListวิธีการของตัวเอง ...
SuperCat

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

there is the common misconception that lock(this) actually modifies the object passed as a parameter, and in some way makes it read-only or inaccessible. This is false- ฉันเชื่อว่าการพูดคุยเหล่านั้นเกี่ยวกับ SyncBlock บิตในวัตถุ CLR ดังนั้นอย่างเป็นทางการนี้ถูกต้อง - ล็อควัตถุที่ปรับเปลี่ยนเอง
sll

@Esteban ฉันรักตัวอย่างของคุณมันยอดเยี่ยมมาก ฉันมีคำถามสำหรับคุณ. รหัสของคุณของเมธอด NameChange (.. ) ลงท้ายด้วย: <code> ถ้า (Monitor.TryEnter (person.Name, 10000)) {. . . } else Monitor.Exit (person.Name); </code> ไม่ควรลงท้ายด้วย: <code> ถ้า (Monitor.TryEnter (person.Name, 10000)) { . . Monitor.Exit (person.Name); } </code>
AviFarah

64

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

นอกจากนี้ยังเป็นการปฏิบัติที่ไม่ดีเพราะมันล็อค "มากเกินไป"

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


44
ย่อหน้าสุดท้ายของคำตอบนี้ไม่ถูกต้อง การล็อกไม่ได้ทำให้วัตถุไม่สามารถอ่านได้อย่างเดียว ล็อค (นี้) ไม่ได้ป้องกันเธรดอื่นจากการโทรหรือแก้ไขวัตถุที่อ้างอิงโดยสิ่งนี้
Esteban Brenes

3
มันจะทำอย่างไรถ้าวิธีการอื่นที่เรียกว่ายังทำล็อค (นี้) ฉันเชื่อว่าเป็นจุดที่เขาทำ สังเกตุ "ถ้าคุณล็อควัตถุทั้งหมดในฟังก์ชั่นของคุณ" ...
Herms

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

Re: ล็อค "มากเกินไป": มันเป็นการปรับสมดุลที่ดีในการตัดสินใจว่าจะล็อคอะไร โปรดทราบว่าการล็อคนั้นเกี่ยวข้องกับการทำงานของ CPU-flush แคชและค่อนข้างแพง กล่าวอีกนัยหนึ่ง: ห้ามล็อคและอัปเดตจำนวนเต็มแต่ละค่า :)
Zan Lynx

ย่อหน้าสุดท้ายยังไม่สมเหตุสมผล หากคุณต้องการ จำกัด การเข้าถึงรายการทำไมฟังก์ชั่นอื่น ๆ ถึงมีการล็อคถ้าพวกเขาไม่เข้าถึงรายการ?
โจอาคิม MH

44

ดูที่หัวข้อการซิงโครไนซ์เธรดหัวข้อ MSDN (คู่มือการเขียนโปรแกรม C #)

โดยทั่วไปแล้วจะเป็นการดีที่สุดที่จะหลีกเลี่ยงการล็อกชนิดสาธารณะหรือบนอินสแตนซ์ของวัตถุที่อยู่นอกเหนือการควบคุมแอปพลิเคชัน ตัวอย่างเช่นการล็อก (นี่) อาจเป็นปัญหาได้หากอินสแตนซ์สามารถเข้าถึงได้แบบสาธารณะเนื่องจากรหัสที่อยู่นอกเหนือการควบคุมของคุณอาจล็อควัตถุเช่นกัน สิ่งนี้สามารถสร้างสถานการณ์การหยุดชะงักที่มีสองเธรดขึ้นไปรอการปล่อยวัตถุเดียวกัน. การล็อกชนิดข้อมูลสาธารณะซึ่งตรงข้ามกับวัตถุอาจทำให้เกิดปัญหาด้วยเหตุผลเดียวกัน การล็อคสตริงตัวอักษรมีความเสี่ยงเป็นพิเศษเนื่องจากสตริงตัวอักษรถูก interned โดย runtime ภาษาทั่วไป (CLR) ซึ่งหมายความว่ามีหนึ่งอินสแตนซ์ของสตริงตัวอักษรใด ๆ ที่กำหนดสำหรับโปรแกรมทั้งหมดวัตถุเดียวกันที่แน่นอนหมายถึงตัวอักษรในโดเมนแอปพลิเคชันที่ทำงานอยู่ทั้งหมดในกระทู้ทั้งหมด ด้วยเหตุนี้การล็อกที่วางอยู่บนสตริงที่มีเนื้อหาเดียวกันทุกที่ในกระบวนการแอปพลิเคชันจะล็อกอินสแตนซ์ทั้งหมดของสตริงนั้นในแอปพลิเคชัน ดังนั้นจึงเป็นการดีที่สุดที่จะล็อคสมาชิกส่วนบุคคลหรือสมาชิกที่ได้รับการป้องกันที่ไม่ได้เข้าฝึกงาน บางคลาสจัดเตรียมสมาชิกสำหรับล็อกโดยเฉพาะ ตัวอย่างเช่น Array type ให้ SyncRoot คอลเลกชันหลายประเภทมีสมาชิก SyncRoot เช่นกัน


34

ฉันรู้ว่านี่เป็นหัวข้อเก่า แต่เนื่องจากผู้คนยังสามารถค้นหาและเชื่อใจได้มันจึงเป็นเรื่องสำคัญที่จะต้องชี้ให้เห็นว่าlock(typeof(SomeObject))แย่กว่าlock(this)อย่างมาก ต้องบอกว่า; ขอแสดงความนับถืออย่างจริงใจต่ออลันสำหรับการชี้ให้เห็นว่าlock(typeof(SomeObject))เป็นการปฏิบัติที่ไม่ดี

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

ดังนั้นlock(this)รูปแบบที่ไม่แข็งแกร่งโดยเฉพาะอาจทำให้เกิดปัญหาและควรเพิ่มคิ้วด้วยเหตุผลทั้งหมดที่อ้างถึง แต่ก็มีการใช้กันอย่างแพร่หลายค่อนข้างเป็นที่เคารพและค่อนข้างเสถียรเช่น log4net ที่ใช้รูปแบบการล็อค (นี้) อย่างกว้างขวางแม้ว่าฉันจะชอบที่จะเห็นรูปแบบการเปลี่ยนแปลง

แต่lock(typeof(SomeObject))เปิดตัวเวิร์มใหม่และปรับปรุงใหม่ทั้งหมด

สำหรับสิ่งที่คุ้มค่า


26

... และอาร์กิวเมนต์เดียวกันที่แน่นอนนำไปใช้กับโครงสร้างนี้เช่นกัน:

lock(typeof(SomeObject))

17
lock (typeof (SomeObject)) จริง ๆ แล้วแย่กว่าล็อค (นี่) ( stackoverflow.com/a/10510647/618649 )
เครก

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

ฉันไม่เห็นด้วยที่lock(this)ดูเหมือนสมเหตุสมผลและรัดกุม มันเป็นการล็อคที่หยาบมากและรหัสอื่น ๆ สามารถล็อควัตถุของคุณซึ่งอาจก่อให้เกิดการรบกวนในรหัสภายใน ใช้การล็อคแบบละเอียดมากขึ้นและใช้การควบคุมที่เข้มงวด สิ่งที่lock(this)เกิดขึ้นคือว่ามันดีกว่าlock(typeof(SomeObject))มาก
เครก

8

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

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

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

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

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

ในทำนองเดียวกันคุณไม่ควรล็อควัตถุ WCF, HttpContext.Current, Thread.Current, Singletons (โดยทั่วไป) ฯลฯ วิธีที่ง่ายที่สุดในการหลีกเลี่ยงปัญหานี้คืออะไร? private [static] object myLock = new object();


3
จริง ๆ แล้วการมีคลาสส่วนตัวไม่ได้ป้องกันปัญหา รหัสภายนอกสามารถรับการอ้างอิงถึงอินสแตนซ์ของคลาสส่วนตัว ...
Rashack

1
@Rackack ในขณะที่คุณถูกต้องทางเทคนิค (+1 สำหรับการชี้ว่า) จุดของฉันคือคุณควรอยู่ในการควบคุมผู้ที่กำลังล็อคอินสแตนซ์ การส่งคืนอินสแตนซ์เช่นนั้นจะทำลายสิ่งนั้น
atlaste

4

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

ลองดูโค้ดนี้เพื่อดูว่าฉันหมายถึงอะไร เพิ่มรหัสต่อไปนี้ลงในโปรแกรมหลักของคุณในแอปพลิเคชันคอนโซล:

    static void Main(string[] args)
    {
         TestThreading();
         Console.ReadLine();
    }

    public static void TestThreading()
    {
        Random rand = new Random();
        Thread[] threads = new Thread[10];
        TestLock.balance = 100000;
        for (int i = 0; i < 10; i++)
        {
            TestLock tl = new TestLock();
            Thread t = new Thread(new ThreadStart(tl.WithdrawAmount));
            threads[i] = t;
        }
        for (int i = 0; i < 10; i++)
        {
            threads[i].Start();
        }
        Console.Read();
    }

สร้างคลาสใหม่เหมือนด้านล่าง

 class TestLock
{
    public static int balance { get; set; }
    public static readonly Object myLock = new Object();

    public void Withdraw(int amount)
    {
      // Try both locks to see what I mean
      //             lock (this)
       lock (myLock)
        {
            Random rand = new Random();
            if (balance >= amount)
            {
                Console.WriteLine("Balance before Withdrawal :  " + balance);
                Console.WriteLine("Withdraw        : -" + amount);
                balance = balance - amount;
                Console.WriteLine("Balance after Withdrawal  :  " + balance);
            }
            else
            {
                Console.WriteLine("Can't process your transaction, current balance is :  " + balance + " and you tried to withdraw " + amount);
            }
        }

    }
    public void WithdrawAmount()
    {
        Random rand = new Random();
        Withdraw(rand.Next(1, 100) * 100);
    }
}

นี่คือการทำงานของล็อคโปรแกรมนี้

   Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  94400
    Balance before Withdrawal :  100000
    Balance before Withdrawal :  100000
    Withdraw        : -5600
    Balance after Withdrawal  :  88800
    Withdraw        : -5600
    Balance after Withdrawal  :  83200
    Balance before Withdrawal :  83200
    Withdraw        : -9100
    Balance after Withdrawal  :  74100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance before Withdrawal :  74100
    Withdraw        : -9100
    Balance after Withdrawal  :  55900
    Balance after Withdrawal  :  65000
    Balance before Withdrawal :  55900
    Withdraw        : -9100
    Balance after Withdrawal  :  46800
    Balance before Withdrawal :  46800
    Withdraw        : -2800
    Balance after Withdrawal  :  44000
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  41200
    Balance before Withdrawal :  44000
    Withdraw        : -2800
    Balance after Withdrawal  :  38400

นี่คือการทำงานของโปรแกรมล็อคmyLock

Balance before Withdrawal :  100000
Withdraw        : -6600
Balance after Withdrawal  :  93400
Balance before Withdrawal :  93400
Withdraw        : -6600
Balance after Withdrawal  :  86800
Balance before Withdrawal :  86800
Withdraw        : -200
Balance after Withdrawal  :  86600
Balance before Withdrawal :  86600
Withdraw        : -8500
Balance after Withdrawal  :  78100
Balance before Withdrawal :  78100
Withdraw        : -8500
Balance after Withdrawal  :  69600
Balance before Withdrawal :  69600
Withdraw        : -8500
Balance after Withdrawal  :  61100
Balance before Withdrawal :  61100
Withdraw        : -2200
Balance after Withdrawal  :  58900
Balance before Withdrawal :  58900
Withdraw        : -2200
Balance after Withdrawal  :  56700
Balance before Withdrawal :  56700
Withdraw        : -2200
Balance after Withdrawal  :  54500
Balance before Withdrawal :  54500
Withdraw        : -500
Balance after Withdrawal  :  54000

1
สิ่งที่ควรทราบในตัวอย่างของคุณคือสิ่งที่คุณแสดงซึ่งไม่ถูกต้อง มันยากที่จะสังเกตเห็นว่ามีอะไรผิดปกติเมื่อคุณใช้Random rand = new Random();nvm ฉันคิดว่าฉันเห็นยอดคงเหลือที่เกิดซ้ำ
Seabizkit

3

มีบทความที่ดีมากเกี่ยวกับเรื่องนี้http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objectsโดย Rico Mariani สถาปนิกด้านประสิทธิภาพสำหรับรันไทม์Microsoft® .NET

ข้อความที่ตัดตอนมา:

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



2

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

class SomeClass
{
    public void SomeMethod(int id)
    {
        **lock(this)**
        {
            while(true)
            {
                Console.WriteLine("SomeClass.SomeMethod #" + id);
            }
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        SomeClass o = new SomeClass();

        lock(o)
        {
            for (int threadId = 0; threadId < 3; threadId++)
            {
                Thread t = new Thread(() => {
                    o.SomeMethod(threadId);
                        });
                t.Start();
            }

            Console.WriteLine();
        }

ในการหลีกเลี่ยงผู้ชายคนนี้ใช้ Thread.TryMonitor (หมดเวลา) แทนที่จะล็อค:

            Monitor.TryEnter(temp, millisecondsTimeout, ref lockWasTaken);
            if (lockWasTaken)
            {
                doAction();
            }
            else
            {
                throw new Exception("Could not get lock");
            }

https://blogs.appbeat.io/post/c-how-to-lock-without-deadlocks


เท่าที่ฉันเห็นเมื่อฉันแทนที่ล็อค (นี้) ด้วยการล็อคสมาชิกอินสแตนซ์ส่วนตัวของSomeClassฉันยังคงได้รับการหยุดชะงักเดียวกัน นอกจากนี้หากการล็อกในคลาสหลักเสร็จสิ้นในสมาชิกอินสแตนซ์ส่วนตัวอื่นของโปรแกรมการล็อกเดียวกันจะเกิดขึ้น ดังนั้นไม่แน่ใจว่าคำตอบนี้ไม่ทำให้เข้าใจผิดและไม่ถูกต้อง ดูพฤติกรรมนั้นได้ที่นี่: dotnetfiddle.net/DMrU5h
Bartosz

1

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

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


1

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

  • คุณไม่สามารถยกเลิกการหยุดชะงักโดยไม่ขัดจังหวะหนึ่งในเธรดดังนั้นหลังจากคุณเข้าสู่การหยุดชะงักคุณจะไม่สามารถออกไปได้
  • การอดอาหารจะสิ้นสุดลงโดยอัตโนมัติหลังจากที่หนึ่งในเธรดเสร็จสิ้น

นี่คือภาพที่แสดงให้เห็นถึงความแตกต่าง

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


9
มีความแตกต่าง แต่ทั้งหมดไม่เกี่ยวข้องกับการสนทนานี้ และประโยคแรกของข้อสรุปของคุณไม่ถูกต้อง
Ben Voigt

1
เพื่อความชัดเจน: ฉันไม่ได้ป้องกันlock(this)- รหัสประเภทนี้ผิดปกติ ฉันคิดว่าการเรียกมันว่าการหยุดชะงักนั้นเป็นสิ่งที่ไม่เหมาะสม
SOReader

2
ลิงก์ไปยังรูปภาพไม่สามารถใช้ได้อีก :( โอกาสใดที่คุณสามารถอ้างอิงได้อีกครั้งขอบคุณ
VG1

1

โปรดอ้างอิงลิงค์ต่อไปนี้ซึ่งอธิบายว่าทำไมการล็อก (นี่) ไม่ใช่ความคิดที่ดี

http://blogs.msdn.com/b/bclteam/archive/2004/01/20/60719.aspx

ดังนั้นวิธีแก้ไขคือการเพิ่มอ็อบเจกต์ส่วนตัวเช่น lockObject ให้กับคลาสและวางขอบเขตของโค้ดไว้ในคำสั่ง lock ดังที่แสดงด้านล่าง:

lock (lockObject)
{
...
}

ลิงก์ไม่ถูกต้องอีกต่อไป
Rauld

1

นี่คือตัวอย่างโค้ดที่ง่ายต่อการติดตาม (IMO): (จะทำงานในLinqPadอ้างอิงเนมสเปซต่อไปนี้: System.Net และ System.Threading.Tasks)

สิ่งที่ต้องจำคือ lock (x) โดยทั่วไปคือน้ำตาล syntactic และสิ่งที่มันคือการใช้ Monitor.Enter แล้วใช้ลองจับและสุดท้ายบล็อกเพื่อเรียก Monitor.Exit ดู: https://docs.microsoft.com/en-us/dotnet/api/system.threading.monitor.enter (ส่วนข้อสังเกต)

หรือใช้คำสั่งการล็อก C # (คำสั่ง SyncLock ใน Visual Basic) ซึ่งล้อมวิธีการป้อนและออกในการลอง ... ในที่สุดก็ปิดกั้น

void Main()
{
    //demonstrates why locking on THIS is BADD! (you should never lock on something that is publicly accessible)
    ClassTest test = new ClassTest();
    lock(test) //locking on the instance of ClassTest
    {
        Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        Parallel.Invoke(new Action[]
        {
            () => {
                //this is there to just use up the current main thread. 
                Console.WriteLine($"CurrentThread {Thread.CurrentThread.ManagedThreadId}");
                },
            //none of these will enter the lock section.
            () => test.DoWorkUsingThisLock(1),//this will dead lock as lock(x) uses Monitor.Enter
            () => test.DoWorkUsingMonitor(2), //this will not dead lock as it uses Montory.TryEnter
        });
    }
}

public class ClassTest
{
    public void DoWorkUsingThisLock(int i)
    {
        Console.WriteLine($"Start ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        lock(this) //this can be bad if someone has locked on this already, as it will cause it to be deadlocked!
        {
            Console.WriteLine($"Running: ClassTest.DoWorkUsingThisLock {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
        }
        Console.WriteLine($"End ClassTest.DoWorkUsingThisLock Done {i}  CurrentThread {Thread.CurrentThread.ManagedThreadId}");
    }

    public void DoWorkUsingMonitor(int i)
    {
        Console.WriteLine($"Start ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        if (Monitor.TryEnter(this))
        {
            Console.WriteLine($"Running: ClassTest.DoWorkUsingMonitor {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
            Monitor.Exit(this);
        }
        else
        {
            Console.WriteLine($"Skipped lock section!  {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        }

        Console.WriteLine($"End ClassTest.DoWorkUsingMonitor Done {i} CurrentThread {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine();
    }
}

เอาท์พุต

CurrentThread 15
CurrentThread 15
Start ClassTest.DoWorkUsingMonitor 2 CurrentThread 13
Start ClassTest.DoWorkUsingThisLock 1 CurrentThread 12
Skipped lock section!  2 CurrentThread 13
End ClassTest.DoWorkUsingMonitor Done 2 CurrentThread 13

ขอให้สังเกตว่าเธรด # 12 จะไม่สิ้นสุดลงเมื่อล็อคตาย


1
ดูเหมือนว่าDoWorkUsingThisLockหัวข้อที่สองไม่จำเป็นต้องแสดงให้เห็นปัญหาหรือไม่
Jack Lu

คุณไม่ได้หมายถึงตัวล็อค outter ใน main หนึ่งเธรดจะรอให้อีกตัวหนึ่งเสร็จสมบูรณ์หรือไม่ ซึ่งจะทำให้
โมเมนตัม

@Seabizkit อัปเดตรหัสเพื่อให้ชัดเจนขึ้นเล็กน้อย Parallel มีเพียงเพื่อสร้างเธรดใหม่และรันโค้ดแบบอะซิงโครนัส ในความเป็นจริงเธรดที่ 2 อาจถูกเรียกใช้โดยวิธีใด ๆ (คลิกปุ่ม, แยกคำขอ ฯลฯ )
Raj Rao

0

คุณสามารถสร้างกฎที่ระบุว่าคลาสสามารถมีรหัสที่ล็อคใน 'this' หรือวัตถุใด ๆ ที่รหัสในคลาส instantiates ดังนั้นจึงเป็นปัญหาหากไม่ได้ติดตามรูปแบบ

หากคุณต้องการป้องกันตัวเองจากรหัสที่ไม่เป็นไปตามรูปแบบนี้แสดงว่าคำตอบที่ยอมรับนั้นถูกต้อง แต่ถ้าเป็นไปตามรูปแบบก็ไม่เป็นปัญหา

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

เมื่อคุณไม่สนใจจำนวนการจัดสรรหรือหน่วยความจำการหลีกเลี่ยงการล็อค (นี้) จะดีกว่าด้วยเหตุผลที่ระบุไว้ในคำตอบอื่น ๆ


-1

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


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