关于在多线程应用程序中正确使用.NET Queue
的主题有很多问题和文章,但是我找不到具体问题的主题。
我们有一个Windows服务,它通过一个线程将消息接收到队列中,然后在另一个线程中出列并处理。
我们在排队和出列时使用lock
,并且服务运行良好了大约2年没有任何问题。有一天,我们注意到已经记录了数千条消息(因此已排队),但从未出列/处理过,它们似乎已经以某种方式被跳过,这对于队列来说是不可能的。
我们无法复制导致它的情况,因为我们并不知道究竟是什么导致了它,因为我们知道这一天与其他任何一天都没有什么不同。
我们唯一的想法是处理队列的并发性。我们没有使用我们计划使用的ConcurrentQueue
数据类型,希望它是一种补救措施。
查看Queue类型的来源的一个想法是它在内部使用数组,一旦这些缓冲区达到一定长度就必须调整大小。我们假设在完成此操作时,一些消息丢失了。
我们的开发经理的另一个想法是,在多核处理器设置上使用多个线程意味着即使使用了锁,各个核也在处理其本地寄存器中的数据,这可能导致它们处理不同的数据。他说他们不会在相同的内存上工作,似乎认为lock
只能按预期使用多个线程的单个核心处理器。
阅读有关ConcurrentQueue使用volatile
的更多信息我不确定这会有所帮助,因为我已经读过使用锁提供了使用最新内存状态的线程的更强保证。
我对这个特定的主题知之甚少,所以我的问题是经理的想法是否合理,以及我们是否可能错过了正确使用队列所需的东西。
代码片段供参考(原谅凌乱的代码,它确实需要重构):
public sealed class Message
{
public void QueueMessage(long messageId, Message msg)
{
lock (_queueLock)
{
_queue.Enqueue(new QueuedMessage() { Id = messageId, Message = msg });
}
}
public static void QueueMessage(string queueProcessorName, long messageId, Message msg)
{
lock (_messageProcessors[queueProcessorName]._queueLock)
{
_messageProcessors[queueProcessorName].QueueMessage(messageId, msg);
_messageProcessors[queueProcessorName].WakeUp(); // Ensure the thread is awake
}
}
public void WakeUp()
{
lock(_monitor)
{
Monitor.Pulse(_monitor);
}
}
public void Process()
{
while (!_stop)
{
QueuedMessage currentMessage = null;
try
{
lock (_queueLock)
{
currentMessage = _queue.Dequeue();
}
}
catch(InvalidOperationException i)
{
// Nothing in the queue
}
while(currentMessage != null)
{
IContext context = new Context();
DAL.Message msg = null;
try
{
msg = context.Messages.SingleOrDefault(x => x.Id == currentMessage.Id);
}
catch (Exception e)
{
// TODO: Handle these exceptions better. Possible infinite loop.
continue; // Keep retrying until it works
}
if (msg == null) {
// TODO: Log missing message
continue;
}
try
{
msg.Status = DAL.Message.ProcessingState.Processing;
context.Commit();
}
catch (Exception e)
{
// TODO: Handle these exceptions better. Possible infinite loop.
continue; // Keep retrying until it works
}
bool result = false;
try {
Transformation.TransformManager mgr = Transformation.TransformManager.Instance();
Transformation.ITransform transform = mgr.GetTransform(currentMessage.Message.Type.Name, currentMessage.Message.Get("EVN:EventReasonCode"));
if (transform != null){
msg.BeginProcessing = DateTime.Now;
result = transform.Transform(currentMessage.Message);
msg.EndProcessing = DateTime.Now;
msg.Status = DAL.Message.ProcessingState.Complete;
}
else {
msg.Status = DAL.Message.ProcessingState.Failed;
}
context.Commit();
}
catch (Exception e)
{
try
{
context = new Context();
// TODO: Handle these exceptions better
Error err = context.Errors.Add(context.Errors.Create());
err.MessageId = currentMessage.Id;
if (currentMessage.Message != null)
{
err.EventReasonCode = currentMessage.Message.Get("EVN:EventReasonCode");
err.MessageType = currentMessage.Message.Type.Name;
}
else {
err.EventReasonCode = "Unknown";
err.MessageType = "Unknown";
}
StringBuilder sb = new StringBuilder("Exception occured\n");
int level = 0;
while (e != null && level < 10)
{
sb.Append("Message: ");
sb.Append(e.Message);
sb.Append("\nStack Trace: ");
sb.Append(e.StackTrace);
sb.Append("\n");
e = e.InnerException;
level++;
}
err.Text = sb.ToString();
}
catch (Exception ne) {
StringBuilder sb = new StringBuilder("Exception occured\n");
int level = 0;
while (ne != null && level < 10)
{
sb.Append("Message: ");
sb.Append(ne.Message);
sb.Append("\nStack Trace: ");
sb.Append(ne.StackTrace);
sb.Append("\n");
ne = ne.InnerException;
level++;
}
EventLog.WriteEntry("Service", sb.ToString(), EventLogEntryType.Error);
}
}
try
{
context.Commit();
lock (_queueLock)
{
currentMessage = _queue.Dequeue();
}
}
catch (InvalidOperationException e)
{
currentMessage = null; // No more messages in the queue
}
catch (Exception ne)
{
StringBuilder sb = new StringBuilder("Exception occured\n");
int level = 0;
while (ne != null && level < 10)
{
sb.Append("Message: ");
sb.Append(ne.Message);
sb.Append("\nStack Trace: ");
sb.Append(ne.StackTrace);
sb.Append("\n");
ne = ne.InnerException;
level++;
}
EventLog.WriteEntry("Service", sb.ToString(), EventLogEntryType.Error);
}
}
lock (_monitor)
{
if (_stop) break;
Monitor.Wait(_monitor, TimeSpan.FromMinutes(_pollingInterval));
if (_stop) break;
}
}
}
private object _monitor = new object();
private int _pollingInterval = 10;
private volatile bool _stop = false;
private object _queueLock = new object();
private Queue<QueuedMessage> _queue = new Queue<QueuedMessage>();
private static IDictionary<string, Message> _messageProcessors = new Dictionary<string, Message>();
}
答案 0 :(得分:1)
所以我的问题是经理的想法听起来是否合理
嗯。没有。如果所有这些同步措施只适用于单核机器,世界将在几十年前的完整混乱中结束。
以及我们是否可能遗漏了正确使用队列所需的内容。
就你的描述而言,你应该没事。我会看看你如何找到你有这个问题。如果我只是关闭服务或重新启动机器,那么日志会进入然后在没有正常出列的情况下消失,这不是默认情况吗?当你的应用程序实际上正在运行时,你确定丢失了它们吗?
答案 1 :(得分:1)
您将用于锁定的对象声明为私有对象。
如果您尝试这样做:
class Program
{
static void Main(string[] args)
{
Test test1 = new Test();
Task Scan1 = Task.Run(() => test1.Run("1"));
Test test2 = new Test();
Task Scan2 = Task.Run(() => test2.Run("2"));
while(!Scan1.IsCompleted || !Scan2.IsCompleted)
{
Thread.Sleep(1000);
}
}
}
public class Test
{
private object _queueLock = new object();
public async Task Run(string val)
{
lock (_queueLock)
{
Console.WriteLine($"{val} locked");
Thread.Sleep(10000);
Console.WriteLine($"{val} unlocked");
}
}
}
您会注意到即使另一个线程在内部运行,也会执行锁定下的代码。
但是如果你改变了
private object _queueLock = new object();
要
private static object _queueLock = new object();
它会改变锁定的工作方式。
现在,这是你的问题取决于你是否有多个实例,或者所有类都在运行同一个类。