【问题标题】:Multithreaded access to a Dictionary多线程访问字典
【发布时间】:2021-06-07 21:23:27
【问题描述】:

我在网上搜索过,我对多线程有点困惑(lockMonitor.Entervolatile 等)所以,我没有在这里询问解决方案,而是尝试了一些“自制”的东西关于多线程管理,我想听听你的建议。

这是我的背景:

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

-我有很多任务(比如说 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 方法中的两次“锁定”bool 分配不会生成多线程异常,因为它是由另一个线程“每次”读取的

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

【问题讨论】:

  • “我对多线程(锁、Monitor.Enter、易失性等)有点困惑”,但只有它们才是处理多线程挑战的正确方法。您上面的代码根本不起作用。
  • @LexLi 事实上它有效,哈哈这就是我想了解的:为什么它有效?
  • 如果你有时间系统地学习多线程,这里有一个很棒的在线资源:Threading in C#,作者是 Joseph Albahari。为了编写正确的多线程代码,需要对基础有一些扎实的了解。

标签: c# multithreading thread-safety


【解决方案1】:

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

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

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(其他线程可以访问它的唯一位置),它就永远不会被修改,因此线程问题就消失了。

【讨论】:

  • 基于“这是缓存的代码:”使用MemoryCache 可能会更好,因为 OP 确实需要具有线程安全和过期的适当缓存。
  • 你的transientDictionary 不是从Entries 字典中构造的,所以它会丢失所有已经在其中的东西,对吗?不确定这如何添加到现有数据中。
【解决方案2】:

使用非易失性bool 标志进行线程同步不是线程安全的,并且会使您的代码容易受到竞争条件和haisenbugs 的影响。正确的做法是在新字典完全构建后,使用Volatile.WriteInterlocked.Exchange 方法等跨线程发布机制,用新字典原子替换旧字典。您的案例很简单,您也可以使用 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 纳秒,因此应该不成问题。这是值得付出的代价,因为它保证了程序的正确性。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-12-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多