Cách tốt nhất để triển khai Từ điển an toàn theo luồng là gì?


109

Tôi đã có thể triển khai Từ điển an toàn luồng trong C # bằng cách lấy từ IDictionary và xác định đối tượng SyncRoot riêng tư:

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...
}

Sau đó, tôi khóa đối tượng SyncRoot này trong suốt người tiêu dùng của tôi (nhiều chủ đề):

Thí dụ:

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

Tôi đã có thể làm cho nó hoạt động, nhưng điều này dẫn đến một số mã xấu xí. Câu hỏi của tôi là, có cách nào tốt hơn, thanh lịch hơn để triển khai Từ điển an toàn luồng không?


3
Bạn thấy nó xấu xí là gì?
Asaf R

1
Tôi nghĩ anh ấy đang đề cập đến tất cả các câu lệnh khóa mà anh ấy có trong suốt mã của mình trong người tiêu dùng của lớp SharedDictionary - anh ấy đang khóa mã gọi mỗi khi anh ấy truy cập một phương thức trên một đối tượng SharedDictionary.
Peter Meyer

Thay vì sử dụng phương thức Thêm, hãy thử thực hiện bằng cách gán các giá trị ex- m_MySharedDictionary ["key1"] = "item1", đây là chuỗi an toàn.
testuser

Câu trả lời:


43

Như Peter đã nói, bạn có thể đóng gói tất cả các luồng an toàn bên trong lớp. Bạn sẽ cần phải cẩn thận với bất kỳ sự kiện nào bạn để lộ hoặc thêm vào, đảm bảo rằng chúng được gọi ra bên ngoài bất kỳ ổ khóa nào.

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...
}

Chỉnh sửa: Tài liệu MSDN chỉ ra rằng việc liệt kê vốn dĩ không an toàn cho chuỗi. Đó có thể là một lý do để lộ một đối tượng đồng bộ hóa bên ngoài lớp của bạn. Một cách khác để tiếp cận đó là cung cấp một số phương pháp để thực hiện một hành động trên tất cả các thành viên và khóa quanh việc liệt kê các thành viên. Vấn đề với điều này là bạn không biết liệu hành động được chuyển đến hàm đó có gọi thành viên nào đó trong từ điển của bạn hay không (điều đó sẽ dẫn đến bế tắc). Việc để lộ đối tượng đồng bộ hóa cho phép người dùng đưa ra những quyết định đó và không che giấu sự bế tắc bên trong lớp của bạn.


@fryguybob: Việc liệt kê thực sự là lý do duy nhất khiến tôi để lộ đối tượng đồng bộ hóa. Theo quy ước, tôi sẽ thực hiện khóa đối tượng đó chỉ khi tôi đang liệt kê qua bộ sưu tập.
Bác sĩ đa khoa.

1
Nếu từ điển của bạn không quá lớn, bạn có thể liệt kê trên một bản sao và cài sẵn bản sao đó vào lớp.
Friedguybob

2
Từ điển của tôi không quá lớn và tôi nghĩ điều đó đã thành công. Những gì tôi đã làm là tạo một phương thức công khai mới có tên CopyForEnum () trả về phiên bản mới của Từ điển với các bản sao của từ điển riêng. Phương thức này sau đó được gọi để khai báo và SyncRoot đã bị loại bỏ. Cảm ơn!
Bác sĩ đa khoa.

12
Đây cũng không phải là một lớp thread an toàn vốn có vì các hoạt động từ điển có xu hướng chi tiết. Một chút logic dọc theo dòng if (dict.Contains (bất cứ điều gì)) {dict.Remove (bất cứ điều gì); dict.Add (bất cứ điều gì, newval); } chắc chắn là một điều kiện đua đang chờ xảy ra.
plinth

207

Lớp .NET 4.0 hỗ trợ đồng thời được đặt tên ConcurrentDictionary.


4
Hãy đánh dấu đây là câu trả lời, bạn không cần phải dictoniary tùy chỉnh nếu Net riêng có một giải pháp
Alberto León

27
(Hãy nhớ rằng câu trả lời khác đã được viết rất lâu trước khi sự tồn tại của .NET 4.0 (phát hành năm 2010).)
Jeppe Stig Nielsen

1
Thật không may, nó không phải là một giải pháp không có khóa, vì vậy nó vô dụng trong các hội đồng an toàn SQL Server CLR. Bạn cần một cái gì đó giống như những gì được mô tả ở đây: cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf hoặc có thể là triển khai này: github.com/hackcraft/Ariadne
Triynko

2
Tôi biết là thực sự cũ, nhưng điều quan trọng cần lưu ý là sử dụng ConcurrentDictionary vs a Dictionary có thể dẫn đến tổn thất hiệu suất đáng kể. Đây rất có thể là kết quả của việc chuyển đổi ngữ cảnh tốn kém, vì vậy hãy chắc chắn rằng bạn cần một từ điển an toàn theo luồng trước khi sử dụng.
outbred

60

Cố gắng đồng bộ hóa nội bộ gần như chắc chắn sẽ không đủ vì nó ở mức trừu tượng quá thấp. Giả sử bạn thực hiện các thao tác AddContainsKeythao tác riêng lẻ theo chuỗi an toàn như sau:

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

Sau đó, điều gì sẽ xảy ra khi bạn gọi đoạn mã được cho là an toàn theo luồng này từ nhiều luồng? Nó sẽ luôn hoạt động ổn chứ?

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

Câu trả lời đơn giản là không có. Tại một thời điểm nào đó, Addphương thức sẽ ném ra một ngoại lệ cho biết rằng khóa đã tồn tại trong từ điển. Bạn có thể hỏi điều này như thế nào với một từ điển an toàn theo chuỗi? Chỉ vì mỗi hoạt động là an toàn cho chuỗi, nên sự kết hợp của hai hoạt động thì không, vì một chuỗi khác có thể sửa đổi nó giữa lệnh gọi của bạn đến ContainsKeyAdd .

Có nghĩa là để viết đúng loại kịch bản này, bạn cần có một khóa bên ngoài từ điển, ví dụ:

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

Nhưng bây giờ, khi bạn phải viết mã khóa bên ngoài, bạn đang trộn lẫn đồng bộ hóa bên trong và bên ngoài, điều này luôn dẫn đến các vấn đề như mã không rõ ràng và bế tắc. Vì vậy, cuối cùng có lẽ bạn tốt hơn nên:

  1. Sử dụng bình thường Dictionary<TKey, TValue>và đồng bộ hóa bên ngoài, bao gồm các hoạt động kết hợp trên nó, hoặc

  2. Viết một trình bao bọc an toàn luồng mới với một giao diện khác (tức là không IDictionary<T>) kết hợp các thao tác như một AddIfNotContainedphương thức để bạn không bao giờ cần kết hợp các thao tác từ nó.

(Tôi có xu hướng tự đi với số 1)


10
Cần phải chỉ ra rằng .NET 4.0 sẽ bao gồm một loạt các vùng chứa an toàn luồng như bộ sưu tập và từ điển, có giao diện khác với bộ sưu tập tiêu chuẩn (tức là chúng đang thực hiện tùy chọn 2 ở trên cho bạn).
Greg Beech

1
Cũng cần lưu ý rằng mức độ chi tiết được cung cấp bởi khóa thô thường sẽ đủ cho phương pháp tiếp cận nhiều người đọc một người viết nếu một người thiết kế một trình kê khai phù hợp cho phép lớp bên dưới biết khi nào nó được xử lý (các phương thức muốn viết từ điển trong khi một điều tra viên không thể tranh cãi tồn tại nên thay thế từ điển bằng một bản sao).
supercat

6

Bạn không nên xuất bản đối tượng khóa riêng tư của mình thông qua một thuộc tính. Đối tượng khóa nên tồn tại riêng tư với mục đích duy nhất là hoạt động như một điểm hẹn.

Nếu hiệu suất kém khi sử dụng khóa tiêu chuẩn thì bộ sưu tập khóa Power Threading của Wintellect có thể rất hữu ích.


5

Có một số vấn đề với phương pháp triển khai mà bạn đang mô tả.

  1. Bạn không nên để lộ đối tượng đồng bộ hóa của mình. Làm như vậy sẽ giúp bạn dễ dàng nhận ra người tiêu dùng đang nắm lấy đồ vật và mở khóa và sau đó bạn sẽ nâng ly.
  2. Bạn đang triển khai giao diện an toàn phi luồng với lớp an toàn luồng. IMHO điều này sẽ khiến bạn phải trả giá

Cá nhân tôi đã tìm thấy cách tốt nhất để triển khai một lớp an toàn luồng là thông qua tính bất biến. Nó thực sự làm giảm số lượng các vấn đề bạn có thể gặp phải với an toàn luồng. Kiểm tra Blog của Eric Lippert để biết thêm chi tiết.


3

Bạn không cần phải khóa thuộc tính SyncRoot trong các đối tượng tiêu dùng của mình. Khóa bạn có trong các phương thức của từ điển là đủ.

Để hoàn thiện: Điều cuối cùng xảy ra là từ điển của bạn bị khóa trong một khoảng thời gian dài hơn mức cần thiết.

Điều gì xảy ra trong trường hợp của bạn là:

Nói chuỗi A có được khóa trên SyncRoot trước đó gọi đến m_mySharedDictionary.Add. Thread B sau đó cố gắng lấy khóa nhưng bị chặn. Trên thực tế, tất cả các chủ đề khác đều bị chặn. Thread A được phép gọi vào phương thức Add. Tại câu lệnh lock trong phương thức Add, luồng A được phép lấy lại khóa vì nó đã sở hữu nó. Khi thoát khỏi ngữ cảnh khóa trong phương thức và sau đó bên ngoài phương thức, luồng A đã giải phóng tất cả các khóa cho phép các luồng khác tiếp tục.

Bạn chỉ cần cho phép bất kỳ người tiêu dùng nào gọi vào phương thức Add vì câu lệnh khóa trong phương thức Add của lớp SharedDictionary của bạn cũng sẽ có tác dụng tương tự. Tại thời điểm này, bạn có khóa dự phòng. Bạn sẽ chỉ khóa SyncRoot bên ngoài một trong các phương thức từ điển nếu bạn phải thực hiện hai thao tác trên đối tượng từ điển cần được đảm bảo xảy ra liên tiếp.


1
Không đúng ... nếu bạn thực hiện hai thao tác khác mà nội bộ an toàn theo luồng, điều đó không có nghĩa là khối mã tổng thể là an toàn cho luồng. Ví dụ: if (! MyDict.ContainsKey (someKey)) {myDict.Add (someKey, someValue); } sẽ không là threadsafe, ngay cả khi nó ContainsKey và Add cũng là threadsafe
Tim

Quan điểm của bạn là đúng, nhưng không phù hợp với câu trả lời và câu hỏi của tôi. Nếu bạn nhìn vào câu hỏi, nó không nói về việc gọi ContainsKey, và câu trả lời của tôi cũng không. Câu trả lời của tôi đề cập đến việc có được một khóa trên SyncRoot được hiển thị trong ví dụ trong câu hỏi ban đầu. Trong ngữ cảnh của câu lệnh khóa, một hoặc nhiều hoạt động an toàn luồng sẽ thực thi một cách an toàn.
Peter Meyer

Tôi đoán nếu tất cả những gì anh ấy làm là thêm vào từ điển, nhưng vì anh ấy có "// thêm thành viên IDictionary ...", tôi cho rằng một lúc nào đó anh ấy cũng sẽ muốn đọc lại dữ liệu từ từ điển. Nếu đúng như vậy, thì cần phải có một số cơ chế khóa có thể truy cập bên ngoài. Không quan trọng nếu nó là SyncRoot trong chính từ điển hay một đối tượng khác chỉ được sử dụng để khóa, nhưng nếu không có sơ đồ như vậy, mã tổng thể sẽ không an toàn theo chuỗi.
Tim

Cơ chế khóa bên ngoài giống như trong ví dụ của anh ấy trong câu hỏi: lock (m_MySharedDictionary.SyncRoot) {m_MySharedDictionary.Add (...); } - sẽ hoàn toàn an toàn nếu làm như sau: lock (m_MySharedDictionary.SyncRoot) {if (! m_MySharedDictionary.Contains (...)) {m_MySharedDictionary.Add (...); }} Nói cách khác, cơ chế khóa ngoài là câu lệnh khóa hoạt động trên thuộc tính công cộng SyncRoot.
Peter Meyer

0

Chỉ là một suy nghĩ tại sao không tạo lại từ điển? Nếu đọc là nhiều lần ghi thì khóa sẽ đồng bộ hóa tất cả các yêu cầu.

thí dụ

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

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.