生产者 - 消费者集合[1] [2]的所有C#实现似乎都有类似于以下的接口:
private Queue<T> items;
public void Produce(T item)
public T Consume()
那里的所有实现如下?
private Queue<T> items;
public void Produce(T[] item)
public T[] Consume(int count)
希望这会让我一次产生/消耗不同数量的物品,而不需要过多的物品锁定。这似乎是生产/消费大量物品所必需的,但我没有找到任何实施的运气。
答案 0 :(得分:4)
根据您要实施的具体内容,有多种可能的方式。
有IProducerConsumerCollection<T>
接口的实现。据我所知,在.NET框架中唯一的线程安全实现是BlockingCollection<T>
。
此类允许您拥有阻止或非阻塞生成器和使用者。通过在构造函数中为集合提供容量限制,生成器端在阻塞和非阻塞之间设置。正如BlockingCollection<T>.Add(T)
方法的文档所述:
如果在初始化此
BlockingCollection<T>
实例时指定了有界容量,则对Add的调用可能会阻塞,直到空间可用于存储提供的项目。
对于提取项目,您可以使用不同的Take
和TryTake
方法或非常方便的BlockingCollection<T>.GetConsumingEnumerable()
方法创建IEnumerable<T>
创建IEnumerator<T>
的方法在获取下一个值时消耗BlockingCollection<T>
中的一个元素,并在源集合为空时阻塞。这是在调用BlockingCollection<T>.CompleteAdding()
之前,集合不接受任何新数据。此时,所有消耗可枚举实例的实例都将停止阻塞并报告不再有数据(只要消耗了所有剩余数据)。
所以你基本上可以实现这样的消费者:
BlockingCollection<...> bc = ...
foreach (var item in bc.GetConsumingEnumerable())
{
// do something with your item
}
这样的消费者可以在多个线程中启动,因此如果您选择,您可以从源中读取多个线程。您可以创建任意数量的消费枚举。
你应该知道这个集合实际上只是一个包装器。有一个构造函数,允许您设置使用的集合类型。默认情况下为ConcurrentQueue<T>
。这意味着默认情况下,集合的行为类似于此队列,并且是先进先出集合,以防您只使用一个生产者和一个消费者。
所有这一切,都有另一种选择。如果您不需要阻止部分(或者您想自己实施阻止部分),并且如果您不需要集合中的任何元素顺序,则可以使用ConcurrentBag<T>
。此集合非常有效地处理来自多个线程的访问。它在ThreadLocal<T>
包装器中使用较小的集合。因此,每个线程使用它自己的存储,并且只有当线程耗尽其自身存储中的项目时,它才开始从另一个线程存储中获取项目。
如果在您的使用案例中按顺序生成和消费,则使用此集合可能会很有趣。因此,您首先添加所有项目,一旦完成,您将使用所有项目,包括多个线程。
答案 1 :(得分:3)
希望这会让我一次产生/消耗不同数量的物品而不需要过多的物品锁定。
您可以使用BlockingCollection<T>
课程;虽然它没有添加或删除多个项目的方法,但它不会在内部使用锁。
答案 2 :(得分:0)
正是需要这个,我自己创建了一个方法扩展。请注意,如果从队列中删除了至少一个元素,则此调用将记录所有其他异常,并返回此一个元素,以防止丢失任何内容。
public static class BlockingCollectionMethodExtensions
{
public static List<T> FetchAtLeastOneBlocking<T>(this BlockingCollection<T> threadSafeQueue, int maxCount, ICommonLog log)
{
var resultList = new List<T>();
// Take() will block the thread until new elements appear
// It will also throw an InvalidOperationException when blockingCollection is Completed
resultList.Add(threadSafeQueue.Take());
try
{
// Fetch more unblocking
while (threadSafeQueue.Count > 0 && resultList.Count < maxCount)
{
T item;
bool success = false;
success = threadSafeQueue.TryTake(out item);
if (success)
{
resultList.Add(item);
}
else
{
}
}
}
catch (Exception ex)
{
log.Fatal($"Unknown error fetching more elements. Continuing to process the {resultList.Count} already fetched items.", ex);
}
return resultList;
}
}
以及相应的测试:
public class BlockingCollectionMethodExtensionsTest : UnitTestBase
{
[Fact]
public void FetchAtLeastOneBlocking_FirstEmpty_ThenSingleEntryAdded_ExpectBlocking_Test()
{
var queue = new BlockingCollection<int>();
var startEvent = new ManualResetEvent(initialState: false);
var completedEvent = new ManualResetEvent(initialState: false);
List<int> fetchResult = null;
var thread = new Thread(() =>
{
startEvent.Set();
fetchResult = queue.FetchAtLeastOneBlocking<int>(maxCount: 3, log: null);
completedEvent.Set();
});
thread.Start();
var startedSuccess = startEvent.WaitOne(TimeSpan.FromSeconds(2)); // Wait until started
Assert.True(startedSuccess);
// Now wait for 2 seconds to ensure that nothing will be fetched
Thread.Sleep(TimeSpan.FromSeconds(1));
Assert.Null(fetchResult);
// Add a new element and verify that the fetch method succeeded
queue.Add(78);
var completedSuccess = completedEvent.WaitOne(timeout: TimeSpan.FromSeconds(2));
Assert.True(completedSuccess);
Assert.NotNull(fetchResult);
Assert.Single(fetchResult);
Assert.Equal(78, fetchResult.Single());
}
[Fact]
public void FetchAtLeastOneBlocking_FirstEmpty_ThenCompleted_ExpectOperationException_Test()
{
var queue = new BlockingCollection<int>();
Exception catchedException = null;
var startEvent = new ManualResetEvent(initialState: false);
var exceptionEvent = new ManualResetEvent(initialState: false);
List<int> fetchResult = null;
var thread = new Thread(() =>
{
startEvent.Set();
try
{
fetchResult = queue.FetchAtLeastOneBlocking<int>(maxCount: 3, log: null);
}
catch (Exception ex)
{
catchedException = ex;
exceptionEvent.Set();
}
});
thread.Start();
var startedSuccess = startEvent.WaitOne(TimeSpan.FromSeconds(2)); // Wait until started
Assert.True(startedSuccess);
// Now wait for 2 seconds to ensure that nothing will be fetched
Thread.Sleep(TimeSpan.FromSeconds(1));
Assert.Null(fetchResult);
// Now complete the queue and assert that fetching threw the expected exception
queue.CompleteAdding();
// Wait for the exception to be thrown
var exceptionSuccess = exceptionEvent.WaitOne(TimeSpan.FromSeconds(2));
Assert.True(exceptionSuccess);
Assert.NotNull(catchedException);
Assert.IsType<InvalidOperationException>(catchedException);
}
[Fact]
public void FetchAtLeastOneBlocking_SingleEntryExists_ExpectNonblocking_Test()
{
var queue = new BlockingCollection<int>();
// Add a new element and verify that the fetch method succeeded
queue.Add(78);
var startEvent = new ManualResetEvent(initialState: false);
var completedEvent = new ManualResetEvent(initialState: false);
List<int> fetchResult = null;
var thread = new Thread(() =>
{
startEvent.Set();
fetchResult = queue.FetchAtLeastOneBlocking<int>(maxCount: 3, log: null);
completedEvent.Set();
});
thread.Start();
var startedSuccess = startEvent.WaitOne(TimeSpan.FromSeconds(2)); // Wait until started
Assert.True(startedSuccess);
// Now wait for expected immediate completion
var completedSuccess = completedEvent.WaitOne(timeout: TimeSpan.FromSeconds(2));
Assert.True(completedSuccess);
Assert.NotNull(fetchResult);
Assert.Single(fetchResult);
Assert.Equal(78, fetchResult.Single());
}
[Fact]
public void FetchAtLeastOneBlocking_MultipleEntriesExist_ExpectNonblocking_Test()
{
var queue = new BlockingCollection<int>();
// Add a new element and verify that the fetch method succeeded
queue.Add(78);
queue.Add(79);
var startEvent = new ManualResetEvent(initialState: false);
var completedEvent = new ManualResetEvent(initialState: false);
List<int> fetchResult = null;
var thread = new Thread(() =>
{
startEvent.Set();
fetchResult = queue.FetchAtLeastOneBlocking<int>(maxCount: 3, log: null);
completedEvent.Set();
});
thread.Start();
var startedSuccess = startEvent.WaitOne(TimeSpan.FromSeconds(2)); // Wait until started
Assert.True(startedSuccess);
// Now wait for expected immediate completion
var completedSuccess = completedEvent.WaitOne(timeout: TimeSpan.FromSeconds(2));
Assert.True(completedSuccess);
Assert.NotNull(fetchResult);
Assert.Equal(2, fetchResult.Count);
Assert.Equal(78, fetchResult[0]);
Assert.Equal(79, fetchResult[1]);
}
[Fact]
public void FetchAtLeastOneBlocking_MultipleEntriesExist_MaxCountExceeded_ExpectNonblocking_Test()
{
var queue = new BlockingCollection<int>();
// Add a new element and verify that the fetch method succeeded
queue.Add(78);
queue.Add(79);
queue.Add(80);
queue.Add(81);
var startEvent = new ManualResetEvent(initialState: false);
var completedEvent = new ManualResetEvent(initialState: false);
List<int> fetchResult = null;
var thread = new Thread(() =>
{
startEvent.Set();
fetchResult = queue.FetchAtLeastOneBlocking<int>(maxCount: 3, log: null);
completedEvent.Set();
});
thread.Start();
var startedSuccess = startEvent.WaitOne(TimeSpan.FromSeconds(2)); // Wait until started
Assert.True(startedSuccess);
// Now wait for expected immediate completion
var completedSuccess = completedEvent.WaitOne(timeout: TimeSpan.FromSeconds(2));
Assert.True(completedSuccess);
Assert.NotNull(fetchResult);
Assert.Equal(3, fetchResult.Count);
Assert.Equal(78, fetchResult[0]);
Assert.Equal(79, fetchResult[1]);
Assert.Equal(80, fetchResult[2]);
}
}