在低于正常优先级的Windows Service中停止Parallel.ForEach

时间:2018-08-15 13:50:12

标签: c# windows-services task-parallel-library

我的Windows服务中有一个Parallel.ForEach代码。如果ParallelOptions.MaxDegreeOfParallelism设置为-1,则说明我使用的是大多数CPU。但是,停止服务将持续半分钟。一些应该接收到该服务应该停止的信号的内部控制器线程将耗尽处理器时间。我将进程优先级设置为低于正常值,但这可能与此处无关。

即使所有线程都忙,我该如何缩短停止服务的时间?

我想暂时降低线程池中线程的优先级,因为我没有任何异步代码,但是Internet表示这是个坏主意,因此在这里要求“适当”的方法。

在所有情况下,OnStartOnStop之间的线程(OS和.NET)都不同。另外,如果停止时间很长,则有时最终会在其中调用OnStop的OS线程是一个新线程,不会在日志中更早显示。

要构建此代码,请创建新的Windows服务项目,从设计器中添加ProjectInstaller类,将Account更改为LocalService,然后使用InstallUtil安装一次。确保LocalService可以写入C:\ Temp。

public partial class Service1 : ServiceBase
{
    private ManualResetEvent stopEvent = new ManualResetEvent(false);
    private Task mainTask;
    private StreamWriter writer = File.AppendText(@"C:\Temp\Log.txt");

    public Service1()
    {
        InitializeComponent();

        writer.AutoFlush = true;
    }

    protected override void OnStart(string[] args)
    {
        Log("--------------");
        Log("OnStart");

        mainTask = Task.Run(new Action(Run));
    }

    protected override void OnStop()
    {
        Log("OnStop");
        stopEvent.Set();

        mainTask.Wait();
        Log("--------------");
    }

    private void Log(string line)
    {
        writer.WriteLine(String.Format("{0:yyyy-MM-dd HH:mm:ss.fff}: [{1,2}] {2}",
            DateTime.Now, Thread.CurrentThread.ManagedThreadId, line));
    }

    private void Run()
    {
        try
        {
            using (var sha = SHA256.Create())
            {
                var parallelOptions = new ParallelOptions();
                parallelOptions.MaxDegreeOfParallelism = -1;

                Parallel.ForEach(Directory.EnumerateFiles(Environment.SystemDirectory),
                    parallelOptions, (fileName, parallelLoopState) =>
                {
                    if (stopEvent.WaitOne(0))
                    {
                        Log("Stop requested");
                        parallelLoopState.Stop();
                        return;
                    }

                    try
                    {
                        var hash = sha.ComputeHash(File.ReadAllBytes(fileName).OrderBy(x => x).ToArray());
                        Log(String.Format("file={0}, sillyhash={1}", fileName, Convert.ToBase64String(hash)));
                    }
                    catch (Exception ex)
                    {
                        Log(String.Format("file={0}, exception={1}", fileName, ex.Message));
                    }
                });
            }
        }
        catch (Exception ex)
        {
            Log(String.Format("exception={0}", ex.Message));
        }
    }
}

5 个答案:

答案 0 :(得分:4)

此代码将在一两秒钟内停止服务,而已在计算的线程只有在完成其实际工作后才会结束。正如您在“服务”中看到的那样,OnStop方法立即接收到信号。但是,TaskManager显示,与服务相关联的进程仅在使用线程全部完成后才会停止。

这使用一个单独的线程正在填充的字符串(路径)的BlockingCollection。而且有许多优先级较低的线程会消耗这些字符串。

public partial class Service1 : ServiceBase
{
    private StreamWriter writer = File.AppendText(@"C:\temp\Log.txt");

    const int nbTreads = 30;
    BlockingCollection<string> dataItems;
    bool stopCompute = false;
    List<Thread> threads = new List<Thread>();
    Thread threadProd;
    private object aLock = new object();

    public Service1()
    {
        InitializeComponent();

        dataItems = new BlockingCollection<string>(nbTreads);

        writer.AutoFlush = true;
    }


    protected override void OnStart(string[] args)
    {
        Log("--------------");
        Log("OnStart");
        threadProd = new Thread(new ThreadStart(ProduireNomFichier));
        threadProd.Start();
        Thread.Sleep(1000); // fill the collection a little
        for (int i = 0; i < nbTreads; i++)
        {
            Thread threadRun = new Thread(() => Run());
            threadRun.Priority = ThreadPriority.Lowest;
            threadRun.Start();
            threads.Add(threadRun);
        }
    }

    private void ProduireNomFichier()
    {
        foreach (string nomFichier in Directory.EnumerateFiles(Environment.SystemDirectory))
        {
            dataItems.Add(nomFichier);
        }
    }

    protected override void OnStop()
    {
        lock (aLock)
        {
            stopCompute = true;
        }
        Log("OnStop");
        Log("--------------");
        threadProd.Abort();
    }

    private void Log(string line)
    {
        writer.WriteLine(String.Format("{0:yyyy-MM-dd HH:mm:ss.fff}: [{1,2}] {2}",
            DateTime.Now, Thread.CurrentThread.ManagedThreadId, line));
    }

    private void Run()
    {
        try
        {
            using (var sha = SHA256.Create())
            {
                while (dataItems.TryTake(out string fileName))
                {
                    lock (aLock)
                    {
                        if (stopCompute) return;
                    }
                    try
                    {
                        var hash = sha.ComputeHash(File.ReadAllBytes(fileName).OrderBy(x => x).ToArray());
                        Log(String.Format("file={0}, sillyhash={1}", fileName, Convert.ToBase64String(hash)));
                    }
                    catch (Exception ex)
                    {
                        Log(String.Format("file={0}, exception={1}", fileName, ex.Message));
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Log(String.Format("exception={0}", ex.Message));
        }
    }
}

答案 1 :(得分:3)

在Parallel.Foreach中,读取文件的所有字节,然后使用LINQ对其进行排序。这不是有效的。尝试使用Array.Sort。对于25 Mb的文件,速度可以提高85%。

Array.Sort 2230 ms
OrderBy 14650 ms

并且因为OnStop方法等待已开始的任何迭代的结束,所以它可以更快地停止服务。

var fileBinary = File.ReadAllBytes(fileName);
Array.Sort(fileBinary);
var hash = sha.ComputeHash(fileBinary);

答案 2 :(得分:2)

这是一个有效的代码。它立即停止。请注意,主要想法来自:SylF。

但是我不能给出清楚的解释为什么会发生... 更新(在下面的评论之后):您找到了原因,并且很好地解释了为什么您有这种行为。谢谢!我真的很高兴知道。

尽管这项工作是在低优先级的线程中完成的,但您应该不会注意到在CPU几乎没有工作的机器上的任何额外延迟。

对不起,我混淆了您的代码示例以进行一些测试。但是主要思想是更改调度程序(似乎不建议这样做)。但这是我发现的唯一方法。

代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.ServiceProcess;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace StackOverflowQuestionWindowsService1
{
    public partial class Service1 : ServiceBase
    {
        private ManualResetEvent stopEvent = new ManualResetEvent(false);
        private Task mainTask;
        private StreamWriter writer = File.CreateText(@"C:\Temp\Log.txt");     //TAKE CARE - I do not append anymore  ********
        private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
        private int count = 0;

        public Service1()
        {
            InitializeComponent();

            writer.AutoFlush = true;
        }

        protected override void OnStart(string[] args)
        {
            Log("--------------");
            Log("OnStart");

            Task.Run(()=>Run());
        }

        protected override void OnStop()
        {
            Log("OnStop with actual thread count: " + Process.GetCurrentProcess().Threads.Count);

            cancellationTokenSource.Cancel();
        }

        private void Log(string line)
        {
            writer.WriteLine(String.Format("{0:yyyy-MM-dd HH:mm:ss.fff}: [{1,2}] {2}",
                DateTime.Now, Thread.CurrentThread.ManagedThreadId, line));
        }

        private void Run()
        {
            Stopwatch stopWatchTotal = new Stopwatch();
            stopWatchTotal.Start();

            try
            {
                using (var sha = SHA256.Create())
                {
                    var parallelOptions = new ParallelOptions();
                    parallelOptions.MaxDegreeOfParallelism = -1;
                    parallelOptions.CancellationToken = cancellationTokenSource.Token;
                    parallelOptions.TaskScheduler = new PriorityScheduler(ThreadPriority.Lowest);

                    Parallel.ForEach(Directory.EnumerateFiles(Environment.SystemDirectory),
                        parallelOptions, (fileName, parallelLoopState) =>
                        {
                            // Thread.CurrentThread.Priority = ThreadPriority.Lowest;
                            Stopwatch stopWatch = new Stopwatch();
                            stopWatch.Start();

                            Interlocked.Increment(ref count);

                            if (parallelOptions.CancellationToken.IsCancellationRequested)
                            {
                                Log(String.Format($"{count}"));
                                return;
                            }

                            try
                            {
                                var hash = sha.ComputeHash(File.ReadAllBytes(fileName).OrderBy(x => x).ToArray());
                                stopWatch.Stop();
                                Log(FormatTicks(stopWatch.ElapsedTicks));
                                Log(String.Format($"{count}, {FormatTicks(stopWatch.ElapsedTicks)}, file={fileName}, sillyhash={Convert.ToBase64String(hash)}"));
                            }
                            catch (Exception ex)
                            {
                                Log(String.Format($"{count} file={fileName}, exception={ex.Message}"));
                            }
                        });
                }
            }
            catch (Exception ex)
            {
                Log(String.Format("exception={0}", ex.Message));
            }

            stopWatchTotal.Stop();

            Log(FormatTicks(stopWatchTotal.ElapsedTicks));

            writer.Close();
            Process.GetCurrentProcess().Kill();
        }

        private string FormatTicks(long ticks)
        {
            return new TimeSpan(ticks).ToString();
        }
    }
}

优先级调度程序:(感谢Roman Starkov的StackOverflow,来自Bnaya Eshet的Microsoft

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace StackOverflowQuestionWindowsService1
{
    public class PriorityScheduler : TaskScheduler
    {
        public static PriorityScheduler AboveNormal = new PriorityScheduler(ThreadPriority.AboveNormal);
        public static PriorityScheduler BelowNormal = new PriorityScheduler(ThreadPriority.BelowNormal);
        public static PriorityScheduler Lowest = new PriorityScheduler(ThreadPriority.Lowest);

        private BlockingCollection<Task> _tasks = new BlockingCollection<Task>();
        private Thread[] _threads;
        private ThreadPriority _priority;
        private readonly int _maximumConcurrencyLevel = Math.Max(1, Environment.ProcessorCount);

        public PriorityScheduler(ThreadPriority priority)
        {
            _priority = priority;
        }

        public override int MaximumConcurrencyLevel
        {
            get { return _maximumConcurrencyLevel; }
        }

        protected override IEnumerable<Task> GetScheduledTasks()
        {
            return _tasks;
        }

        protected override void QueueTask(Task task)
        {
            _tasks.Add(task);

            if (_threads == null)
            {
                _threads = new Thread[_maximumConcurrencyLevel];
                for (int i = 0; i < _threads.Length; i++)
                {
                    int local = i;
                    _threads[i] = new Thread(() =>
                    {
                        foreach (Task t in _tasks.GetConsumingEnumerable())
                            base.TryExecuteTask(t);
                    });
                    _threads[i].Name = string.Format("PriorityScheduler: ", i);
                    _threads[i].Priority = _priority;
                    _threads[i].IsBackground = true;
                    _threads[i].Start();
                }
            }
        }

        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
        {
            return false; // we might not want to execute task that should schedule as high or low priority inline
        }
    }
}

答案 3 :(得分:1)

在parallelOptions对象中,有一个用于取消令牌来源的属性。您可以将该属性设置为新的CancellationTokenSource。然后,在并行循环中,您将调用parallelOptions.CancellationToken.ThrowIfCancellationRequested()。这将导致您的线程被终止。

有关详细示例,请参见: How to: Cancel a Parallel.For or ForEach Loop

编辑:如果您希望服务停止得更快,则可能还需要取消ComputeHash函数的执行。一旦您的线程处于该调用中,就无法将其取消。因此,解决方案是在循环中使用TransformBlock方法进行块转换。在该循环中,您需要检查CancellationToken,或者检查手动重置事件对象。如果您需要有关操作方法的指导,请查看以下答案:stop hashing operation using filestream。他们在展示如何使用MD5算法使用块转换方面做得非常出色,但是可以直接移植到SHA256算法中。

答案 4 :(得分:1)

问题在于,当您发出停止命令时,您的线程每个需要查看停止命令,然后进行同步才能真正停止。这意味着您的停止只会和您的最慢​​哈希计算一样快。如果您是我,我将要做的是重写代码的哈希计算部分,以便它迭代地计算哈希,而不是调用内置函数。这样,您就可以停止进行哈希计算了。

SHA256中有称为TransformBlockTransformFinalBlock的方法来实现此目的。

我为自己的个人项目编写的一些代码示例为:

do
{
    if(SHOULD_STOP)
        STOP();
    oldBytesRead = bytesRead;
    oldBuffer = buffer;

    buffer = new byte[4096];
    bytesRead = stream.Read(buffer, 0, buffer.Length);

    totalBytesRead += bytesRead;

    if (bytesRead == 0)
    {
        hashAlgorithm.TransformFinalBlock(oldBuffer, 0, oldBytesRead);
    }
    else
    {
        hashAlgorithm.TransformBlock(oldBuffer, 0, oldBytesRead, oldBuffer, 0);
    }

    int progress = (int)((double)totalBytesRead * 100 / size);

} while (bytesRead != 0);

return BitConverter.ToString(hashAlgorithm.Hash).Replace("-", "").ToLowerInvariant();