在负载下减少 CPU 使用 NetNamedPipe

reducing the CPU use of NetNamedPipe when under load

我有一个 Windows 服务,它使用 NetNamedPipe 与同一台机器上的其他进程进行通信。它工作正常,除了一个问题:CPU 使用率高。我可以做些什么来减少这种使用?

为了更好地理解这个问题,我制作了一个简单的测试程序,它通过命名管道与自身对话并跟踪它自己的 CPU 使用。当不经常使用命名管道时(每秒 1 次操作),CPU 使用率非常低。当频繁使用命名管道(每秒数千次操作)时,CPU 使用增加。

下面是一些演示该行为的示例输出。 (请注意,CPU 使用 Process > % Processor Time 计数器,这不像您在任务管理器中看到的 CPU 使用那么简单。)

NetNamedPipe    Passed: 31309   Failed: 0       Elapsed: 10.4 s Rate: 3000 Hz    Process CPU: 30.0 %
NetNamedPipe    Passed: 13      Failed: 0       Elapsed: 11.0 s Rate: 1 Hz       Process CPU: 0.9 %

理想情况下,我想继续使用 NetNamedPipe,但做一些事情来减少 CPU 的使用。我已经尝试使用 Stack Overflow 和其他地方的想法调整 the optional settings of NetNamedPipeBinding,但无法减少 CPU 的使用。也许我遗漏了什么?

我意识到,很可能,我可能不得不做一些更激烈的事情。我可能需要在 "bundles" 中发送更少、更大的消息。或者我可能需要使用不同的进程间通信方式。任何有关调查内容的建议都将不胜感激。

我的测试程序代码如下。它以 .NET Framework 4.7.2 为目标。我已经 运行 Windows 10.

Program.cs

using System;
using System.Diagnostics;
using System.Threading;

namespace IpcExperiments
{
    class Program
    {
        private static readonly string MyName = "Alice";
        private static readonly string ProcessName = "IpcExperiments";
        private static readonly double DesiredRate = 3000; // In Hz
        // private static readonly double DesiredRate = Double.MaxValue; // Go as fast as possible!

        private static PerformanceCounter ProcessCpu = null;

        static void Main(string[] args)
        {
            ProcessCpu = new PerformanceCounter("Process", "% Processor Time", ProcessName);

            Test(new Experiments.NetNamedPipe(), MyName, DesiredRate);            
            // Optionally, add other tests here.

            Console.Write("\r                                            ");
            Console.WriteLine();
            Console.WriteLine("All tests complete! Press Enter to finish.");
            Console.ReadLine();
        }

        private static void Test(Experiments.IIpcExperiment experiment, string myName, double desiredRate = Double.MaxValue)
        {
            int i = 0;
            int successes = 0;
            int fails = 0;
            double elapsed = 0;
            double rate = 0;
            double thisCpu = 0;
            double avgCpu = 0;
            double cpuCount = 0;
            string matchingName = String.Format("Hello {0}!", myName);
            string experimentName = experiment.GetExperimentName();

            Console.Write("\rCreating {0}...", experimentName);
            experiment.Setup();
            DateTime startTime = DateTime.Now;
            DateTime nextCpuRead = DateTime.MinValue;
            while (!Console.KeyAvailable)
            {
                if (experiment.SayHello(myName).Equals(matchingName))
                {
                    successes++;
                }
                else
                {
                    fails++;
                }
                if (nextCpuRead < DateTime.Now)
                {
                    thisCpu = ProcessCpu.NextValue();
                    if (cpuCount == 0)
                    {
                        avgCpu = thisCpu;
                    }
                    else
                    {
                        avgCpu = ((avgCpu * cpuCount) + thisCpu) / (cpuCount + 1);
                    }
                    cpuCount++;
                    nextCpuRead = DateTime.Now.AddSeconds(1);
                }
                elapsed = (DateTime.Now - startTime).TotalSeconds;
                rate = ((double)i) / elapsed;
                Console.Write("\r{0}\tPassed: {1}\tFailed: {2}\tElapsed: {3:0.0} s\tRate: {4:0} Hz\t Process CPU: {5:0.0} %"
                    , experimentName
                    , successes
                    , fails
                    , elapsed
                    , rate
                    , avgCpu);
                while (rate > desiredRate && !Console.KeyAvailable)
                {
                    Thread.Sleep(1);
                    elapsed = (DateTime.Now - startTime).TotalSeconds;
                    rate = ((double)i) / elapsed;
                }
                i++;
            }
            Console.ReadKey(true);
            Console.WriteLine();
            Console.Write("\rDisposing {0}...", experimentName);
            experiment.Shutdown();
        }
    }
}

IIpcExperiment.cs

namespace IpcExperiments.Experiments
{
    interface IIpcExperiment
    {
        string GetExperimentName();
        void Setup();
        void Shutdown();
        string SayHello(string myName);
    }
}

NetNamedPipe.cs

using System;
using System.ServiceModel;

namespace IpcExperiments.Experiments
{
    [ServiceContract]
    public interface INetNamedPipe
    {
        [OperationContract]
        string SayHello(string myName);
    }

    public class IpcInterface : INetNamedPipe
    {
        public string SayHello(string myName)
        {
            return String.Format("Hello {0}!", myName);
        }
    }

    public class NetNamedPipe : IIpcExperiment
    {
        private ServiceHost Host;
        private INetNamedPipe Client;

        public void Setup()
        {
            SetupHost();
            SetupClient();
        }

        public void Shutdown()
        {
            Host.Close();
        }

        public string GetExperimentName()
        {
            return "NetNamedPipe";
        }

        public string SayHello(string myName)
        {
            return Client.SayHello(myName);
        }
        private void SetupHost()
        {
            Host = new ServiceHost(typeof(IpcInterface),
                new Uri[]{
                  new Uri(@"net.pipe://localhost")
                });

            NetNamedPipeBinding nnpb = new NetNamedPipeBinding();
            Host.AddServiceEndpoint(typeof(INetNamedPipe)
                    , nnpb
                    , "NetNamedPipeExample");
            Host.Open();
        }

        private void SetupClient()
        {
            NetNamedPipeBinding nnpb = new NetNamedPipeBinding();
            ChannelFactory<INetNamedPipe> pipeFactory =
              new ChannelFactory<INetNamedPipe>(
                nnpb,
                new EndpointAddress(@"net.pipe://localhost/NetNamedPipeExample"));
            Client = pipeFactory.CreateChannel();
        }
    }
}

最后我是这样解决的。

修复前,在上述问题的示例代码中,我多次调用SayHello,这样做的开销消耗了大量CPU。

修复后,我通过单个 Stream 获得了相同的数据。我怀疑设置流的 CPU 开销大致相同,但流只需要设置一次 一次 。总体 CPU 使用率要低得多。

WCF 命名管道支持流,因此我不必放弃使用命名管道。

您可以阅读有关流式传输的内容 here,或者如果 link 死了,请将 TransferMode.Streaming 放入您最喜欢的搜索引擎。

我的流需要 "infinite" 才能永远推送数据,所以我需要自定义 StreamThis answer on Stack Overflow 帮助指导了我。

我还有一些问题需要解决,但是 CPU 使用问题(即这个问题的关键)似乎已经通过这种方法解决了。