我在多线程示例中做错了什么?

时间:2011-03-29 14:40:57

标签: c# asp.net multithreading

这是我进入多线程的第一次冒险,我想我缺少一些关键概念,所以任何帮助都将受到赞赏。我正在尝试为asp.net应用程序创建一个日志管理器。我们将在系统中的大量数据上记录视图/插入/修改/删除。而不是不断地插入行,我想如果我创建一个单例来保存内存中的日志条目列表,直到它达到一定大小,然后立即将它们全部写入数据库。然后我想可能在新线程上运行它会在需要编写日志时提高性能。下面是我的测试代码。如果我删除线程,我会在db中得到500行,但是当我使用多线程时,我会得到200-300左右。大约一半的记录没有被插入。这是多线程的有效用途,我做错了什么?谢谢。

日志管理:

  public sealed class LogManager
  {
    private static LogManager _Log = null;
    private static readonly object singletonLock = new object();
    private static readonly object listLock = new object();

    private List<LogEntry> LogEntries { get; set; }

    public static LogManager Log
    {
      get
      {
        if (_Log == null)
        {
          lock (singletonLock)
          {
            if (_Log == null)
            {
              _Log = new LogManager();
            }
          }
        }
        return _Log;
      }
    }

    public LogManager()
    {
      LogEntries = new List<LogEntry>();
    }

    public void Add(LogEntry logEntry)
    {
      lock (listLock)
      {
        LogEntries.Add(logEntry);
        if (LogEntries.Count >= 100)
        {          
          ThreadStart thread = delegate { Flush(new List<LogEntry>(LogEntries)); };
          new Thread(thread).Start();
          //Flush(LogEntries);          
          LogEntries.Clear();
        }
      }
    }

    private static void Flush(List<LogEntry> logEntries)
    {
      using (var conn = new SqlConnection(DAL.ConnectionString))
      {
        using (var cmd = conn.CreateCommand())
        {
          cmd.CommandType = CommandType.StoredProcedure;
          cmd.CommandText = "spInsertLog";
          conn.Open();
          foreach (var logEntry in logEntries)
          {
            cmd.Parameters.AddWithValue("@ID", logEntry.ID);
            try
            {
              cmd.ExecuteNonQuery();
            }
            catch (Exception ex) { throw (ex);/*KeepGoing*/}
            cmd.Parameters.Clear();
          }
        }      
      }
    }
  }

控制台应用:

  class Program
  {
    static void Main(string[] args)
    {
      var stopwatch = new Stopwatch();      
      for (int i = 0; i < 500; i++)
      {
        stopwatch.Start();
        LogManager.Log.Add(new LogEntry() { ID = i });
        Console.WriteLine(String.Format("Count: {0}   Time: {1}",i.ToString(),stopwatch.ElapsedMilliseconds));
        stopwatch.Stop();
        stopwatch.Reset();
      }      
    }
  }

3 个答案:

答案 0 :(得分:3)

ThreadStart thread = delegate { Flush(new List<LogEntry>(LogEntries)); };
      new Thread(thread).Start();
      //Flush(LogEntries);          
      LogEntries.Clear();

List<LogEntry>是参考类型。您的新线程开始插入它们,但是在完成之前您将清除该列表。如果不使用多线程,则在清除之前等待刷新整个列表。您可以通过更改Flush的签名来获取数组并执行

来解决此问题
ThreadStart thread = delegate { Flush(LogEntries.ToArray()); };
      new Thread(thread).Start();
      //Flush(LogEntries);          
      LogEntries.Clear();

答案 1 :(得分:1)

在分析单行代码之前,Log Messages的中间存储位置错误。我强烈建议在等待LogManager处理时使用MSMQ或其他一些排队机制存储您的消息。

您在一个单独的线程中调用Flush并将引用传递给您的日志条目列表,然后清除当前线程中的列表。您已经有效地销毁了新线程应该记录的条目列表。在清除LogEntries字段之前,您需要将LogEntries列表的副本传递到Flush主题。

也许是这样的:

{Flush(LogEntries.ToList())}

LINQ表达式ToList()将为您的Flush方法创建一个列表副本。

顺便说一句,我会改变您的Flush方法以获取IEnumerable<LogEntry>,以便您可以将其他集合(而不仅仅是列表)传递给方法。

答案 2 :(得分:1)

我看到的一些事情:首先,我不会使用List<>我会使用Queue<>,它更适合这种情况。其次,在您关闭线程后立即清除列表。因此,当线程实际开始执行其代码时,很有可能列表已经为空。 Queue<>应该有助于解决此问题,因为您可以在将数据写入数据库时​​从队列中删除项目。

此外,您应该在访问列表时锁定代码,如果在迭代时将项目添加到列表中,则可能会出现异常。这也适用于Queue<>,我通常做的是:

LogEntry myEntry;
lock(sync) {
   myEntry = myQueue.Dequeue();
}

然后还锁定您的Add方法(您这样做)。