具有单实例化的全局线程安全多值自定义字典

For*_*rna 3 c# multithreading global-variables thread-safety

我想有一个类似于不同线程之间共享的多值字典的全局对象.

我希望只创建一次对象(例如从数据库获取数据),然后由不同的线程使用.

Object应该可以使用其他属性轻松扩展(目前只有JobName和URL).

如果可能的话,我宁愿避免锁定.

我面临以下问题:

  • 下面显示的当前版本不是线程安全的;
  • 我不能使用ConcurrentDictionary,因为我已经扩展了Dictionary对象以允许每个键的多个值;

这是应该轻松修改的对象结构:

    public struct JobData
    {
        public string JobName;
        public string URL;
    }
Run Code Online (Sandbox Code Playgroud)

我已经扩展了Dictionary对象,允许每个键有多个值:

    public class JobsDictionary : Dictionary<string, JobData>
    {
        public void Add(string key, string jobName, string url)
        {
            JobData data;
            data.JobName = jobName;
            data.URL = url;
            this.Add(key, data);
        }
    }
Run Code Online (Sandbox Code Playgroud)

在Threads之间共享的静态类.正如您所看到的,它在第一次为该Job调用时为特定Job创建了一个Dictionary条目.

例如,第一次调用"收益"时,它将创建"收入"字典条目.这会产生线程安全问题:

public static class GlobalVar
{
    private static JobsDictionary jobsDictionary = new JobsDictionary();
    public static JobData Job(string jobCat)
    {   
        if (jobsDictionary.ContainsKey(jobCat))
            return jobsDictionary[jobCat];
        else
        {
            String jobName;
            String url = null;

            //TODO: get the Data from the Database
            switch (jobCat)
            {
                case "earnings":
                    jobName="EarningsWhispers";
                    url = "http://www.earningswhispers.com/stocks.asp?symbol={0}";
                    break;
                case "stock":
                    jobName="YahooStock";
                    url = "http://finance.yahoo.com/q?s={0}";
                    break;
                case "functions":
                    jobName = "Functions";
                    url = null;
                    break;
                default:
                    jobName = null;
                    url = null;
                    break;
            }
            jobsDictionary.Add(jobCat, jobName, url);
            return jobsDictionary[jobCat];
        }
    }
Run Code Online (Sandbox Code Playgroud)

在每个线程中,我以这种方式获取特定的Job属性:

//Get the Name
string JobName= GlobalVar.Job(jobName).JobName;

//Get the URL
string URL = string.Format((GlobalVar.Job(jobName).URL), sym);
Run Code Online (Sandbox Code Playgroud)

如何创建一个"实例化"的自定义词典(我知道它不是正确的术语,因为它是静态的......)并且它是线程安全的?

谢谢

UPDATE

好的,这是新版本.

我通过删除switch语句并一次加载所有字典项来简化代码(无论如何我都需要它们).

此解决方案的优点是它只被锁定一次:添加字典数据时(进入锁定的第一个线程将向字典添加数据).当线程访问字典进行读取时,它不会被锁定.

它应该是线程安全的,它不应该导致死锁,因为jobsDictionary是私有的.

public static class GlobalVar
{
    private static JobsDictionary jobsDictionary = new JobsDictionary();   
    public static JobData Job(string jobCat)
    {
        JobData result;
        if (jobsDictionary.TryGetValue(jobCat, out result))
            return result;

        //if the jobsDictionary is not initialized yet...
        lock (jobsDictionary)
        {
            if (jobsDictionary.Count == 0)
            {
                //TODO: get the Data from the Database
                jobsDictionary.Add("earnings", "EarningsWhispers", "http://www.earningswhispers.com/stocks.asp?symbol={0}");
                jobsDictionary.Add("stock", "YahooStock", "http://finance.yahoo.com/q?s={0}");   
                jobsDictionary.Add("functions", "Functions", null);  
            }
            return jobsDictionary[jobCat];
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Ste*_*ven 6

如果要填充集合一次,则根本不需要任何锁定,因为只有在读取时,Dictionary才是线程安全的.如果要防止多个线程多次初始化,可以在初始化期间使用双重检查锁定,如下所示:

static readonly object syncRoot = new object();
static Dictionary<string, JobData> cache;

static void Initialize()
{
    if (cache == null)
    {
        lock (syncRoot)
        {
            if (cache == null)
            {
                cache = LoadFromDatabase();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

而不是允许每个线程访问字典,将其隐藏在仅暴露您真正需要的操作的外观后面.这使得更容易推理线程安全性.例如:

public class JobDataCache : IJobData
{
    readonly object syncRoot = new object();
    Dictionary<string, JobData> cache;

    public void AddJob(string key, JobData data)
    {
        lock (this.syncRoot)
        {
            cache[key] = data;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

试图在没有测量到锁定实际上对性能产生太大影响的情况下防止锁定是不好的.防止那样做.通常使用简单lock语句比编写无锁代码简单得多.与普通软件错误相比,并发错误存在一个令人讨厌的问题.它们很难再现,很难追踪.如果可以,请防止编写并发错误.您可以通过编写最简单的代码来完成此操作,即使速度较慢也是如此.如果它被证明太慢,你可以随时优化.

如果您还想编写无锁代码,请尝试使用不可变数据结构,或阻止更改现有数据.这是我在编写Simple Injector(一个可重用的库)时使用的一个技巧.在这个框架中,我从不更新内部字典,但始终完全用新字典替换它.因此字典本身永远不会改变,对该实例的引用只是被一个全新的字典所取代.这可以防止您完全执行锁定.但是,您必须意识到可能会丢失更新.换句话说,当多个线程正在更新该字典时,可以放松其更改,只是因为每个线程创建该字典的新副本并将其自己的值添加到其自己的副本中,然后将该引用公开给其他线程.

换句话说,您只能在外部调用者只读取时使用此方法(并且您可以从丢失的更改中恢复,例如通过再次查询数据库).

UPDATE

您的更新版本仍然不是线程安全的,因为我在@ ili的答案中解释了原因.以下将做到这一点:

public static class GlobalVar
{
    private static readonly object syncRoot = new object();
    private static JobsDictionary jobsDictionary = null;

    public static JobData Job(string jobCat)
    {
        Initialize();

        return jobsDictionary[jobCat];
    }

    private void Initialize()
    {
        // Double-checked lock.
        if (jobsDictionary == null)
        {
            lock (syncRoot)
            {
                if (jobsDictionary == null)
                {
                    jobsDictionary = CreateJobsDictionary();
                }
            }
        }
    }

    private static JobsDictionary CreateJobsDictionary()
    {
        var jobs = new JobsDictionary();

        //TODO: get the Data from the Database
        jobs.Add("earnings", "EarningsWhispers", "http://...");
        jobs.Add("stock", "YahooStock", "http://...");
        jobs.Add("functions", "Functions", null);

        return jobs;
    }
}
Run Code Online (Sandbox Code Playgroud)

您还可以使用静态构造函数,这将阻止您自己编写双重检查锁.但是,在静态构造函数中调用数据库是不合时宜的,因为静态构造函数只会运行一次,当它失败时,只要AppDomain存在,完整类型就无法使用.换句话说,当发生这种情况时,必须重新启动应用程序.

更新2:

您还可以使用.NET 4.0 Lazy<T>,它比双重检查锁更安全,因为它更容易实现(并且更容易正确实现),并且在具有弱内存模型的处理器体系结构上也是线程安全的(弱于x86)比如ARM):

static Lazy<Dictionary<string, JobData>> cache =
    new Lazy<Dictionary<string, JobData>>(() => LoadFromDatabase());
Run Code Online (Sandbox Code Playgroud)