C# 对字典的多线程访问

C# 对字典的多线程访问,c#,multithreading,thread-safety,C#,Multithreading,Thread Safety,我在网上搜索过,对多线程(lock,Monitor.Enter,volatile等)有点困惑。因此,我没有在这里询问解决方案,而是尝试了一些关于多线程管理的“自制”方法,我想听听你的建议 以下是我的背景: -我有一个静态类,它包含一个静态字典 -我有很多任务(比如说1000)每秒钟阅读一次这本字典 -我有一个另一个任务,每10秒更新一次字典 以下是缓存的代码: public static class Cache { public static bool locked = false;

我在网上搜索过,对多线程(
lock
Monitor.Enter
volatile
等)有点困惑。因此,我没有在这里询问解决方案,而是尝试了一些关于多线程管理的“自制”方法,我想听听你的建议

以下是我的背景:

-我有一个静态类,它包含一个静态
字典

-我有很多任务(比如说1000)每秒钟阅读一次这本
字典

-我有一个另一个任务,每10秒更新一次字典

以下是缓存的代码:

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;
        }
    }
}
公共静态类缓存
{
公共静态布尔锁定=假;
公共静态字典条目=新字典();
公共静态字典TempEntries=新字典();
//由1000多个任务调用
公共静态字符串GetStringByTaskId(int taskId)
{
字符串结果;
如果(已锁定)
TempEntries.TryGetValue(任务ID,输出结果);
其他的
Entries.TryGetValue(任务ID,输出结果);
返回结果;
}
//由1个任务调用
公共静态void UpdateEntries(列出任务ID)
{
TempEntries=新词典(条目);
锁定=真;
条目。清除();
尝试
{
//模拟数据库访问
睡眠(3000);
foreach(taskIds中的int taskId)
{
Add(taskId,$“task{taskId}:{DateTime.Now}”);
}
}
捕获(例外情况除外)
{
对数(ex);
}
最后
{
锁定=错误;
}
}
}
我的问题是:

程序运行,但我不明白为什么
UpdateEntries
方法中两次“锁定”的
bool
赋值不会生成多线程异常,因为它“每次”都被另一个线程读取


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

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

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

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; 
}
publicstaticvoidupdateEntries(列出任务ID)
{
//其他线程无法查看此词典
var transientDictionary=新字典();
foreach(taskIds中的int taskId)
{
Add(taskId,$“task{taskId}:{DateTime.Now}”);
}
//发布新词典,以便其他线程可以看到它
TempEntries=瞬时字典;
}

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

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

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

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; 
}
publicstaticvoidupdateEntries(列出任务ID)
{
//其他线程无法查看此词典
var transientDictionary=新字典();
foreach(taskIds中的int taskId)
{
Add(taskId,$“task{taskId}:{DateTime.Now}”);
}
//发布新词典,以便其他线程可以看到它
TempEntries=瞬时字典;
}

一旦字典被分配到
TempEntries
(其他线程只能访问它的唯一位置),它将永远不会被修改,因此线程问题将消失。

使用非易失性
bool
标记进行线程同步是不安全的,并且会使代码容易受到竞争条件和冲突的影响。正确的方法是在新词典完全构建之后,使用诸如或方法之类的跨线程发布机制,以原子方式将旧词典替换为新词典。您的案例非常简单,您也可以使用关键字来简化,如下例所示:

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);
    }
}
公共静态类缓存
{
私有静态volatile ReadOnlyDictionary\u条目
=新的只读字典(新字典());
公共静态IReadOnlyDictionary条目=>\u条目;
//由1000多个任务调用
公共静态字符串GetStringByTaskId(int taskId)
{
_TryGetValue(任务ID,输出var结果);
返回结果;
}
//由1个任务调用
公共静态void UpdateEntries(列出任务ID)
{
Thread.Sleep(3000);//模拟数据库访问
var temp=新字典();
foreach(taskIds中的int taskId)
{
临时添加(taskId,$“task{taskId}:{DateTime.Now}”);
}
_条目=新的只读字典(临时);
}
}

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

使用非易失性
bool
标志进行线程同步不是线程安全的,并且会使代码容易受到竞争条件和冲突的影响。正确的方法