多线程访问字典



我在网上搜索过,我对多线程(lock,Monitor.Enter,volatile等)有点困惑,所以,不是在这里问解决方案,我尝试了一些"自制"的东西。关于多线程管理,我想听听你的建议。

这是我的上下文:

-我有一个包含静态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赋值不会产生多线程异常,因为它每次都被另一个线程读取

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

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

针对您的特定情况处理此问题的另一种方法是使用普通字典,但一旦它对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标志进行线程同步不是线程安全的,并且使您的代码容易受到竞争条件和haisenbug的影响。正确的做法是,在新字典完全构造完成后,使用像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 nsec,所以它应该不是问题。这是值得付出的代价,因为它保证了程序的正确性。

最新更新