繁体   English   中英

如何防止“最大化”CPU:同步方法使用SemaphoreSlim异步调用多个worker并进行限制?

[英]How do I prevent “maxing out” of CPU: Synchronous method calling multiple workers asynchronously & throttling using SemaphoreSlim?

我目前正在优化现有的,非常慢的超时生产应用程序。 没有选择重写它

简而言之,它是一个WCF服务,当前顺序调用其他4个“工作”WCF服务。 工作人员服务都不依赖于另一方的结果。 所以我们希望它能够立即调用它们(不是按顺序) 我将重申,我们没有重写它的奢侈。

在此输入图像描述

优化包括让它立即调用所有工作服务。 这就是想到异步的地方。

我对异步编程的经验有限,但就我的解决方案而言,我已尽可能广泛地阅读该主题。

问题是,在测试时,它可以工作但最大化我的CPU。 我很感激你的帮助

以下是主WCF服务中基本代码的简化版本

// The service operation belonging to main WCF Service
public void ProcessAllPendingWork()
{
    var workerTasks = new List<Task<bool>>();
    foreach(var workerService in _workerServices)
    {
        //DoWorkAsync is the worker method with the following signature:
        // Task<bool> DoWorkAsync()

        var workerTask = workerService.DoWorkAsync()
        workerTasks.Add(workerTask);
    }

    var task = Task.Run(async ()=>
    {
        await RunWorkerTasks(workerTasks);
    });
    task.Wait();


}

private async RunWorkerTasks(IEnumerable<Tast<bool>> workerTasks)
{
    using(var semaphore = new SemaphoreSlim(initialCount:3))
    {

        foreach (var workerTask in workerTasks)
        {
            await semaphore.WaitAsync();
            try
            {
                await workerTask;
            }
            catch (System.Exception)
            {
                //assume 'Log' is a predefined logging service
                Log.Error(ex);
            }
        }
    }
} 

我读过的内容:

多种方式如何限制并行任务处理

如何限制并发异步I / O操作的数量?

在C#中限制异步方法的方法

在C#中约束并发线程

使用SemaphoresSlim限制并发线程数

使用ChannelFactory和CreateChannel调用异步WCF

您没有解释如何限制并发调用。 您是否希望运行30个并发工作任务,或者您是否需要30个WCF调用,每个调用都同时运行所有工作任务,或者您是否希望每个并发WCF调用都有自己的并发工作任务限制? 鉴于您说每个WCF调用只有4个工作任务并查看您的示例代码,我假设您希望全局限制30个并发工作任务。

首先,正如@mjwills暗示的那样,您需要使用SemaphoreSlim来限制对workerService.DoWorkAsync()调用。 您的代码目前启动所有代码,并且只试图限制您等待完成的数量。 我认为这就是你最大化CPU的原因。 开始的工作人员任务数量仍然无限制。 但是请注意,您还需要在持有信号量时等待工作人员任务,否则您只会限制创建任务的速度,而不是同时运行的任务数量。

其次,您要为每个WCF请求创建一个新的SemaphoreSlim。 因此我的问题来自我的第一段。 唯一可以扼杀任何东西的方法是,如果你有比初始计数更多的工人服务,你的样本中有30个,但你说只有4个工人。 要获得“全局”限制,您需要使用单例SemaphoreSlim。

第三,你永远不会在SemaphoreSlim上调用.Release() ,所以如果你确实把它变成了一个单例,那么你的代码会在进程开始后一旦启动30个工作程序就会挂起。 确保在try-finally块中执行此操作,以便在工作程序崩溃时仍然会释放它。

这是一些草率编写的示例代码:

public async Task ProcessAllPendingWork()
{
    var workerTasks = new List<Task<bool>>();
    foreach(var workerService in _workerServices)
    {
        var workerTask = RunWorker(workerService);
        workerTasks.Add(workerTask);
    }

    await Task.WhenAll(workerTasks);
}

private async Task<bool> RunWorker(Func<bool> workerService)
{
    // use singleton semaphore.
    await _semaphore.WaitAsync();
    try
    {
        return await workerService.DoWorkAsync();
    }
    catch (System.Exception)
    {
        //assume error is a predefined logging service
        Log.Error(ex);
        return false; // ??
    }
    finally
    {
        _semaphore.Release();
    }
}

TPL(任务并行库)提供的Task抽象是Thread的抽象; 任务在线程池中排队,然后在执行者可以管理该请求时执行。

换句话说,根据某些因素(您的流量,CPU与IO buound和部署模型),尝试在您的工作函数中执行托管任务可能根本没有任何好处(或者在某些情况下会更慢)。

说,我建议你使用Task.WaitAll (可从.NET 4.0获得),它使用非常高级别的抽象来管理并发性; 特别是这段代码对你有用:

  • 它创造了工人并等待所有人
  • 执行需要10秒钟(最长的工人)
  • 它抓住并为您提供管理异常的机会
  • [最后但并非最不重要]是一个令人厌恶的api,它将注意力集中在做什么而不是怎么做。
public class Q57572902
{
    public void ProcessAllPendingWork()
    {
        var workers = new Action[] {Worker1, Worker2, Worker3};

        try
        {
            Task.WaitAll(workers.Select(Task.Factory.StartNew).ToArray());
            // ok
        }
        catch (AggregateException exceptions)
        {
            foreach (var ex in exceptions.InnerExceptions)
            {
                Log.Error(ex);
            }
            // ko
        }
    }

    public void Worker1() => Thread.Sleep(FromSeconds(5)); // do something

    public void Worker2() => Thread.Sleep(FromSeconds(10)); // do something

    public void Worker3() => throw new NotImplementedException("error to manage"); // something wrong

}

我从评论中看到,你需要最多同时运行3名工人; 在这种情况下,你可以简单地复制粘贴LimitedConcurrencyLevelTaskScheduler的TaskScheduler文档。

之后你必须用它的onw TaskFactory创建sigleton实例TaskScheduler

public static class WorkerScheduler
{
    public static readonly TaskFactory Factory;

    static WorkerScheduler()
    {
        var scheduler = new LimitedConcurrencyLevelTaskScheduler(3);
        Factory = new TaskFactory(scheduler);
    }
}

以前的ProcessAllPendingWork()代码保持不变,除了

...workers.Select(Task.Factory.StartNew)...

变成了

...workers.Select(WorkerScheduler.Factory.StartNew)...

因为你必须使用TaskFactory关联到您的自定义WorkerScheduler

如果您的工作人员需要将某些数据返回到响应,则需要以不同的方式管理错误和数据,如下所示:

public void ProcessAllPendingWork()
{
    var workers = new Func<bool>[] {Worker1, Worker2, Worker3};
    var tasks = workers.Select(WorkerScheduler.Factory.StartNew).ToArray();

    bool[] results = null;

    Task
        .WhenAll(tasks)
        .ContinueWith(x =>
        {
            if (x.Status == TaskStatus.Faulted)
            {
                foreach (var exception in x.Exception.InnerExceptions)
                    Log(exception);

                return;
            }

            results = x.Result; // save data in outer scope
        })
        .Wait();

    // continue execution
    // results is now filled: if results is null, some errors occured
}

除非我错过了什么 - 您的示例代码并行运行所有工作程序。 在调用'workerService.DoWorkAsync()'时,工作人员开始工作。 'RunWorkerTasks'仅等待工作人员任务完成。 'doWorkAsync()'启动异步操作,而'await'暂停执行调用方法,直到等待的任务完成。

高CPU使用率的事实很可能是由于您的workerService活动而不是由于您调用它们的方式。 为了验证这一点,请尝试使用Thread.Sleep(..)Task.Delay(..)替换workerService.DoWorkAsync() Task.Delay(..) 如果你的CPU使用率下降,那就是工人的责任。 (根据workerService的作用),一旦并行运行,CPU消耗可能会增加,甚至可能会增加。

想到你如何限制并行执行的问题。 请注意,以下示例并不完全使用3个线程,但最多使用3个线程。

    Parallel.ForEach(
        _workerServices,
        new ParallelOptions { MaxDegreeOfParallelism = 3 },
        workerService => workerService.DoWorkAsync()
            .ContinueWith(res => 
            {
                // Handle your result or possible exceptions by consulting res.
            })
            .Wait());

正如您之前提到的那样,您的代码是按顺序执行的,我假设工作者也有非异步等效代码。 使用它们可能更容易。 同步调用异步方法主要是麻烦。 我只是通过调用DoWorkAsync().Wait()死锁问题DoWorkAsync().Wait() 关于如何同步运行异步Task <T>方法的讨论很多 本质上我试图避免它。 如果那是不可能的,我尝试使用ContinueWith ,这会增加复杂性,或者之前的SO讨论的AsyncHelper

    var results = new ConcurrentDictionary<WorkerService, bool>();
    Parallel.ForEach(
        _workerServices,
        new ParallelOptions { MaxDegreeOfParallelism = 3 },
        workerService => 
            {
                // Handle possible exceptions via try-catch.
                results.TryAdd(workerService, workerService.DoWork());
            });
    // evaluate results

Parallel.ForEach利用Thread或TaskPool。 这意味着它将给定参数Action<TSource> body每次执行都调度到专用线程上。 您可以使用以下代码轻松验证。 如果Parallel.ForEach已经在不同的线程上调度工作,您可以简单地同步执行“昂贵”操作。 任何异步操作都是不必要的,甚至会对运行时性能产生不良影响。

    Parallel.ForEach(
        Enumerable.Range(1, 4),
        m => Console.WriteLine(Thread.CurrentThread.ManagedThreadId));

这是我用于测试的演示项目,它不依赖于你的workerService。

    private static bool DoWork()
    {
        Thread.Sleep(5000);
        Console.WriteLine($"done by {Thread.CurrentThread.ManagedThreadId}.");
        return DateTime.Now.Millisecond % 2 == 0;
    }

    private static Task<bool> DoWorkAsync() => Task.Run(DoWork);

    private static void Main(string[] args)
    {
        var sw = new Stopwatch();
        sw.Start();

        // define a thread-safe dict to store the results of the async operation
        var results = new ConcurrentDictionary<int, bool>();

        Parallel.ForEach(
            Enumerable.Range(1, 4), // this replaces the list of workers
            new ParallelOptions { MaxDegreeOfParallelism = 3 },
            // m => results.TryAdd(m, DoWork()), // this is the alternative synchronous call
            m => DoWorkAsync().ContinueWith(res => results.TryAdd(m, res.Result)).Wait());

        sw.Stop();

        // print results
        foreach (var item in results)
        {
            Console.WriteLine($"{item.Key}={item.Value}");
        }

        Console.WriteLine(sw.Elapsed.ToString());
        Console.ReadLine();
    }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM