多线程访问字典

Multithreaded access to a Dictionary

我在网上搜索了一下,我对多线程(lockMonitor.Entervolatile 等)有点困惑,所以,与其在这里询问解决方案,我尝试了一些关于多线程管理的“自制”方法,希望得到您的建议。

这是我的背景:

-我有一个包含静态 Dictionary<int,string>

的静态 class

-我有很多任务(比方说 1000)每秒阅读这个 Dictionary

-我有 一个 另一个任务,它将每 10 秒更新一次 Dictionary

缓存代码如下:

public static class Cache
{
    public static bool locked = false;
    public static Dictionary<int, string> Entries = new Dictionary<int, string>();
    public static Dictionary<int, string> TempEntries = new Dictionary<int, string>();

    // Called by 1000+ Tasks
    public static string GetStringByTaskId(int taskId)
    {
        string result;

        if (locked)
            TempEntries.TryGetValue(taskId, out result);
        else
            Entries.TryGetValue(taskId, out result);

        return result;
    }

    // Called by 1 task
    public static void UpdateEntries(List<int> taskIds)
    {
        TempEntries = new Dictionary<int, string>(Entries);

        locked = true;
        Entries.Clear();

        try
        {
            // Simulates database access
            Thread.Sleep(3000);

            foreach (int taskId in taskIds)
            {
                Entries.Add(taskId, $"task {taskId} : {DateTime.Now}");
            }
        }
        catch (Exception ex)
        {
            Log(ex);
        }
        finally
        {
            locked = false;
        }
    }
}

这是我的问题:

程序运行但我不明白为什么UpdateEntries方法中的两次'locked'bool赋值没有产生多线程异常,因为它被另一个线程读取“每次”

有没有更传统的方法来处理这个问题,我觉得这是一种奇怪的方法?

处理此问题的常规方法是使用 ConcurrentDictionary。这个 class 是线程安全的,专为多线程读写而设计。您仍然需要注意潜在的逻辑问题(例如,如果必须同时添加两个键,其他线程才能看到它们中的任何一个),但对于大多数操作来说,无需额外锁定就可以了。

针对您的特定情况处理此问题的另一种方法是使用普通字典,但一旦它对 reader 线程可用,就将其视为不可变的。这将更有效,因为它避免了锁定。

public static void UpdateEntries(List<int> taskIds)
{
    //Other threads can't see this dictionary
    var transientDictionary = new Dictionary<int, string>();  

    foreach (int taskId in taskIds)
    {
        transientDictionary.Add(taskId, $"task {taskId} : {DateTime.Now}");
    }

    //Publish the new dictionary so other threads can see it
    TempEntries = transientDictionary; 
}

一旦字典被分配给 TempEntries(其他线程唯一可以访问它的地方),它就永远不会被修改,所以线程问题就消失了。

使用非易失性 bool 标志进行线程同步不是线程安全的,并且使您的代码容易受到竞争条件和 haisenbugs. The correct way to do it is to replace atomically the old dictionary with the new one, after the new one has been fully constructed, using a cross-thread publishing mechanism like the Volatile.Write or the Interlocked.Exchange methods. Your case is simple enough that you could also use the volatile 关键字的影响,如下例所示:

public static class Cache
{
    private static volatile ReadOnlyDictionary<int, string> _entries
        = new ReadOnlyDictionary<int, string>(new Dictionary<int, string>());

    public static IReadOnlyDictionary<int, string> Entries => _entries;

    // Called by 1000+ Tasks
    public static string GetStringByTaskId(int taskId)
    {
        _entries.TryGetValue(taskId, out var result);
        return result;
    }

    // Called by 1 task
    public static void UpdateEntries(List<int> taskIds)
    {
        Thread.Sleep(3000); // Simulate database access

        var temp = new Dictionary<int, string>();
        foreach (int taskId in taskIds)
        {
            temp.Add(taskId, $"task {taskId} : {DateTime.Now}");
        }
        _entries = new ReadOnlyDictionary<int, string>(temp);
    }
}

使用这种方法,每次访问 _entries 字段都会产生波动成本,每次操作通常少于 10 纳秒,因此这应该不是问题。这是值得付出的代价,因为它保证了你程序的正确性。