多线程访问字典
Multithreaded access to a Dictionary
我在网上搜索了一下,我对多线程(lock
、Monitor.Enter
、volatile
等)有点困惑,所以,与其在这里询问解决方案,我尝试了一些关于多线程管理的“自制”方法,希望得到您的建议。
这是我的背景:
-我有一个包含静态 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 纳秒,因此这应该不是问题。这是值得付出的代价,因为它保证了你程序的正确性。
我在网上搜索了一下,我对多线程(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
方法中的两次'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 纳秒,因此这应该不是问题。这是值得付出的代价,因为它保证了你程序的正确性。