.Net BlockingCollection.Take(2):一次安全删除两个项目

时间:2012-06-04 19:56:56

标签: c# .net-4.0 task-parallel-library concurrent-programming

在做了一些研究后,我正在寻求有关如何有效删除Concurrent集合中的两个项目的任何反馈。我的情况涉及UDP上的传入消息,这些消息当前被放入BlockingCollection中。一旦集合中有两个用户,我需要安全地接管两个用户并处理它们。我已经看到了几种不同的技术,包括下面列出的一些想法。我目前的实现方式如下,但我认为有一种更简洁的方法可以确保用户以两人为一组进行处理。这是这种情况下的唯一限制。

当前实施:

    private int userQueueCount = 0;
    public BlockingCollection<User> UserQueue = new BlockingCollection<User>();

    public void JoinQueue(User u)
    {
           UserQueue.Add(u);
           Interlocked.Increment(ref userQueueCount);

           if (userQueueCount > 1)
           {
               IEnumerable<User> users = UserQueue.Take(2);
               if(users.Count==2) {
                 Interlocked.Decrement(ref userQueueCount);
                 Interlocked.Decrement(ref userQueueCount);
                 ... do some work with users but if only one 
                 is removed I'll run into problems
               }

           }
    }

我想做的是这样的事情,但我目前无法在生产环境中对此进行测试以确保完整性。

 Parallel.ForEach(UserQueue.Take(2), (u) => { ... });

或者更好:

    public void JoinQueue(User u)
    {
           UserQueue.Add(u);
           // if needed? increment
           Interlocked.Increment(ref userQueueCount);
           UserQueue.CompleteAdding();
    }

然后在某处实现:

        Task.Factory.StartNew(() =>
        {
            while (userQueueCount > 1) OR (UserQueue.Count > 1) If it's safe?
            {
                IEnumerable<User> users = UserQueue.Take(2);
                ... do stuff
            }

        });

这个问题是我不确定我能保证条件(Count&gt; 1)和Take(2)之间我确保UserQueue至少有两个要处理的项目?传入的UDP消息是并行处理的,所以我需要一种方法来安全地从阻塞/并发集合中将项目从两对中拉出来。

有更好/更安全的方法吗?

修订评论: 这个问题的目标实际上只是为了实现一个稳定/线程安全的方法来处理.Net 4.0中的并发集合中的项目。它不一定非常漂亮,它只需要在并行环境中处理无序二对中的项目时保持稳定。

4 个答案:

答案 0 :(得分:2)

以下是我在粗略代码中所做的事情:

ConcurrentQueuequeue = new ConcurrentQueue(); //can use a BlockingCollection too (as it's just a blocking ConcurrentQueue by default anyway)

public void OnUserStartedGame(User joiningUser)
{
   User waitingUser;
   if (this.gameQueue.TryDequeue(out waitingUser)) //if there's someone waiting, we'll get him
      this.MatchUsers(waitingUser, joiningUser);
   else
      this.QueueUser(joiningUser); //it doesn't matter if there's already someone in the queue by now because, well, we are using a queue and it will sort itself out.
}

private void QueueUser(User user)
{
   this.gameQueue.Enqueue(user);
}

private void MatchUsers(User first, User second)
{
   //not sure what you do here
}

基本的想法是,如果某人想要开始一个游戏并且队列中有人,则匹配他们并开始游戏 - 如果没有人,则将他们添加到队列中。 最好一次只有一个用户在队列中,但如果没有,那么,这也不是太糟糕,因为当其他用户启动游戏时,等待的用户将逐渐删除,并且在队列为空之前不会添加新的用户试。

答案 1 :(得分:1)

如果我出于某种原因无法将对用户放入集合中,我会使用ConcurrentQueue并尝试一次尝试TryDequeue 2项,如果我只能获得一项 - 将其放回去。等待。

答案 2 :(得分:1)

我认为这里最简单的解决方案是使用锁定:您将为所有消费者提供一个锁定(生产者不会使用任何锁定),这将确保您始终以正确的顺序接受用户:

User firstUser;
User secondUser;

lock (consumerLock)
{
    firstUser = userQueue.Take();
    secondUser = userQueue.Take();
}

Process(firstUser, secondUser);

另一个选择是拥有两个队列:一个用于单个用户,一个用于用户对,并且有一个进程将它们从第一个队列传输到第二个队列。

如果您不介意浪费其他线程,可以使用两个BlockingCollection来执行此操作:

while (true)
{
    var firstUser = incomingUsers.Take();
    var secondUser = incomingUsers.Take();

    userPairs.Add(Tuple.Create(firstUser, secondUser));
}

您不必担心此处的锁定,因为单个用户的队列只有一个消费者,而对的消费者现在可以安全地使用简单的Take()

如果你关心浪费线程并且可以使用TPL数据流,你可以使用BatchBlock<T>,它将传入的项目组合成批量的 n 项目,其中 n 在创建块时配置,因此您可以将其设置为2.

答案 3 :(得分:0)

这可以帮助

public static IList<T> TakeMulti<T>(this BlockingCollection<T> me, int count = 100) where T : class
{
    T last = null;
    if (me.Count == 0)
    {
        last = me.Take(); // blocking when queue is empty
    }

    var result = new List<T>(count);

    if (last != null)
    {
        result.Add(last);
    }

    //if you want to take more item on this time.
    //if (me.Count < count / 2)
    //{
    //    Thread.Sleep(1000);
    //}

    while (me.Count > 0 && result.Count <= count)
    {
        result.Add(me.Take());
    }

    return result;
}