Hashset đồng thời <T> trong .NET Framework?


151

Tôi có lớp sau.

class Test{
    public HashSet<string> Data = new HashSet<string>();
}

Tôi cần thay đổi trường "Dữ liệu" từ các luồng khác nhau, vì vậy tôi muốn có một số ý kiến ​​về việc triển khai an toàn luồng hiện tại của mình.

class Test{
    public HashSet<string> Data = new HashSet<string>();

    public void Add(string Val){
            lock(Data) Data.Add(Val);
    }

    public void Remove(string Val){
            lock(Data) Data.Remove(Val);
    }
}

Có một giải pháp tốt hơn, để trực tiếp đến trường và bảo vệ nó khỏi sự truy cập đồng thời bởi nhiều luồng?


Cách sử dụng một trong các bộ sưu tập dướiSystem.Collections.Concurrent
I4V

8
Tất nhiên, làm cho nó riêng tư.
Hans Passant

3
Từ góc độ đồng thời, tôi không thấy sai nhiều với những gì bạn đã làm ngoài trường Dữ liệu được công khai! Bạn có thể có được hiệu suất đọc tốt hơn bằng ReaderWriterLockSlim nếu đó là một vấn đề đáng lo ngại. msdn.microsoft.com/en-us/l Library / từ
Allan Elder

@ ALLanElder ReaderWriterLocksẽ hữu ích (hiệu quả) khi có nhiều người đọc và một người viết. Chúng tôi đã biết liệu đây có phải là trường hợp của OP
Sriram Sakth Xoay

2
Việc triển khai hiện tại không thực sự 'đồng thời' :) Nó chỉ an toàn theo luồng.
không xác định

Câu trả lời:


164

Thực hiện của bạn là chính xác. Thật không may, .NET Framework không cung cấp loại băm đồng thời tích hợp sẵn. Tuy nhiên, có một số cách giải quyết.

Đồng thời Từ điển (khuyên dùng)

Điều đầu tiên này là sử dụng lớp ConcurrentDictionary<TKey, TValue>trong không gian tên System.Collections.Concurrent. Trong trường hợp, giá trị là vô nghĩa, vì vậy chúng ta có thể sử dụng một đơn giản byte(1 byte trong bộ nhớ).

private ConcurrentDictionary<string, byte> _data;

Đây là tùy chọn được đề xuất vì loại này là an toàn cho luồng và cung cấp cho bạn những lợi thế giống như HashSet<T>ngoại trừ khóa và giá trị là các đối tượng khác nhau.

Nguồn: MSDN xã hội

Đồng thờiBag

Nếu bạn không bận tâm về các mục trùng lặp, bạn có thể sử dụng lớp ConcurrentBag<T>trong cùng một không gian tên của lớp trước đó.

private ConcurrentBag<string> _data;

Tự thực hiện

Cuối cùng, như bạn đã làm, bạn có thể thực hiện kiểu dữ liệu của riêng mình, sử dụng khóa hoặc các cách khác mà .NET cung cấp cho bạn để đảm bảo an toàn cho chuỗi. Dưới đây là một ví dụ tuyệt vời: Cách triển khai ConcurrencyHashset trong .Net

Hạn chế duy nhất của giải pháp này là loại HashSet<T>không truy cập chính thức đồng thời, ngay cả đối với các hoạt động đọc.

Tôi trích dẫn mã của bài đăng được liên kết (ban đầu được viết bởi Ben Mosher ).

using System;
using System.Collections.Generic;
using System.Threading;

namespace BlahBlah.Utilities
{
    public class ConcurrentHashSet<T> : IDisposable
    {
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
        private readonly HashSet<T> _hashSet = new HashSet<T>();

        #region Implementation of ICollection<T> ...ish
        public bool Add(T item)
        {
            _lock.EnterWriteLock();
            try
            {
                return _hashSet.Add(item);
            }
            finally
            {
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }
        }

        public void Clear()
        {
            _lock.EnterWriteLock();
            try
            {
                _hashSet.Clear();
            }
            finally
            {
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }
        }

        public bool Contains(T item)
        {
            _lock.EnterReadLock();
            try
            {
                return _hashSet.Contains(item);
            }
            finally
            {
                if (_lock.IsReadLockHeld) _lock.ExitReadLock();
            }
        }

        public bool Remove(T item)
        {
            _lock.EnterWriteLock();
            try
            {
                return _hashSet.Remove(item);
            }
            finally
            {
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }
        }

        public int Count
        {
            get
            {
                _lock.EnterReadLock();
                try
                {
                    return _hashSet.Count;
                }
                finally
                {
                    if (_lock.IsReadLockHeld) _lock.ExitReadLock();
                }
            }
        }
        #endregion

        #region Dispose
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
                if (_lock != null)
                    _lock.Dispose();
        }
        ~ConcurrentHashSet()
        {
            Dispose(false);
        }
        #endregion
    }
}

EDIT: Di chuyển các phương thức khóa lối vào bên trong các trykhối, vì chúng có thể ném ngoại lệ và thực hiện các hướng dẫn có trong các finallykhối.


8
một từ điển với các giá trị rác là một danh sách
Ralf

44
@Ralf Vâng, đó là một bộ, không phải là một danh sách, vì nó không có thứ tự.
Phục vụ

11
Theo tài liệu khá ngắn của MSDN về "Bộ sưu tập và đồng bộ hóa (An toàn luồng )" , các lớp trong System.Collections và các không gian tên liên quan có thể được đọc bởi nhiều luồng một cách an toàn. Điều này có nghĩa là Hashset có thể được đọc một cách an toàn bởi nhiều luồng.
Hank Schultz

7
@Oliver, một tham chiếu sử dụng nhiều bộ nhớ hơn cho mỗi mục, ngay cả khi đó là nulltham chiếu (tham chiếu cần 4 byte trong thời gian chạy 32 bit và 8 byte trong thời gian chạy 64 bit). Do đó, sử dụng a byte, một cấu trúc trống hoặc tương tự có thể làm giảm dung lượng bộ nhớ (hoặc có thể không nếu bộ thực thi sắp xếp dữ liệu trên ranh giới bộ nhớ riêng để truy cập nhanh hơn).
Lucero

4
Tự thực hiện không phải là một Đồng thờiHashset mà là một ThreadSafeHashset. Có một sự khác biệt lớn giữa 2 cái đó và đó là lý do tại sao Micorosft từ bỏ SyncizedCollections (mọi người đã hiểu sai). Để được thực hiện các hoạt động "Đồng thời" như GetOrAdd, v.v. (như từ điển) hoặc không thể đảm bảo đồng thời nếu không có khóa bổ sung. Nhưng nếu bạn cần khóa bổ sung bên ngoài lớp thì tại sao bạn không sử dụng Hashset đơn giản ngay từ đầu?
George Mavritsakis

36

Thay vì gói một ConcurrentDictionaryhoặc khóa trên một HashSettôi đã tạo ra một thực tế ConcurrentHashSetdựa trên ConcurrentDictionary.

Việc triển khai này hỗ trợ các hoạt động cơ bản cho mỗi mục mà không cần HashSetcác thao tác được đặt vì chúng ít có ý nghĩa hơn trong các kịch bản đồng thời IMO:

var concurrentHashSet = new ConcurrentHashSet<string>(
    new[]
    {
        "hamster",
        "HAMster",
        "bar",
    },
    StringComparer.OrdinalIgnoreCase);

concurrentHashSet.TryRemove("foo");

if (concurrentHashSet.Contains("BAR"))
{
    Console.WriteLine(concurrentHashSet.Count);
}

Đầu ra: 2

Bạn có thể lấy nó từ NuGet tại đây và xem nguồn trên GitHub tại đây .


3
Đây phải là câu trả lời được chấp nhận, thực hiện tuyệt vời
smirkingman

Không nên Add được đổi tên thành TryAdd để nó phù hợp với ConcurrentDictionary ?
Neo

8
@Neo Không ... bởi vì nó cố ý sử dụng Hashset <T> semantics, trong đó bạn gọi Add và nó trả về một boolean cho biết liệu mục đó đã được thêm (đúng) hay nó đã tồn tại (sai). msdn.microsoft.com/en-us/l Library / bb353005 (v = vs.110) .aspx
G-Mac

Không nên thực hiện ISet<T>giao diện bo thực sự phù hợp với HashSet<T>ngữ nghĩa?
Nekromancer

1
@Nekromancer như tôi đã nói trong câu trả lời, tôi không nghĩ việc cung cấp các phương thức thiết lập này trong một triển khai đồng thời là hợp lý. Overlapsví dụ, hoặc cần phải khóa cá thể trong suốt quá trình chạy hoặc cung cấp câu trả lời có thể đã sai. Cả hai tùy chọn đều là IMO xấu (và có thể được thêm bởi người tiêu dùng bên ngoài).
i3arnon

21

Vì không ai khác đề cập đến nó, tôi sẽ đưa ra một cách tiếp cận khác có thể phù hợp hoặc không phù hợp với mục đích cụ thể của bạn:

Bộ sưu tập bất biến của Microsoft

Từ một bài đăng trên blog của nhóm MS phía sau:

Mặc dù việc tạo và chạy đồng thời trở nên dễ dàng hơn bao giờ hết, một trong những vấn đề cơ bản vẫn tồn tại: trạng thái chia sẻ có thể thay đổi. Đọc từ nhiều chủ đề thường rất dễ dàng, nhưng một khi trạng thái cần được cập nhật, nó sẽ khó hơn rất nhiều, đặc biệt là trong các thiết kế yêu cầu khóa.

Một thay thế cho khóa là sử dụng trạng thái bất biến. Các cấu trúc dữ liệu bất biến được đảm bảo không bao giờ thay đổi và do đó có thể được chuyển tự do giữa các luồng khác nhau mà không phải lo lắng về việc dẫm lên ngón chân của người khác.

Thiết kế này tạo ra một vấn đề mới: Làm thế nào để bạn quản lý các thay đổi trong trạng thái mà không sao chép toàn bộ trạng thái mỗi lần? Điều này đặc biệt khó khăn khi các bộ sưu tập có liên quan.

Đây là nơi bộ sưu tập bất di bất dịch.

Các bộ sưu tập này bao gồm ImmutableHashset <T>ImmutableList <T> .

Hiệu suất

Do các bộ sưu tập bất biến sử dụng các cấu trúc dữ liệu cây bên dưới để cho phép chia sẻ cấu trúc, nên các đặc tính hiệu suất của chúng khác với các bộ sưu tập có thể thay đổi. Khi so sánh với một bộ sưu tập có thể thay đổi khóa, kết quả sẽ phụ thuộc vào sự tranh chấp khóa và các mẫu truy cập. Tuy nhiên, lấy từ một bài đăng trên blog khác về các bộ sưu tập bất biến:

Q: Tôi đã nghe nói rằng các bộ sưu tập bất biến là chậm. Đây có phải là bất kỳ khác nhau? Tôi có thể sử dụng chúng khi hiệu suất hoặc bộ nhớ là quan trọng?

Trả lời: Các bộ sưu tập bất biến này đã được điều chỉnh cao để có các đặc tính hiệu suất cạnh tranh với các bộ sưu tập có thể thay đổi trong khi cân bằng chia sẻ bộ nhớ. Trong một số trường hợp, chúng rất nhanh như các bộ sưu tập có thể thay đổi cả về mặt thuật toán và thời gian thực tế, đôi khi còn nhanh hơn, trong khi trong các trường hợp khác, chúng phức tạp hơn về mặt thuật toán. Trong nhiều trường hợp tuy nhiên sự khác biệt sẽ không đáng kể. Nói chung, bạn nên sử dụng mã đơn giản nhất để hoàn thành công việc và sau đó điều chỉnh hiệu suất khi cần thiết. Các bộ sưu tập bất biến giúp bạn viết mã đơn giản, đặc biệt là khi tính an toàn của luồng phải được xem xét.

Nói cách khác, trong nhiều trường hợp, sự khác biệt sẽ không đáng chú ý và bạn nên chọn lựa chọn đơn giản hơn - đối với các bộ đồng thời sẽ được sử dụng ImmutableHashSet<T>, vì bạn không có triển khai khóa có thể thay đổi hiện có! :-)


1
ImmutableHashSet<T>không giúp được gì nhiều nếu ý định của bạn là cập nhật trạng thái chia sẻ từ nhiều luồng hoặc tôi có thiếu điều gì ở đây không?
tugberk

7
@tugberk Có và không. Vì bộ này là bất biến, bạn sẽ phải cập nhật tham chiếu đến nó, mà bộ sưu tập không giúp bạn. Tin tốt là bạn đã giảm được vấn đề phức tạp khi cập nhật cấu trúc dữ liệu được chia sẻ từ nhiều luồng thành vấn đề đơn giản hơn nhiều khi cập nhật tham chiếu được chia sẻ. Thư viện cung cấp cho bạn phương thức ImmutableInterlocked.Update để giúp bạn thực hiện điều đó.
Søren Boisen

1
@ SørenBoisenjust đọc về các bộ sưu tập bất biến và cố gắng tìm ra cách sử dụng chúng an toàn. ImmutableInterlocked.Updatedường như là liên kết còn thiếu Cảm ơn bạn!
xneg

4

Phần khó khăn trong việc tạo ISet<T>đồng thời là các phương thức tập hợp (hợp, giao, khác) có tính chất lặp. Ít nhất bạn phải lặp đi lặp lại trên tất cả n thành viên của một trong các bộ liên quan đến hoạt động, trong khi khóa cả hai bộ.

Bạn mất đi những lợi thế ConcurrentDictionary<T,byte>khi bạn phải khóa toàn bộ thiết lập trong quá trình lặp. Không có khóa, các hoạt động này không phải là chủ đề an toàn.

Với chi phí bổ sung ConcurrentDictionary<T,byte>, có lẽ sẽ khôn ngoan hơn khi chỉ sử dụng trọng lượng nhẹ hơn HashSet<T>và chỉ bao quanh mọi thứ trong ổ khóa.

Nếu bạn không cần các thao tác đã đặt, hãy sử dụng ConcurrentDictionary<T,byte>và chỉ sử dụng default(byte)làm giá trị khi bạn thêm khóa.


2

Tôi thích các giải pháp hoàn chỉnh vì vậy tôi đã làm điều này: Hãy nhớ rằng Count của tôi được triển khai theo một cách khác bởi vì tôi không hiểu tại sao người ta nên cấm đọc hashset trong khi cố gắng đếm các giá trị của nó.

@Zen, Cảm ơn vì đã bắt đầu.

[DebuggerDisplay("Count = {Count}")]
[Serializable]
public class ConcurrentHashSet<T> : ICollection<T>, ISet<T>, ISerializable, IDeserializationCallback
{
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

    private readonly HashSet<T> _hashSet = new HashSet<T>();

    public ConcurrentHashSet()
    {
    }

    public ConcurrentHashSet(IEqualityComparer<T> comparer)
    {
        _hashSet = new HashSet<T>(comparer);
    }

    public ConcurrentHashSet(IEnumerable<T> collection)
    {
        _hashSet = new HashSet<T>(collection);
    }

    public ConcurrentHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer)
    {
        _hashSet = new HashSet<T>(collection, comparer);
    }

    protected ConcurrentHashSet(SerializationInfo info, StreamingContext context)
    {
        _hashSet = new HashSet<T>();

        // not sure about this one really...
        var iSerializable = _hashSet as ISerializable;
        iSerializable.GetObjectData(info, context);
    }

    #region Dispose

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
            if (_lock != null)
                _lock.Dispose();
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _hashSet.GetEnumerator();
    }

    ~ConcurrentHashSet()
    {
        Dispose(false);
    }

    public void OnDeserialization(object sender)
    {
        _hashSet.OnDeserialization(sender);
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        _hashSet.GetObjectData(info, context);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion

    public void Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.Add(item);
        }
        finally
        {
            if(_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public void UnionWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.UnionWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void IntersectWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.IntersectWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void ExceptWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.ExceptWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void SymmetricExceptWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.SymmetricExceptWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsSubsetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsSubsetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsSupersetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsSupersetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsProperSupersetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsProperSupersetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsProperSubsetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsProperSubsetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool Overlaps(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Overlaps(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool SetEquals(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.SetEquals(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    bool ISet<T>.Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Add(item);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public void Clear()
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.Clear();
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool Contains(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Contains(item);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.CopyTo(array, arrayIndex);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool Remove(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Remove(item);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public int Count
    {
        get
        {
            _lock.EnterWriteLock();
            try
            {
                return _hashSet.Count;
            }
            finally
            {
                if(_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }

        }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }
}

Khóa được xử lý ... nhưng còn băm bên trong, khi nào bộ nhớ của nó được giải phóng?
David Rettenbacher

1
@Warappa nó được phát hành khi thu gom rác. Lần duy nhất tôi tự hủy mọi thứ và xóa toàn bộ sự hiện diện của chúng trong một lớp là khi các đối tượng chứa các sự kiện và do đó CÓ THỂ rò rỉ bộ nhớ (như khi bạn sử dụng ObservableCollection và sự kiện đã thay đổi của nó). Tôi sẵn sàng cho các đề xuất nếu bạn có thể bổ sung kiến ​​thức cho sự hiểu biết của tôi về chủ đề này. Tôi cũng đã dành vài ngày để nghiên cứu về việc thu gom rác và luôn tò mò về thông tin mới
Dbl

@ AndreasMüller câu trả lời tốt, tuy nhiên tôi tự hỏi tại sao bạn lại sử dụng '_lock. EntryWriteLock ();' theo sau là '_lock. EntryReadLock ();' trong một số phương pháp như 'IntersectWith' Tôi nghĩ rằng không cần đọc nhìn ở đây vì khóa ghi sẽ ngăn mọi thao tác đọc khi nhập theo mặc định.
Jalal nói

Nếu bạn luôn luôn phải EnterWriteLock, tại sao EnterReadLockthậm chí tồn tại? Nó có thể không sử dụng khóa đọc cho các phương thức như thế Containsnào?
ErikE

2
Đây không phải là một concienHashset mà là một ThreadSafeHashset. Xem nhận xét của tôi về câu trả lời @ZenLulz liên quan đến việc tự thực hiện. Tôi chắc chắn 99% rằng bất kỳ ai đã sử dụng những triển khai đó sẽ có một lỗi nghiêm trọng trong ứng dụng của họ.
George Mavritsakis
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.