3

我们有一个 C# 应用程序,它连接到 FTP 服务器,下载一些文件,断开连接,并在一定时间后(由用户通过 UI 选择)重新连接并重复该过程。我们使用 BackgroundWorker 实现了这一点,但我们注意到在运行较长时间后,程序停止在 UI 和日志文件中记录其操作。那时,它没有文件可供下载,所以我们上传了一些文件,它恢复了活动,就好像什么都没发生一样。

问题是普通用户无法知道该程序仍在运行,因此我们决定使用我们自己的线程来实现它。我们做了一个更简单的程序,以排除任何其他问题,这个程序只连接到 FTP 并断开连接。它停止显示消息,就像 BackgroundWorker 一样(2 小时后一次,22 小时后一次,没有我们能找到的任何模式,并且在没有其他任何操作的计算机上)。

DoFTPWork += new DoFTPWorkDelegate(WriteFTPMessage);

FTPWorkThread = new Thread(new ParameterizedThreadStart(Process));

//seData is the FTP login info
FTPWorkThread.Start(seData);

FTP方法是:

private void Process(object seData1)
{
    seData = (SEData)seData1;
    while (!stopped)
    {
        try
        {
            ftp = null;
            ftp = new FTP_Client();

            if (ftp.IsConnected)
            {
                logMessages += DateTime.Now + "\t" + "info" + "\t" + "Ftp disconnected from " + seData.host + "\r\n";
                ftp.Disconnect();
            }

            ftp.Connect(seData.host, 21);
            ftp.Authenticate(seData.userName, seData.password);
            logMessages += DateTime.Now + "\t" + "info" + "\t" + "Ftp connected to " + seData.host + "\r\n";

            error = false;
            logMessages += DateTime.Now + "\t" + "info" + "\t" + "Trying to reconnect in 5 seconds\r\n";
            System.Threading.Thread.Sleep(5000);
            SlaveEventArgs ev = new SlaveEventArgs();
            ev.Message = logMessages;
            txtLog.Invoke(DoFTPWork, ev);
            System.Threading.Thread.Sleep(200);
            logMessages = "";
        }

        catch (Exception ex)
        {
            logMessages = "";
            if (ftp.IsConnected)
            {
                ftp.Disconnect();
            }
            ftp.Dispose();
            logMessages += DateTime.Now + "\t" + "ERR" + "\t" + ex.Message + "\r\n";

            logMessages += DateTime.Now + "\t" + "info" + "\t" + "Trying to reconnect in 5 seconds\r\n";
            SlaveEventArgs ev = new SlaveEventArgs();
            ev.Message = logMessages;
            txtLog.Invoke(DoFTPWork, ev);
            System.Threading.Thread.Sleep(5 * 1000);
            error = true;
        }
    }
}

WriteFTPMessage 在 TextBox 中显示消息,并在原始程序中写入 .txt 文件。

4

5 回答 5

4

如果我理解正确,这个while(!stopped)循环是运行了几个小时的循环?如果是这种情况,如果在任何地方,您在哪里终止您的 ftp 连接?您在发布的代码中关闭它的唯一时间是抛出异常,否则您只需取消引用该对象并创建一个新的对象,这是一个非常严重的资源泄漏,如果不引起问题,至少会导致问题。

此外,似乎 ftp 是全球可访问的。您是否使用不同的线程在任何地方访问它?对象线程安全吗?

编辑:

我在这里看到的最大问题是设计。并不是说我要包庇你或其他任何东西,而是你已经混合了各种操作。线程、日志记录和 ftp 访问代码都在同一个函数中。

我建议的是重组你的程序。创建一个类似于以下的方法:

// Called by thread
void MyThreadOperation()
{
   while(!stopped)
   {
      // This is poor design in terms of performance.
      // Consider using a ResetEvent instead.
      Thread.Sleep(5000);

      try
      {
         doFTPDownload();
      }
      catch(Exception ex)
      {
         logMessage(ex.ToString());
      }
   }
}

doFTPDownload()应该是自给自足的。FTP 对象应该在被调用时在函数内部创建和打开,在它完成之前它应该被关闭。同样的概念也应该适用logMessage()。我还建议使用数据库而不是文件来存储日志消息,这样锁定问题就不会使事情复杂化。

我知道这不是一个答案,因为您可能仍然会遇到问题,因为我无法确定可能是什么原因。但是,我相信通过一些设计重组,您将能够更好地找到问题的根源。

于 2009-01-12T07:33:15.927 回答
2

我建议将任何可能出错的内容放在 catch 块中(特别是与 FTP 服务器断开连接的位)放在它自己的 try/catch 块中。此外,在您发现异常后立即记录一些内容,然后再执行其他任何操作 - 这样您就更有可能判断日志记录是否由于某种原因在中途死亡。

此外,在 while 循环的末尾添加一条日志消息,以便您可以判断它是否“正常”完成。

于 2009-01-12T07:34:18.860 回答
0

我建议在问题重现时使用 adplus 并让自己陷入挂起转储。在 Windbg 和 SoS 中进行分析。

是在 Winforms 应用程序中吗?也许 ISynchronizeInvoke 实现挂起。这是作为交互式用户运行的吗?

于 2009-01-12T07:48:08.830 回答
0

Rupert:我在 catch 块之后添加了 ftp.Disconnect() 并再次启动它。我检查了原始应用程序,并且在重新连接之前我们断开了连接,所以虽然它会影响问题,但我不认为它会导致它。没有其他线程访问它,所以那里没有问题。

乔恩:我会的,谢谢你的建议。

JD:这是一个Windows应用程序,在选择延迟和FTP连接数据后,用户没有输入任何内容。我会调查 ISynchronizeInvoke

于 2009-01-12T07:54:15.463 回答
0

我认为您必须努力使其更加线程安全。您有很多共享字段:ftp, logMessages, error.

例如这部分:

        ev.Message = logMessages;
        txtLog.Invoke(DoFTPWork, ev);
        System.Threading.Thread.Sleep(200);
        logMessages = "";

在我看来,你正试图通过睡觉和交叉手指来解决多线程问题,你睡得够多了......

您可以通过以下方式解决此问题:

        ev.Message = logMessages.Clone();
        txtLog.Invoke(DoFTPWork, ev);

或使用不同的沟通方式。

您可以使用ManualResetEvent代替停止的布尔值,这是一种线程安全的通信方法。对于错误,您可以使用相同的或信号量。

ManualResetEvent 的好处是您可以使用它来休眠您的线程,而无需完全锁定它。如果我没记错的话,在线程休眠时停止线程的唯一方法是调用 thread.Abort。如果您使用 ManualResetEvent,您可以执行以下操作:

if (!shouldStop.WaitOne(5000))
{
    // do thread stuff
}
else
{
    // do cleanup stuff and exit thread.
}

好消息是,你会说我想知道事件是否已发出信号,但我会等待 5 秒钟让它发出信号,否则我会继续未发出信号。

因此,如果您的应用程序决定在睡眠 3 秒后退出,它可以执行 shouldStop.Set() 并且线程将停止。线程仍然有可能正在与 ftp 服务器通信,所以在设置之后你应该执行一个 thread.Join() 来等待它退出。

我并不是说您的问题与我的建议有关,如果不是,我只是想帮助减少可能的原因。

于 2009-01-12T08:33:51.047 回答