在Windows服务中异步运行外部进程

5
我正在编写一个程序,将csv文件从“队列”文件夹移动到“处理”文件夹,然后启动名为import.exe的第三方进程,并将csv文件路径作为参数传递。Import.exe是一个长时间运行的任务。
我需要程序继续运行并检查队列中是否有新文件。出于这个原因,我选择了Windows服务应用程序,因为它将长时间运行。
我的问题是,我被选项所压倒,无法理解我是否应该使用后台线程并行编程来解决这个问题,或者最可能是两者的结合。
到目前为止,我只是同步运行这段代码。您很快就会看到,目前我只是疯狂地启动进程,没有任何管理或检查完成的方式。我已经注释掉了process.WaitForExit(),因为这显然是一个阻塞调用。
public int maxConcurrentProcesses = 10;
protected override void OnStart(string[] args)
    {
        // Set up a timer to trigger every minute.
        System.Timers.Timer timer = new System.Timers.Timer(60000);            
        timer.Elapsed += new System.Timers.ElapsedEventHandler(this.OnTimer);
        timer.Start();
    }

private void OnTimer(object sender, System.Timers.ElapsedEventArgs args)
    {            
        // How many instances of import.exe are running?
        Process[] importProcesses = Process.GetProcessesByName("import");
        int countRunning = importProcesses.Count();

        // If there are less than maxConcurrentProcesses, create as many as needed to reach maxConcurrentProcesses
        if (countRunning < maxConcurrentProcesses)
        {
            int processesToStart = maxConcurrentProcesses - countRunning;
            for (int i = 0; i < processesToStart; i++)
            {
                FireOffImport();
            }
        }
    }

private void FireOffImport()
    {
        // Get the first file returned from the Queue folder
        string filePathSource = GetNextCSVInQueue();

        if (filePathSource != "")
        {
            // …
            // commandArguments = create our arguments here
            // …        
            // Move the file to processing folder here
            // … 

            // Give a new process the import tool location and arguments
            ProcessStartInfo startInfo = new ProcessStartInfo(importLocation + "\\import.exe", commandArguments);
            try
            {
                Process process = Process.Start(startInfo);
                // process.WaitForExit(20000);
                // If the process has exited, there will be 4 csv files created in the same directory as the file.                  
            }
            catch (Exception ex)
            {
               // Deal with exception here
            }
       }
    }

我也尝试创建任务数组,并异步运行它们。但最后我仍然需要调用Task.WaitAll()才能读取结果。因此,即使一个任务早已完成,它也必须等待运行时间最长的任务。
我认为我需要尝试通过循环异步创建进程,可能使用任务,但我不知道如何将其作为后台进程执行,以便在需要创建更多进程时保持服务计时器检查。

1
除了Task.WaitAll()之外,还有Task.WaitAny()可以帮助您解决问题。我首先考虑的一般设计是,每个任务都只做一件事情,包括生成import.exe实例并等待其完成。使用文件系统监视器(例如),您可以检测文件夹中的更改,并为每个更改(新文件)安排一个任务。计时器在这里并不是真正必要的,除非您想限制处理并仅在间隔内检查而不是立即响应更改。 - BitTickler
1
你需要纠正的一个问题是:考虑一下如果其他软件恰好使用了名为“import.exe”的进程会发生什么情况。(你应该跟踪你已经启动的特定进程,而不是计算具有给定名称的进程数量。) - Harry Johnston
那是@HarryJohnston。根据给出的答案,我认为我不需要再跟踪正在运行的进程数量了。如果我继续使用我的问题中的代码,我将保留进程ID列表,并确保在进程退出时从列表中删除它们。 - SeanOB
是的,被接受的答案隐式地解决了这个问题 - 通过将每个进程与单独的任务相关联来跟踪它们。 :-) - Harry Johnston
1个回答

5
首先,您的代码需要改进的第一点是删除计时器,并用一个System.IO.FileSystemWatcher和一个Created事件处理程序替换它。这样,您的代码就不需要管理之前在队列中的文件以及新到达的文件。通常来说,少量的代码=少量的问题。
其次,认真考虑“任务”一词,暗示着在1个System.IO.Tasks.Task实例中执行完整的导入任务,包括生成相应的导入进程实例并等待其退出完成。
如果您希望限制任何时候运行的导入进程数量,那么可以使用与您的代码类似的另一种方式,将调度程序替换为调度程序,该调度程序限制了允许并行运行的任务数,与默认调度程序相比。如果每个任务都与1个导入程序实例相关联,并且最多允许同时运行N个任务,则您最多可以有N个导入程序的实例。
下面的代码(以控制台应用程序的形式)显示了上述描述的内容,但不包括自定义调度程序,该调度程序在提供的链接中介绍。
using System.Threading.Tasks;

namespace ConsoleApplication4
{
    class Program
    {
        static string importerProcessName = "import.exe";
        static string RootFolder = @"E:\temp\A\";
        static string queuePath = System.IO.Path.Combine(RootFolder, "Queue" );
        static string processingPath = System.IO.Path.Combine(RootFolder, "Processing");
        static string donePath = System.IO.Path.Combine(RootFolder, "Done");
        static void Main(string[] args)
        {
            GrantFolders(); // Make sure we have all our folders ready for action...
            var watcher = new System.IO.FileSystemWatcher(queuePath, "*.txt");
            watcher.Created += watcher_Created;
            watcher.EnableRaisingEvents = true;
            System.Console.ReadLine();
        }
        static Task ProcessFile( string fileName )
        {
            Task task = new Task(() =>
            {
                System.Console.WriteLine("Processing: " + fileName);
                System.IO.File.Move(System.IO.Path.Combine(queuePath, fileName), System.IO.Path.Combine(processingPath, fileName));
                string commandLine = "-import " + System.IO.Path.Combine(processingPath, fileName);
                using (var importer = new System.Diagnostics.Process())
                {
                    importer.StartInfo = new System.Diagnostics.ProcessStartInfo(importerProcessName, commandLine);
                    importer.Start();
                    importer.WaitForExit(20000);
                    System.IO.File.Move(System.IO.Path.Combine(processingPath, fileName), System.IO.Path.Combine(donePath, fileName));
                    System.Console.WriteLine("Done with: " + fileName);
                }
            });
            return task;
        }
        static void watcher_Created(object sender, System.IO.FileSystemEventArgs e)
        {
            System.Console.WriteLine("Found in queue: " + e.Name);
            var task = ProcessFile(e.Name);
            task.Start();
        }

        private static void GrantFolders()
        {
            string[] paths = new string[] { queuePath, processingPath, donePath };
            foreach( var path in paths)
            {
                if(!System.IO.Directory.Exists(path))
                {
                    System.IO.Directory.CreateDirectory(path);
                }
            }
        }
    }
}

这看起来很不错。我现在正在深入研究,会让你知道结果的。我不知道FileSystemWatcher的存在...非常感谢您提供完整的示例。 - SeanOB
这很好用。我也使用了你提供的链接中的任务调度器限制类并且它可以很好地限制运行进程的数量。我没有实现工厂相关的任何内容。 我想这里的教训是,因为我不关心处理“批次”,所以我不需要将我的任务捆绑在一起或其他什么(虽然 Task.WaitAny() 可以完成任务)。 - SeanOB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接