Tại sao khóa (này) {'}} xấu?


484

Các tài liệu MSDN nói rằng

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

là "một vấn đề nếu thể hiện có thể được truy cập công khai". Tôi đang tự hỏi tại sao? Có phải vì khóa sẽ được giữ lâu hơn cần thiết? Hoặc có một số lý do ngấm ngầm hơn?

Câu trả lời:


508

Nó là hình thức xấu để sử dụng thistrong các câu lệnh khóa bởi vì nó thường nằm ngoài tầm kiểm soát của bạn, những người khác có thể đang khóa đối tượng đó.

Để lập kế hoạch đúng cho các hoạt động song song, cần đặc biệt chú ý đến các tình huống bế tắc có thể xảy ra và việc có một số điểm nhập cảnh khóa không xác định cản trở điều này. Ví dụ, bất kỳ ai có tham chiếu đến đối tượng đều có thể khóa nó mà không cần người thiết kế / người tạo đối tượng biết về nó. Điều này làm tăng sự phức tạp của các giải pháp đa luồng và có thể ảnh hưởng đến tính chính xác của chúng.

Một trường riêng thường là một tùy chọn tốt hơn vì trình biên dịch sẽ thực thi các hạn chế truy cập đối với nó và nó sẽ đóng gói cơ chế khóa. Sử dụng thisvi phạm đóng gói bằng cách phơi bày một phần việc triển khai khóa của bạn ra công chúng. Nó cũng không rõ ràng rằng bạn sẽ có được một khóa trên thistrừ khi nó đã được ghi nhận. Ngay cả sau đó, dựa vào tài liệu để ngăn chặn một vấn đề là tối ưu.

Cuối cùng, có một quan niệm sai lầm phổ biến lock(this)thực sự sửa đổi đối tượng được truyền dưới dạng tham số và theo một cách nào đó làm cho nó chỉ đọc hoặc không thể truy cập được. Điều này là sai . Đối tượng được truyền dưới dạng tham số để lockchỉ đóng vai trò là khóa . Nếu một khóa đã được giữ trên phím đó, khóa không thể được thực hiện; nếu không, khóa được cho phép.

Đây là lý do tại sao thật tệ khi sử dụng các chuỗi làm khóa trong các lockcâu lệnh, vì chúng không thay đổi và được chia sẻ / truy cập trên các phần của ứng dụng. Bạn nên sử dụng một biến riêng tư thay vào đó, một Objectthể hiện sẽ làm tốt.

Chạy mã C # sau đây làm ví dụ.

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

Bảng điều khiển đầu ra

'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
Khi tôi mò mẫm: (1) Nancy đang ở trong chủ đề1 có khóa (cái này). (2) CÙNG Nancy đang trong quá trình lão hóa thread2 trong khi vẫn bị khóa trong thread1 - chứng minh một đối tượng bị khóa không chỉ đọc. CSONG (2a) trong khi ở luồng 2, đối tượng Nancy này cũng bị khóa trên Tên. (3) Tạo một đối tượng KHÁC BIỆT có cùng tên . (4) Chuyển vào thread3 và cố gắng khóa với Tên. (kết thúc lớn) NHƯNG "chuỗi là bất biến" có nghĩa là bất kỳ đối tượng nào tham chiếu chuỗi "Nancy Drew" đang xem xét theo cùng một thể hiện chuỗi trong bộ nhớ. Vì vậy, object2 không thể có được khóa trên một chuỗi khi object1 bị khóa trên cùng một giá trị
radarbob

Sử dụng một biến tiêu chuẩn thay vì lock(this)là lời khuyên tiêu chuẩn; Điều quan trọng cần lưu ý là làm như vậy thường sẽ khiến mã bên ngoài không thể khiến khóa được liên kết với đối tượng bị giữ giữa các lệnh gọi phương thức. Điều này có thể hoặc không thể là một điều tốt . Có một số nguy hiểm trong việc cho phép mã bên ngoài giữ khóa trong thời gian tùy ý và các lớp thường được thiết kế để làm cho việc sử dụng đó không cần thiết, nhưng luôn không có sự thay thế thực tế. Một ví dụ đơn giản, trừ khi một bộ sưu tập thực hiện một ToArrayhoặc ToListphương pháp của riêng nó ...
supercat

4
(trái ngược với các phương thức mở rộng `IEnumerable <T>), cách duy nhất cho một chủ đề muốn có một ảnh chụp nhanh của bộ sưu tập để có được một cái có thể là liệt kê nó trong khi khóa tất cả các thay đổi . Để làm điều đó, nó phải có quyền truy cập vào một khóa được lấy bởi bất kỳ mã nào sẽ thay đổi bộ sưu tập. Việc không để lộ khóa có thể khiến chương trình không thể thực hiện định kỳ một ảnh chụp nhanh không đồng bộ của bộ sưu tập (ví dụ: để cập nhật giao diện người dùng duyệt qua bộ sưu tập).
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- Tôi tin rằng các cuộc thảo luận đó là về bit SyncBlock trong đối tượng CLR vì vậy chính thức đây là chính xác - khóa đối tượng đã sửa đổi
sll

@Esteban, tôi hoàn toàn thích ví dụ của bạn, nó thật tuyệt vời. Tôi có một câu hỏi dành cho bạn. Mã của phương thức NameChange (..) kết thúc bằng: <code> if (Monitor.Try Entry (person.Name, 10000)) {. . . } khác Monitor.Exit (person.Name); </ code> Không nên kết thúc bằng: <code> if (Monitor.Try Entry (person.Name, 10000)) {. . . Monitor.Exit (người. Tên); } </ code>
AviFarah

64

Bởi vì nếu mọi người có thể lấy thiscon trỏ đối tượng (ví dụ: con trỏ) của bạn , thì họ cũng có thể cố gắng khóa cùng đối tượng đó. Bây giờ họ có thể không nhận thức được rằng bạn đang khóa thisnội bộ, vì vậy điều này có thể gây ra sự cố (có thể là bế tắc)

Thêm vào đó, đây cũng là một thực tế tồi tệ, bởi vì nó khóa "quá nhiều"

Ví dụ: bạn có thể có một biến thành viên List<int>và điều duy nhất bạn thực sự cần khóa là biến thành viên đó. Nếu bạn khóa toàn bộ đối tượng trong các chức năng của mình, thì những thứ khác gọi các chức năng đó sẽ bị chặn chờ khóa. Nếu các chức năng đó không cần truy cập vào danh sách thành viên, bạn sẽ khiến các mã khác phải chờ và làm chậm ứng dụng của bạn mà không có lý do nào cả.


44
Đoạn cuối của câu trả lời này là không chính xác. Khóa không theo bất kỳ cách nào làm cho đối tượng không thể đọc được hoặc chỉ đọc. Khóa (cái này) không ngăn luồng khác gọi hoặc sửa đổi đối tượng được tham chiếu bởi cái này.
Esteban Brenes

3
Nó thực hiện nếu các phương thức khác được gọi cũng thực hiện khóa (cái này). Tôi tin rằng đó là điểm anh ấy đang làm. Lưu ý "Nếu bạn khóa toàn bộ đối tượng trong chức năng của mình" ...
Herms

@ Tổ chức: Điều đó rõ ràng hơn. @Herms: Vâng, nhưng bạn không cần sử dụng 'this' để đạt được chức năng đó, ví dụ, thuộc tính SyncRoot trong danh sách phục vụ mục đích đó, trong khi làm cho việc đồng bộ hóa rõ ràng nên được thực hiện trên khóa đó.
Esteban Brenes

Re: khóa "quá nhiều": Đó là một hành động cân bằng tốt quyết định những gì để khóa. Xin lưu ý rằng việc khóa liên quan đến các hoạt động CPU xóa bộ nhớ cache và hơi tốn kém. Nói cách khác: không khóa và cập nhật từng số nguyên riêng lẻ. :)
Zan Lynx

Đoạn cuối vẫn không có ý nghĩa. Nếu bạn chỉ cần hạn chế quyền truy cập vào danh sách, tại sao các chức năng khác sẽ có khóa nếu họ không truy cập danh sách?
Joakim MH

44

Hãy xem phần Đồng bộ hóa chủ đề MSDN (Hướng dẫn lập trình C #)

Nói chung, tốt nhất là tránh khóa trên một loại công khai hoặc trên các trường hợp đối tượng nằm ngoài sự kiểm soát của ứng dụng của bạn. Ví dụ, khóa (cái này) có thể có vấn đề nếu thể hiện có thể được truy cập công khai, vì mã ngoài tầm kiểm soát của bạn cũng có thể khóa trên đối tượng. Điều này có thể tạo ra các tình huống bế tắc trong đó hai hoặc nhiều luồng chờ phát hành cùng một đối tượng. Khóa trên một loại dữ liệu công khai, trái ngược với một đối tượng, có thể gây ra vấn đề với cùng một lý do. Việc khóa các chuỗi ký tự đặc biệt rủi ro vì các chuỗi ký tự được tập trung bởi thời gian chạy ngôn ngữ chung (CLR). Điều này có nghĩa là có một phiên bản của bất kỳ chuỗi ký tự nào cho toàn bộ chương trình, cùng một đối tượng chính xác đại diện cho nghĩa đen trong tất cả các miền ứng dụng đang chạy, trên tất cả các luồng. Kết quả là, một khóa được đặt trên một chuỗi có cùng nội dung ở bất kỳ đâu trong quy trình ứng dụng sẽ khóa tất cả các phiên bản của chuỗi đó trong ứng dụng. Do đó, tốt nhất là khóa một thành viên tư nhân hoặc được bảo vệ mà không được thực tập. Một số lớp cung cấp các thành viên đặc biệt để khóa. Kiểu Array, ví dụ, cung cấp SyncRoot. Nhiều loại bộ sưu tập cũng cung cấp một thành viên SyncRoot.


34

Tôi biết đây là một chủ đề cũ, nhưng bởi vì mọi người vẫn có thể tìm kiếm nó và dựa vào nó, điều quan trọng là chỉ ra rằng điều đó lock(typeof(SomeObject))còn tồi tệ hơn đáng kể lock(this). Có nói rằng; danh tiếng chân thành với Alan vì đã chỉ ra rằng đó lock(typeof(SomeObject))là thực hành xấu.

Một ví dụ System.Typelà một trong những đối tượng chung, thô nhất có. Ít nhất, một phiên bản của System.Type là toàn cầu đối với một AppDomain và .NET có thể chạy nhiều chương trình trong một AppDomain. Điều này có nghĩa là hai chương trình hoàn toàn khác nhau có thể có khả năng gây nhiễu lẫn nhau thậm chí đến mức tạo ra bế tắc nếu cả hai đều cố gắng khóa đồng bộ hóa trên cùng một thể loại.

Vì vậy, lock(this)hình thức không đặc biệt mạnh mẽ, có thể gây ra vấn đề và phải luôn luôn nhướn mày vì tất cả các lý do được trích dẫn. Tuy nhiên, có mã được sử dụng rộng rãi, tương đối tốt và rõ ràng là mã ổn định như log4net sử dụng rộng rãi mẫu khóa này (mặc dù tôi muốn thấy thay đổi mẫu đó).

Nhưng lock(typeof(SomeObject))mở ra một lon giun hoàn toàn mới và được tăng cường.

Cho những gì nó có giá trị.


26

... Và các đối số chính xác cũng áp dụng cho cấu trúc này:

lock(typeof(SomeObject))

17
khóa (typeof (someObject)) thực sự tệ hơn rất nhiều so với khóa (cái này) ( stackoverflow.com/a/10510647/618649 ).
Craig

1
tốt, khóa (Application.C Hiện tại) thậm chí còn tồi tệ hơn, nhưng ai sẽ thử một trong những điều ngu ngốc này? khóa (cái này) có vẻ hợp lý và gọn gàng, nhưng những ví dụ khác thì không.
Zar Shardan

Tôi không đồng ý rằng lock(this)có vẻ đặc biệt hợp lý và cô đọng. Đó là một khóa cực kỳ thô và bất kỳ mã nào khác có thể khóa đối tượng của bạn, có khả năng gây nhiễu cho mã nội bộ của bạn. Lấy các khóa hạt nhiều hơn, và giả định kiểm soát chặt chẽ hơn. Những gì lock(this)đã xảy ra cho nó là nó tốt hơn nhiều lock(typeof(SomeObject)).
Craig

8

Hãy tưởng tượng rằng bạn có một thư ký lành nghề tại văn phòng của bạn, đó là một tài nguyên được chia sẻ trong bộ phận. Thỉnh thoảng, bạn lao về phía họ vì bạn có một nhiệm vụ, chỉ hy vọng rằng một đồng nghiệp khác của bạn chưa yêu cầu họ. Thông thường bạn chỉ phải chờ trong một khoảng thời gian ngắn.

Vì sự quan tâm là chia sẻ, người quản lý của bạn quyết định rằng khách hàng cũng có thể sử dụng thư ký trực tiếp. Nhưng điều này có tác dụng phụ: Một khách hàng thậm chí có thể yêu cầu họ trong khi bạn làm việc cho khách hàng này và bạn cũng cần họ thực hiện một phần nhiệm vụ. Một bế tắc xảy ra, bởi vì tuyên bố không còn là một hệ thống phân cấp. Điều này có thể tránh được tất cả cùng nhau bằng cách không cho phép khách hàng yêu cầu họ ngay từ đầu.

lock(this)là xấu như chúng ta đã thấy. Một đối tượng bên ngoài có thể khóa đối tượng và vì bạn không kiểm soát ai đang sử dụng lớp, bất kỳ ai cũng có thể khóa đối tượng đó ... Đó là ví dụ chính xác như được mô tả ở trên. Một lần nữa, giải pháp là hạn chế tiếp xúc với đối tượng. Tuy nhiên, nếu bạn có mộtprivate , protectedhoặc internallớp học mà bạn đã có thể kiểm soát những người được khóa trên đối tượng của bạn , bởi vì bạn chắc chắn rằng bạn đã viết mã của bạn mình. Vì vậy, thông điệp ở đây là: đừng phơi bày nó như public. Ngoài ra, đảm bảo rằng khóa được sử dụng trong trường hợp tương tự để tránh bế tắc.

Trái ngược hoàn toàn với điều này là khóa các tài nguyên được chia sẻ trên toàn miền ứng dụng - trường hợp xấu nhất. Nó giống như đưa thư ký của bạn ra bên ngoài và cho phép mọi người ra khỏi đó để yêu cầu họ. Kết quả là sự hỗn loạn hoàn toàn - hoặc về mặt mã nguồn: đó là một ý tưởng tồi; vứt nó đi và bắt đầu lại. Vì vậy, làm thế nào để chúng tôi làm điều đó?

Các loại được chia sẻ trong miền ứng dụng như hầu hết mọi người ở đây chỉ ra. Nhưng có những thứ thậm chí còn tốt hơn chúng ta có thể sử dụng: chuỗi. Lý do là các chuỗi được gộp lại . Nói cách khác: nếu bạn có hai chuỗi có cùng nội dung trong một miền ứng dụng, có khả năng chúng có cùng một con trỏ. Vì con trỏ được sử dụng làm khóa khóa, nên về cơ bản, bạn nhận được một từ đồng nghĩa với "chuẩn bị cho hành vi không xác định".

Tương tự, bạn không nên khóa trên các đối tượng WCF, HttpContext.C Hiện tại, Thread.C Hiện tại, Singletons (nói chung), v.v ... Cách dễ nhất để tránh tất cả những điều này? private [static] object myLock = new object();


3
Trên thực tế có một lớp học riêng không ngăn chặn vấn đề. Mã bên ngoài có thể có được một tham chiếu đến một thể hiện của một lớp riêng ...
Rashack

1
@Rashack trong khi bạn đúng về mặt kỹ thuật (+1 để chỉ ra điều đó), quan điểm của tôi là bạn nên kiểm soát ai đang khóa ví dụ. Trả lại các trường hợp như thế phá vỡ điều đó.
đúng

4

Khóa trên con trỏ này có thể là xấu nếu bạn đang khóa tài nguyên được chia sẻ . Tài nguyên được chia sẻ có thể là một biến tĩnh hoặc một tệp trên máy tính của bạn - tức là một thứ được chia sẻ giữa tất cả người dùng của lớp. Lý do là con trỏ này sẽ chứa một tham chiếu khác đến một vị trí trong bộ nhớ mỗi khi lớp của bạn được khởi tạo. Vì vậy, khóa cái này trong một lần của một lớp khác với việc khóa hơn cái này trong một thể hiện khác của một lớp.

Kiểm tra mã này để xem những gì tôi có ý nghĩa. Thêm mã sau vào chương trình chính của bạn trong ứng dụng Console:

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

Tạo một lớp mới như dưới đây.

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

Đây là một chương trình khóa về điều này .

   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

Đây là một chương trình khóa trên 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
điều cần lưu ý trong ví dụ của bạn, như những gì bạn thể hiện không đúng. Thật khó để nhận ra điều gì sai khi bạn sử dụng Random rand = new Random();nvm Tôi nghĩ rằng tôi thấy Số dư lặp lại của nó
Seabizkit

3

Có một bài viết rất hay về nó http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects của Rico Mariani, kiến ​​trúc sư hiệu suất cho thời gian chạy Microsoft® .NET

Trích đoạn:

Vấn đề cơ bản ở đây là bạn không sở hữu đối tượng loại và bạn không biết ai khác có thể truy cập nó. Nói chung, việc dựa vào việc khóa một đối tượng bạn không tạo và không biết ai khác có thể truy cập là một ý tưởng rất tồi. Làm như vậy mời bế tắc. Cách an toàn nhất là chỉ khóa các đối tượng riêng tư.



2

Đây là một minh họa đơn giản hơn nhiều (lấy từ Câu hỏi 34 ở đây ) tại sao khóa (cái này) xấu và có thể dẫn đến bế tắc khi người tiêu dùng của lớp bạn cũng cố gắng khóa đối tượng. Dưới đây, chỉ một trong ba luồng có thể tiến hành, hai luồng còn lại bị bế tắc.

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

Để giải quyết, anh chàng này đã sử dụng Thread.TryMonitor (có thời gian chờ) thay vì khóa:

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

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


Theo như tôi thấy, khi tôi thay thế khóa (cái này) bằng khóa trên một thành viên cá nhân của SomeClass, tôi vẫn gặp bế tắc tương tự. Ngoài ra, nếu khóa trong lớp chính được thực hiện trên một thành viên cá nhân khác của Chương trình, thì khóa tương tự sẽ xảy ra. Vì vậy, không chắc chắn nếu câu trả lời này không sai lệch và không chính xác. Xem hành vi đó tại đây: dotnetfiddle.net/DMrU5h
Bartosz

1

Bởi vì bất kỳ đoạn mã nào có thể thấy thể hiện của lớp bạn cũng có thể khóa trên tham chiếu đó. Bạn muốn ẩn (đóng gói) đối tượng khóa của mình để chỉ mã cần tham chiếu nó mới có thể tham chiếu nó. Từ khóa này đề cập đến thể hiện của lớp hiện tại, vì vậy bất kỳ số lượng nào có thể có tham chiếu đến nó và có thể sử dụng nó để thực hiện đồng bộ hóa luồng.

Để rõ ràng, điều này là xấu vì một số đoạn mã khác có thể sử dụng thể hiện của lớp để khóa và có thể ngăn mã của bạn có được khóa kịp thời hoặc có thể tạo ra các vấn đề đồng bộ hóa luồng khác. Trường hợp tốt nhất: không có gì khác sử dụng một tham chiếu đến lớp của bạn để khóa. Trường hợp giữa: một cái gì đó sử dụng một tham chiếu đến lớp của bạn để thực hiện các khóa và nó gây ra vấn đề về hiệu suất. Trường hợp xấu nhất: một cái gì đó sử dụng một tài liệu tham khảo của lớp của bạn để thực hiện các khóa và nó gây ra các vấn đề thực sự tồi tệ, thực sự tinh tế, thực sự khó gỡ lỗi.


1

Xin lỗi các bạn nhưng tôi không thể đồng ý với lập luận rằng việc khóa này có thể gây ra bế tắc. Bạn đang nhầm lẫn hai điều: bế tắc và đói khát.

  • Bạn không thể hủy bỏ bế tắc mà không làm gián đoạn một trong các chủ đề để sau khi bạn rơi vào bế tắc, bạn không thể thoát ra được
  • Đói sẽ tự động kết thúc sau khi một trong các chủ đề hoàn thành công việc của nó

Đây là một hình ảnh minh họa sự khác biệt.

Kết luận
Bạn vẫn có thể sử dụng một cách an toàn lock(this)nếu việc bỏ đói luồng không phải là vấn đề đối với bạn. Bạn vẫn phải lưu ý rằng khi luồng đang bỏ đói sử dụng lock(this)kết thúc trong một khóa có đối tượng của bạn bị khóa, cuối cùng nó sẽ kết thúc trong tình trạng đói vĩnh cửu;)


9
Có một sự khác biệt nhưng nó hoàn toàn không liên quan đến cuộc thảo luận này. Và câu đầu tiên của kết luận của bạn là sai.
Ben Voigt

1
Để rõ ràng: Tôi không bảo vệ lock(this)- loại mã này đơn giản là sai. Tôi chỉ nghĩ gọi nó là bế tắc là một chút lạm dụng.
Chủ tịch

2
Liên kết đến hình ảnh không còn có sẵn. :( Bất kỳ cơ hội nào bạn có thể tham khảo lại? Thx
VG1


1

Dưới đây là một số mã mẫu đơn giản hơn để làm theo (IMO): (Sẽ hoạt động trong LinqPad , tham khảo các không gian tên sau: System.Net và System.Threading.T Nhiệm vụ)

Một điều cần nhớ là khóa (x) về cơ bản là đường cú pháp và những gì nó làm là sử dụng Monitor. Entry và sau đó sử dụng thử, bắt, cuối cùng là chặn để gọi Monitor.Exit. Xem: https://docs.microsoft.com/en-us/dotnet/api/system.threading.monitor.enter (phần nhận xét)

hoặc sử dụng câu lệnh khóa C # (câu lệnh SyncLock trong Visual Basic), nó bao bọc các phương thức Enter và Exit trong một khối thử cuối cùng.

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

Đầu ra

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

Lưu ý rằng Chủ đề # 12 không bao giờ kết thúc khi khóa chết của nó.


1
dường như DoWorkUsingThisLockchủ đề thứ hai là không cần thiết để minh họa vấn đề?
Jack Lu

không phải bạn có nghĩa là khóa bên ngoài trong chính, một chủ đề sẽ chỉ đơn giản là chờ cho người khác hoàn thành? mà sau đó sẽ làm mất hiệu lực Song song ... tôi cảm thấy chúng ta cần những ví dụ thực tế tốt hơn ..
Seabizkit

@Seabizkit, đã cập nhật mã để làm cho nó rõ ràng hơn một chút. Parallel ở đó chỉ để tạo một luồng mới và chạy mã không đồng bộ. Trong thực tế, luồng thứ 2 có thể đã được gọi bằng bất kỳ số cách nào (nhấp vào nút, yêu cầu riêng biệt, v.v.).
Raj Rao

0

Bạn có thể thiết lập một quy tắc nói rằng một lớp có thể có mã khóa trên 'this' hoặc bất kỳ đối tượng nào mà mã trong lớp bắt đầu. Vì vậy, nó chỉ là một vấn đề nếu mô hình không được tuân theo.

Nếu bạn muốn bảo vệ bản thân khỏi mã không tuân theo mẫu này, thì câu trả lời được chấp nhận là chính xác. Nhưng nếu mô hình được tuân theo, nó không phải là một vấn đề.

Ưu điểm của khóa (cái này) là hiệu quả. Điều gì xảy ra nếu bạn có một "đối tượng giá trị" đơn giản chứa một giá trị duy nhất. Nó chỉ là một trình bao bọc, và nó được khởi tạo hàng triệu lần. Bằng cách yêu cầu tạo một đối tượng đồng bộ hóa riêng chỉ để khóa, về cơ bản, bạn đã nhân đôi kích thước của đối tượng và nhân đôi số lượng phân bổ. Khi hiệu suất quan trọng, đây là một lợi thế.

Khi bạn không quan tâm đến số lượng phân bổ hoặc dấu chân bộ nhớ, tránh khóa (điều này) là thích hợp hơn cho các lý do được chỉ ra trong các câu trả lời khác.


-1

Sẽ có một vấn đề nếu thể hiện có thể được truy cập công khai vì có thể có các yêu cầu khác có thể đang sử dụng cùng một thể hiện đối tượng. Tốt hơn là sử dụng biến riêng tư / tĩnh.


5
Không chắc chắn những gì thêm vào người đàn ông, câu trả lời chi tiết đã tồn tại mà nói điều tương tự.
Andrew Barber
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.