【问题标题】:How to prevent a method of a class to be called before the class is ready?如何防止在类准备好之前调用类的方法?
【发布时间】:2010-06-20 23:24:04
【问题描述】:

这是我的自定义“记录器”类,它可以帮助我在项目中创建日志文件。

namespace MyProject
{
class Logger
{
    private FileInfo fFile;
    private DirectoryInfo dDir;

    /// <summary>Add a new entry to the log file.</summary>
    /// <param name="sData">The line to add.</param>
    public void Add(string sData)
    {
        DateTime CurrTime = DateTime.Now;

        if (fFile.Length > 1048576)
        {
            fFile.MoveTo(Path.Combine(dDir.FullName, CurrTime.ToShortDateString() + fFile.Name));
            fFile = new FileInfo(Path.Combine(dDir.FullName,fFile.Name));
            using (StreamWriter sw = fFile.CreateText())
            {
                sw.WriteLine("{0:u}|{1}", CurrTime, sData);
            }
        }
        else
        {
            using (StreamWriter sw = fFile.AppendText())
            {
                sw.WriteLine("{0:u}|{1}", CurrTime, sData);
            }
        }
    }

    /// <summary>Logger instance</summary>
    /// <param name="sFile">Full name of the file to use as logs. Ex : "MyLogs.txt"</param>
    public Logger(string sFile)
    {
        dDir = new DirectoryInfo(Path.Combine(MyProject.AppPath, "logs"));
        if (!dDir.Exists)
        {
            dDir.Create();
        }

        fFile = new FileInfo(Path.Combine(dDir.FullName,sFile));

        if (!fFile.Exists)
        {
            using (StreamWriter sw = fFile.CreateText())
            {
                sw.WriteLine("{0:u}|Logger Started", DateTime.Now);
            }
        }
        else
        {
            Add("Logger Started");
        }           
    }
}
}

显然,我对这段代码的问题是,有时在该记录器的新实例有时间创建文件之前调用 Logger.Add。所以我的程序崩溃说“找不到文件”,但是,文件最终被创建,如果我使用相同的日志文件名重新启动我的程序,一切正常(因为文件现在存在......)

除了确保在创建文件之前不调用 logger.add 之外,有没有办法“锁定”类?

我试过lock方法,但是没用……Lock(this)没有做任何事情,我不能在方法本身上使用它。

【问题讨论】:

  • 你能试试if file.exists,如果不存在,运行system.threadding.threadding.sleep(10)再试一次。这有点小技巧,但它基本上会让你的应用程序在执行 add 方法之前“等待”。
  • @rockinthesixstring - 这将起作用,因为代码仍需要在第二个线程调用该方法之前创建文件。如果两个线程都休眠 10 毫秒,它们仍然可能发生冲突。
  • 这完全超出了左侧字段,但可能值得查看 Log4Net 以了解如何进行日志记录。使用现有的轮子,而不是重新发明它们;-)。当然,这是假设您还没有尝试过。如果该假设不正确,请忽略此评论并接受我的道歉。
  • 我没有看过log4net,我不想重新发明轮子,但是由于我还在学习c#,所以我想做我认为“简单”的东西我自己……虽然这可能确实比我想象的要复杂。
  • 您能否发布足够的代码来重现该问题?或者至少是异常的堆栈跟踪?目前还不清楚是什么导致了问题。

标签: c# class methods locking


【解决方案1】:

这个异常其实不是因为文件不存在,而是因为 FileInfo 实例是陈旧的!您在文件不存在时创建了 FileInfo,它会在此时拍摄文件状态的快照。当我测试它时,当您在 Add 方法中调用 fFile.Length 时会引发异常。如果我添加对fFile.Refresh() 的调用,我发现它有效:

...
DateTime currTime = DateTime.Now;
fFile.Refresh();
if (fFile.Length > 1048576)
...

看这里:

http://msdn.microsoft.com/en-us/library/system.io.filesysteminfo.refresh.aspx

“在尝试获取属性信息之前,必须先调用 Refresh,否则信息将过时。”

【讨论】:

    【解决方案2】:

    问题是IO操作已经被缓存了。这在理论上不应该是一个问题,但实际上它是。

    您可以在构造函数中调用 sw.Flush()。这将强制文件从缓存到磁盘,从而创建文件。

    if (!fFile.Exists) 
    { 
        using (StreamWriter sw = fFile.CreateText()) 
        { 
            sw.WriteLine("{0:u}|Logger Started", DateTime.Now); 
            sw.Flush();
        } 
    }
    

    【讨论】:

      【解决方案3】:

      我个人不会走这条路,锁定班级。我会取出创建日志文件的代码并使其成为自己的方法。然后在调用 add 方法的时候,加入一些逻辑来检查日志文件是否存在(这个逻辑在我看来也应该是它自己的方法)。如果文件存在,继续记录,如果不存在,则创建日志文件(使用上面提取的方法),然后一旦该方法成功返回,继续写入日志。

      【讨论】:

      • 我想这可能是我要做的,我从一开始就没有这样做,因为我希望只做一次检查就可以逃脱......但还有更多如果避免崩溃,那么运行该代码的毫秒数是值得的 :)
      【解决方案4】:

      在方法中,检查类是否“准备就绪”(无论是什么),如果不是等待,则返回错误或您需要做的任何事情。

      【讨论】:

        【解决方案5】:

        您可以尝试使用FileSystemWatcher 类并处理事件以使该类准备就绪。这样,事件可以作为委托添加,并且只有在创建相关文件后才会调用。 Thread.Sleep 可能是处理器密集型的,并导致其他进程在该进程预期完成时锁定。

        【讨论】:

          【解决方案6】:

          在您的构造函数中,您正在设置字段 fFile 和 dDir,而不是您应该有一个作为流写入器的字段,并在 Add 方法中使用它。

          【讨论】:

          • 我会调查一下,不过,除非我错了,否则这并不能解决我在创建文件之前调用 Add 方法的问题,并且让我的程序崩溃。
          • 我认为这可以解决您的问题。这不会在写入过程中出现文件未找到错误而失败,因为文件未找到仅在打开文件时发生。
          • 为了绝对确定这会解决您的问题,我需要知道崩溃实际发生在哪里。但是通过阅读代码,我会说它发生在 Add 方法中 if 语句的“else”子句中。问题是在文件系统更新之前调用了 Add 方法 - 如果您使用相同的流写入器,这将不是问题。
          【解决方案7】:

          您希望在创建文件之前阻止您的 add 方法运行 - 您可以使用 EventWaitHandle 或其他一些线程控制机制(可能使用监视器类)来执行此操作。

          并且还使用Lock 将文件的使用一次限制为一个线程,您不希望两个线程同时运行Add - 并且都试图同时移动文件。

          EventWaitHandle _handle = new EventWaitHandle (false, EventResetMode.ManualReset);
          Object _fileLock = new Object();
          
          
           public Logger(string sFile)
          {
          
          // Do as you do here
          
          _handle.Set();
          }
          
          
          public void Add(string sData)
          {
              _handle.WaitOne ();    // Blocks the thread untill Set() is called on _handle
          
          
              Lock(_fileLock){    // Only one thread may enter at a time
                 // Do as you do already
              }
          
          }
          

          再想一想,在您的 ctor 代码周围使用 Lock(_fileLock){ 应该与 WaitHandle 完成相同的工作。

          【讨论】:

            【解决方案8】:

            当类的构造与编译器工作一样快时,该类应该已准备就绪。除此之外,任何逻辑都必须进行编程。

                fFile.MoveTo(Path.Combine(dDir.FullName, CurrTime.ToShortDateString() + fFile.Name));
                fFile = new FileInfo(Path.Combine(dDir.FullName,fFile.Name));
            

            这看起来像是竞态条件,需要序列化。即使在使用锁 (perl/log4perl) 之后,我们也遇到了一次性问题,最终我们更喜欢为每个进程使用单独的日志文件。 除非有多个 Logger 对象处于活动状态(即使是跨进程,因为我们正在处理文件)这应该不是问题。

            奇怪,C# 中没有 log4 等效项吗?

            【讨论】:

              【解决方案9】:

              编辑 - 当我没有 C# 编译器时,我在 Linux 机器上编写了这个。在 Windows 机器上测试代码后,我发现这里观察到的问题实际上比我的猜测要简单得多。请参阅我的其他答案。

              你在这里使用多个线程吗?如果你是,你将需要采取不同的方法。例如,如果两个线程同时尝试调用 Add 并且其中一个或两个都尝试移动文件,您将遭受竞争条件。

              任何其他进程可以在您的程序运行时移动或锁定您的日志文件,如果他们这样做了,您的程序是否需要不崩溃?如果是这种情况,您将需要进行大量更改,因为您的所有文件 I/O 操作都可能失败。

              如果这些情况都不适用,那么您的问题似乎是文件存储在某个文件系统上,该文件系统在您创建文件和您能够观察到之间存在一定的延迟那个文件。我认为 NTFS 或 FAT32 不是这种情况。您是否将日志文件存储在网络共享上?在不了解更多信息的情况下,我想不出比采用 rockinthesixstring 的建议来测试文件是否存在以及如果不使用简短的 Sleep() 调用直到它确实存在(或者直到你等待足够长的时间以明确某些东西)更好的方法出错了——也许有人从你下面删除了文件)。

              请注意,如果问题与文件系统有关,则类中的任何锁定都无济于事,调用 Flush 也无济于事,因为“使用”语句确保 StreamWriter 已释放,这将关闭文件,除了具有与Flush几乎相同的效果外,因此Flush是多余的。

              说了这么多,你为什么一直关闭文件又打开呢?如果您的程序崩溃,是否确保不会丢失任何东西?如果是这样,您最好按照 jmoreno 的建议保持 TextWriter 处于打开状态,但在写完每一行后对其调用 Flush。

              【讨论】:

              • 我正在使用“using”构造,因为这就是 MSDN 上的示例中的用法,而且我认为该构造更具可读性和易用性。我不知道为什么文件没有“足够快”地创建。我正在笔记本电脑上运行程序...
              猜你喜欢
              • 2010-12-08
              • 1970-01-01
              • 2018-04-12
              • 2019-04-05
              • 2016-04-07
              • 1970-01-01
              • 1970-01-01
              • 2017-08-27
              • 1970-01-01
              相关资源
              最近更新 更多