在我需要之前获取后台线程上我需要的下一个值

时间:2012-11-08 17:09:34

标签: c# multithreading

我希望在我需要之前找到一些关于获取一堆id值(比如数据库Identity值)的最佳方法的建议。我有许多类需要一个唯一的id(int),我想要做的是获取下一个可用的id(每个类,每个服务器)并让它在本地缓存。当一个id被带走时,我想让下一个准备等等。

我已经制作了一些代码来演示我想要做的事情。代码很糟糕(它应该包含锁等),但我认为它得到了重点。丢失奇数ID不是问题 - 重复的ID是(问题)。我很满意GetNextIdAsync的胆量 - 它调用了proc

this.Database.SqlQuery<int>("EXEC EntityNextIdentityValue @Key", 
            new SqlParameter("Key", key))).First();

在SQL Server上使用sp_getapplock确保每个返回值都是唯一的(和增量的)。

static class ClassId
{
    static private Dictionary<string, int> _ids = new Dictionary<string,int>();
    static private Dictionary<string, Thread> _threads = new Dictionary<string,Thread>();

    static ClassId()
    {
        //get the first NextId for all known classes
        StartGetNextId("Class1");
        StartGetNextId("Class2");
        StartGetNextId("Class3");
    }

    static public int NextId(string key)
    {
        //wait for a current call for nextId to finish
        while (_threads.ContainsKey(key)) { }
        //get the current nextId
        int nextId = _ids[key];
        //start the call for the next nextId
        StartGetNextId(key);
        //return the current nextId
        return nextId;
    }

    static private void StartGetNextId(string key)
    {
        _threads.Add(key, new Thread(() => GetNextIdAsync(key)));
        _threads[key].Start();
    }

    static private void GetNextIdAsync(string key)
    {
        //call the long running task to get the next available value
        Thread.Sleep(1000);
        if (_ids.ContainsKey(key)) _ids[key] += 1;
        else _ids.Add(key, 1);
        _threads.Remove(key);
    }
}

我的问题是 - 在我需要它之前总是拥有我需要的下一个值的最佳方法是什么?如何安排课程以及锁定在哪里?例如。锁定GetNextIdAsync()添加新线程但不启动它并更改StartGetNextId()以调用.Start()?

3 个答案:

答案 0 :(得分:2)

您应该让数据库通过适当地标记该列来生成标识值。您可以使用SCOPE_IDENTITY或类似内容检索该值。

您实现的主要缺点是NextId中的忙等待以及从多个线程同时访问Dictionary。最简单的解决方案是使用像下面的ohadsc建议的BlockingCollection。您需要预测数据库出现故障并且无法获得更多ID的情况 - 您不希望使应用程序死锁。因此,您可能希望使用接受ConcellationToken的Take()重载,如果访问数据库失败,您将通知它。

答案 1 :(得分:0)

这似乎是生产者 - 消费者模式的一个很好的应用。

我在想:

private ConcurrentDictionary<string, int> _ids;
private ConcurrentDictionary<string, Thread> _threads;
private Task _producer;
private Task _consumer;
private CancellationTokenSource _cancellation;

private void StartProducer()
{
    _producer = Task.Factory.StartNew(() => 
       while (_cancellation.Token.IsCancellationRequested == false)
       {
           _ids.Add(GetNextKeyValuePair());
       }
   )
}

private void StartConsumer()
{
    _consumer = Task.Factory.StartNew(() => 
       while (_cancellation.Token.IsCancellationRequested == false)
       {
           UseNextId(id);
           _ids.Remove(id);
       }
   )
}

要指出的一些事情......

首先,您可能已经知道这一点,使用ConcurrentDictionaryBlockingCollection等线程安全集合代替普通DictonaryList非常重要。如果你不这样做,会发生坏事,人们会死,而且婴儿会哭。

其次,你可能需要一些比基本CancellationTokenSource少得多的东西,这就是我在服务编程中习惯的东西。关键是要有一些方法来取消这些东西,这样你就可以优雅地关闭它们。

第三,考虑在那里投掷sleep以防止它过于猛烈地冲击处理器。

具体细节取决于您生成这些东西的速度,而不是消耗它们的速度。如果消费者以比生产者高得多的速度运行,我的代码绝对不能保证在消费者要求之前你会拥有你想要的ID。然而,这是一种体面的,虽然是同时组织准备这类数据的基本方法。

答案 2 :(得分:0)

您可以使用BlockingCollection。基本上你会有一个线程将新的ID抽入缓冲区:

BlockingCollection<int> _queue = new BlockingCollection<int>(BufferSize);

void Init()
{
    Task.Factory.StartNew(PopulateIdBuffer, TaskCreationOptions.LongRunning);
}

void PopulateIdBuffer()
{
    int id = 0;
    while (true)
    {
        Thread.Sleep(1000); //Simulate long retrieval
        _queue.Add(id++);
    }
}

void SomeMethodThatNeedsId()
{
    var nextId = _queue.Take();
    ....
}