如何从ConcurrentBag<>中删除单个特定对象?

140

在.NET 4中,使用新的ConcurrentBag<T>,当只有TryTake()TryPeek()可用时,如何从中删除特定的对象?

我考虑使用TryTake(),如果我想删除它,就将其添加回列表中,但我觉得我可能遗漏了什么。这是正确的方法吗?


2
每次都让我感到困惑。终于意识到“包”意味着你不能看里面的东西。就像在玩Scrabble游戏一样。 - Simon_Weaver
2
这每次都让我困惑。终于意识到“包”意味着你不能看里面的东西。就像在玩Scrabble游戏一样。 - undefined
我还没有找到 ConcurrentBag<T> 适用的案例,而且它一直被不断宣传作为 List<T> 的替代品。关于这个问题有很多错误信息和模糊之处,也有很多有用的信息,但几乎所有这些信息都没有正确地解释如何并发使用 List<T>。 - omJohn8372
9个回答

121

简而言之:没有简便的方法可以做到。

ConcurrentBag为每个线程保留了一个本地队列,只有在自己的队列为空时才会查看其他线程的队列。如果您移除并重新放置一个项目,则下一个要移除的项目可能仍然是同一个项目。不能保证重复删除项目并将其放回会使您能够遍历所有项目。

以下是两种替代方案:

  • 移除所有项目并记住它们,直到找到要移除的项目,然后再将其他项目放回去。请注意,如果两个线程同时尝试执行此操作,可能会出现问题。
  • 使用更适合的数据结构,例如ConcurrentDictionary

16
SynchronizedCollection也可能是一个合适的替代品。 - ILIA BROUDNO
3
@ILIABROUDNO -- 你应该把那个作为答案!当你不需要一个字典时,这比笨重的ConcurrentDictionary好得多。 - Denis
4
请注意,SynchronizedCollection在.NET Core中不可用。 截至本评论日期,System.Collections.Concurrent类型是基于.NET Core的实现中的当前选择。 - Matthew Snyder
2
我不确定正在使用哪个版本的.NET Core,但我正在基于.NET Core 2.1 SDK的项目上工作,现在在Collections.Generic命名空间中可用SynchronizedCollection。 - Lucas Leblanc
请注意,SynchronizedCollection 表示它将集合实现为 List<T>。因此,移除操作的时间复杂度为O(n)。 - Oliver Bock

19

你做不到。这是一个“袋子”,它没有顺序。当你把它放回去时,你只会陷入无限循环。

你需要一个集合(Set)。你可以用ConcurrentDictionary模拟一个集合,或者使用带锁的HashSet来保护自己。


10
请扩展。在底层ConcurrentDictionary中,您将使用什么作为关键字? - Denise Skidmore
2
我认为关键点在于您尝试存储的对象类型,然后值将是某种集合。这将“模拟”他所描述的 HashSet - Mathias Lykkegaard Lorenzen

14

ConcurrentBag非常适合处理列表,您可以从多个线程添加项目并枚举,最终根据其名称的建议扔掉它 :)

正如Mark Byers所说,您可以重新构建一个不包含要删除项的新ConcurrentBag,但必须使用锁来保护它以防止多个线程同时操作。这是一行代码:

myBag = new ConcurrentBag<Entry>(myBag.Except(new[] { removedEntry }));

这个方法是有效的,并符合ConcurrentBag的设计理念。


18
我认为这个答案有误导性。明确地说,这并不在所需的"Remove操作"中提供任何线程安全保证。而在它周围添加锁会有点违背使用并发集合的初衷。 - ILIA BROUDNO
1
我同意。嗯,稍微澄清一下,ConcurrentBag 旨在填充、枚举并在完成时与其整个内容一起丢弃。任何尝试 - 包括我的尝试 - 删除项目都将导致一个不好的 hack。至少我试图提供一个答案,尽管最好使用一个更好的并发集合类,比如 ConcurrentDictionary。 - Larry

5

Mark说的没错,ConcurrentDictionary可以按照你想要的方式工作。如果你仍然想使用ConcurrentBag,以下方法(虽然不够高效)可以帮助你实现目标。

var stringToMatch = "test";
var temp = new List<string>();
var x = new ConcurrentBag<string>();
for (int i = 0; i < 10; i++)
{
    x.Add(string.Format("adding{0}", i));
}
string y;
while (!x.IsEmpty)
{
    x.TryTake(out y);
    if(string.Equals(y, stringToMatch, StringComparison.CurrentCultureIgnoreCase))
    {
         break;
    }
    temp.Add(y);
}
foreach (var item in temp)
{
     x.Add(item);
}

4

如您所述,TryTake()是唯一的选择。这也是在MSDN上的示例。反编译程序也没有显示任何其他有用的隐藏内部方法。


0
public static void Remove<T>(this ConcurrentBag<T> bag, T item)
{
    while (bag.Count > 0)
    {
        T result;
        bag.TryTake(out result);

        if (result.Equals(item))
        {
            break; 
        }

        bag.Add(result);
    }

}

2
ConcurrentBag是一个无序的集合,但是你的代码期望bag.TryTakebag.Add按照FIFO的方式工作。 你的代码假设bag包含item,并且它会循环直到在bag中找到item。 仅提供代码答案是不鼓励的,你应该解释你的解决方案。 - GDavid
在你的例子中,有些情况下集合中将没有其他线程所需的项。 - nim

-4

这是我在项目中使用的扩展类。它可以从ConcurrentBag中删除单个项,也可以从包中删除一组项。

public static class ConcurrentBag
{
    static Object locker = new object();

    public static void Clear<T>(this ConcurrentBag<T> bag)
    {
        bag = new ConcurrentBag<T>();
    }


    public static void Remove<T>(this ConcurrentBag<T> bag, List<T> itemlist)
    {
        try
        {
            lock (locker)
            {
                List<T> removelist = bag.ToList();

                Parallel.ForEach(itemlist, currentitem => {
                    removelist.Remove(currentitem);
                });

                bag = new ConcurrentBag<T>();


                Parallel.ForEach(removelist, currentitem =>
                {
                    bag.Add(currentitem);
                });
            }

        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }

    public static void Remove<T>(this ConcurrentBag<T> bag, T removeitem)
    {
        try
        {
            lock (locker)
            {
                List<T> removelist = bag.ToList();
                removelist.Remove(removeitem);                

                bag = new ConcurrentBag<T>();

                Parallel.ForEach(removelist, currentitem =>
                {
                    bag.Add(currentitem);
                });
            }

        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }
}

很难相信它能够工作,因为你正在创建一个新的ConcurrentBag在本地变量中,但我不确定。有任何测试吗? - shtse8
3
不,它不起作用。它应该如何工作呢?你只是创建了一个新的引用,旧的引用仍指向旧对象... 如果使用“ref”,它就能起作用。 - Bin4ry

-6
public static ConcurrentBag<String> RemoveItemFromConcurrentBag(ConcurrentBag<String> Array, String Item)
{
    var Temp=new ConcurrentBag<String>();
    Parallel.ForEach(Array, Line => 
    {
       if (Line != Item) Temp.Add(Line);
    });
    return Temp;
}

-16

怎么样:

bag.Where(x => x == item).Take(1);

它可以工作,但我不确定效率如何...


2
这不会从袋子里移除任何物品。您要检索的物品仍然在袋子里。 - Keith
5
应为“bag = new ConcurrentBag(bag.Where(x => x != item))”,意思不变,通俗易懂。 - atikot
6
@atikot,这句话让我笑了。 - parek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接