如何在C#中以特定吞吐量调用我的方法?

如何在C#中以特定吞吐量调用我的方法?,c#,multithreading,performance-testing,C#,Multithreading,Performance Testing,我正在做一些性能测试,所以希望以我可以控制的特定吞吐量调用我的方法。通过这种方式,我可以生成一些负载,并了解它在不同吞吐量下的行为 例如:我需要调用我的doIOStuff方法,从多个线程以大约每秒x个请求的速率调用,其中x通常小于2000个,但在这种情况下这并不重要。它不必精确,因此存在一些出错的空间,但总体思路是我需要确保我的方法doIOStuff在y秒的滑动窗口中执行不超过x次 下面是我得到的代码,我正在创建3个不同的线程,并行调用doIOStuff方法,但无法计算如何将此方法的速率限制为每

我正在做一些性能测试,所以希望以我可以控制的特定吞吐量调用我的方法。通过这种方式,我可以生成一些负载,并了解它在不同吞吐量下的行为

例如:我需要调用我的
doIOStuff
方法,从多个线程以大约每秒
x个请求的速率调用,其中x通常小于2000个,但在这种情况下这并不重要。它不必精确,因此存在一些出错的空间,但总体思路是我需要确保我的方法
doIOStuff
在y秒的滑动窗口中执行不超过x次

下面是我得到的代码,我正在创建3个不同的线程,并行调用
doIOStuff
方法,但无法计算如何将此方法的速率限制为每秒x个请求。有没有一个简单的方法可以控制这个

class Program
{
    static void Main(string[] args)
    {
        var tasks = new List<Task>();

        for(int i = 0; i< 100; i ++)
            tasks.Add(Task.Factory.StartNew(() => doIOStuff(), i));

        Task.WaitAll(tasks.ToArray());
        Console.WriteLine("All threads complete");
    }

    // how to call this method at a particular rate?
    static void doIOStuff()
    {
        // do some IO work
    }
}
类程序
{
静态void Main(字符串[]参数)
{
var tasks=新列表();
对于(int i=0;i<100;i++)
tasks.Add(Task.Factory.StartNew(()=>doIOStuff(),i));
Task.WaitAll(tasks.ToArray());
Console.WriteLine(“所有线程完成”);
}
//如何以特定速率调用此方法?
静态void doIOStuff()
{
//做一些IO工作
}
}
我希望在一段时间内继续运行此测试,但在这段时间内,它应该始终仅以特定吞吐量调用该方法

注意:


这只是我的一个想法,即生成随机吞吐量,我可以控制它,但如果这不能正确完成任务,那么我们应该尽可能以更好、更高效的方式来实现,但总体想法是自己控制随机吞吐量,并为我的方法生成如此大的负载。

假设您启动n个线程,并希望每秒最多有m个呼叫。这可以通过让每个线程每秒生成0到1k次的随机数来实现,并且仅当生成的数字小于m/n/k时才调用IO方法

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;


namespace ConsoleApp7
{
    class Program
    {
        const int m_threads = 100;
        const int n_throughput = 2000;
        const int k_toss_per_second = 2000; // Note that k_toss_per_second x  m_threads >= n_throughput

        static void Main(string[] args)
        {
            var tasks = new List<Task>();



            for (int i = 0; i < m_threads; i++)
                tasks.Add(Task.Factory.StartNew(() => callDoIOStuff()));

            Task.WaitAll(tasks.ToArray());
            Console.WriteLine("All threads complete");
        }


        static void callDoIOStuff()
        {
           int sleep_time = (int) (1000 * 1.0d / k_toss_per_second);
           double threshold = (double) n_throughput / m_threads / k_toss_per_second; 
           Random random = new Random();
           while (true) {
                Thread.Sleep(sleep_time);
                if (random.NextDouble() < threshold)
                    doIOStuff();
            }
        }

        // how to call this method at a particular rate?
        static void doIOStuff()
        {
            // do some IO work
        }
    }
}
使用系统;
使用System.Collections.Generic;
使用System.Threading.Tasks;
使用系统线程;
名称空间控制台App7
{
班级计划
{
const int m_线程=100;
const int n_吞吐量=2000;
const int k_toss_per_second=2000;//注意k_toss_per_second x m_线程>=n_吞吐量
静态void Main(字符串[]参数)
{
var tasks=新列表();
对于(int i=0;icallDoIOStuff());
Task.WaitAll(tasks.ToArray());
Console.WriteLine(“所有线程完成”);
}
静态void callDoIOStuff()
{
int sleep_time=(int)(1000*1.0d/k_toss_/s);
双阈值=(双)n_吞吐量/m_线程/k_丢弃率/u秒;
随机=新随机();
while(true){
睡眠(睡眠时间);
if(random.NextDouble()<阈值)
doIOStuff();
}
}
//如何以特定速率调用此方法?
静态void doIOStuff()
{
//做一些IO工作
}
}
}

您需要注意传递取消令牌和任何参数。使用全局变量时,此代码示例既快速又脏,可以大大改进。

一种方法是在给定的时间段内执行它

创建表示
DoIOStuff
方法的
Action
类型的2000个实例。然后尝试使用此操作启动2000个任务,但只有您才能在1秒内完成(
TakeWhile

var period=TimeSpan.FromSeconds(1);
var maxAmountOfCalls=2000;
var methods=Enumerable.Range(0,maxAmountOfCalls)
.选择(=>doIOStuff)
.ToArray();
var watch=新秒表();
watch.Start();
var任务=方法
.Select(方法=>Task.Run(方法))
.TakeWhile(任务=>watch.appeased

如果时间段较短,此方法开始时将少于所需的量。

以下是一种方法,可用于对异步
doIOStuffAsync
方法进行压力测试:

public static async Task<long> StressTestAsync(
    Func<CancellationToken, Task> taskFactory,
    TimeSpan duration,
    int concurrencyLimit,
    int tasksStartedPerSecondLimit,
    IProgress<long> progress = default,
    CancellationToken cancellationToken = default)
{
    long successfullyCompletedCount = 0;
    using (var linkedCTS = CancellationTokenSource
        .CreateLinkedTokenSource(cancellationToken))
    using (var progressTimer = new System.Threading.Timer(_ =>
    {
        progress.Report(Interlocked.Read(ref successfullyCompletedCount));
    }))
    {
        var concurrencySemaphore = new SemaphoreSlim(concurrencyLimit);
        var perSecondSemaphore = new SemaphoreSlim(tasksStartedPerSecondLimit);
        var completionSemaphore = new SemaphoreSlim(0, 1);
        int pendingCount = 1; // The initial 1 represents the while loop
        var exceptions = new ConcurrentQueue<Exception>();
        linkedCTS.CancelAfter(duration);
        if (progress != null)
            progressTimer.Change(1000, 1000); // Report progress every second
        while (true)
        {
            try
            {
                await concurrencySemaphore.WaitAsync(linkedCTS.Token)
                    .ConfigureAwait(false);
                await perSecondSemaphore.WaitAsync(linkedCTS.Token)
                    .ConfigureAwait(false);
            }
            catch (OperationCanceledException) { break; }
            ReleaseSemaphoreAfterOneSecond();
            StartOneTask();
        }
        if (Interlocked.Decrement(ref pendingCount) == 0)
            completionSemaphore.Release();
        await completionSemaphore.WaitAsync().ConfigureAwait(false); // No token
        cancellationToken.ThrowIfCancellationRequested();
        if (!exceptions.IsEmpty) throw new AggregateException(exceptions);

        async void ReleaseSemaphoreAfterOneSecond()
        {
            try
            {
                await Task.Delay(1000, linkedCTS.Token).ConfigureAwait(false);
            }
            catch (OperationCanceledException) { } // Ignore
            finally
            {
                perSecondSemaphore.Release();
            }
        }

        async void StartOneTask()
        {
            Interlocked.Increment(ref pendingCount);
            try
            {
                var task = taskFactory(linkedCTS.Token);
                await task.ConfigureAwait(false);
                Interlocked.Increment(ref successfullyCompletedCount);
            }
            catch (OperationCanceledException) { } // Ignore
            catch (Exception ex)
            {
                exceptions.Enqueue(ex);
                linkedCTS.Cancel();
            }
            finally
            {
                if (Interlocked.Decrement(ref pendingCount) == 0)
                    completionSemaphore.Release();
                concurrencySemaphore.Release();
            }
        }

    }
    return Interlocked.Read(ref successfullyCompletedCount);
}
concurrencyLit
参数是在任何给定时刻可以并发运行的最大任务数。
tasksStartedPerSecondLimit
参数是任何1秒时间跨度内可以启动的最大任务数。这两个限制相互竞争,因此通常只有其中一个限制了吞吐量。如果任务很快,
并发限制将是限制因素。如果任务速度较慢,则限制因素将是
tasksStartedPerSecondLimit

StressTestAsync
不允许出现异常。
taskFactory
方法引发的任何异常,或者如果任何创建的任务在故障状态下完成,都将导致测试终止


A可以作为可选参数传递,以传播有关当前成功完成任务数的进度报告。

这意味着它要向
doIOStuff
方法发送2000个qps?如果我需要改变这一点,那么我可以将
2000
切换到任何能给我带来那么多qps的数字。正确吗?这与每次调用之间睡眠时间为1/2000秒的简单循环有多大不同?@Tarik,只是使用此方法,我不需要显式创建空任务集合,然后将任务添加到循环中的集合中。代码与描述不匹配。真正发生的是,2000个任务立即排队,它们执行所需的时间没有定义。(这也是一种非常迂回的方式
public static async Task<long> StressTestAsync(
    Func<CancellationToken, Task> taskFactory,
    TimeSpan duration,
    int concurrencyLimit,
    int tasksStartedPerSecondLimit,
    IProgress<long> progress = default,
    CancellationToken cancellationToken = default)
{
    long successfullyCompletedCount = 0;
    using (var linkedCTS = CancellationTokenSource
        .CreateLinkedTokenSource(cancellationToken))
    using (var progressTimer = new System.Threading.Timer(_ =>
    {
        progress.Report(Interlocked.Read(ref successfullyCompletedCount));
    }))
    {
        var concurrencySemaphore = new SemaphoreSlim(concurrencyLimit);
        var perSecondSemaphore = new SemaphoreSlim(tasksStartedPerSecondLimit);
        var completionSemaphore = new SemaphoreSlim(0, 1);
        int pendingCount = 1; // The initial 1 represents the while loop
        var exceptions = new ConcurrentQueue<Exception>();
        linkedCTS.CancelAfter(duration);
        if (progress != null)
            progressTimer.Change(1000, 1000); // Report progress every second
        while (true)
        {
            try
            {
                await concurrencySemaphore.WaitAsync(linkedCTS.Token)
                    .ConfigureAwait(false);
                await perSecondSemaphore.WaitAsync(linkedCTS.Token)
                    .ConfigureAwait(false);
            }
            catch (OperationCanceledException) { break; }
            ReleaseSemaphoreAfterOneSecond();
            StartOneTask();
        }
        if (Interlocked.Decrement(ref pendingCount) == 0)
            completionSemaphore.Release();
        await completionSemaphore.WaitAsync().ConfigureAwait(false); // No token
        cancellationToken.ThrowIfCancellationRequested();
        if (!exceptions.IsEmpty) throw new AggregateException(exceptions);

        async void ReleaseSemaphoreAfterOneSecond()
        {
            try
            {
                await Task.Delay(1000, linkedCTS.Token).ConfigureAwait(false);
            }
            catch (OperationCanceledException) { } // Ignore
            finally
            {
                perSecondSemaphore.Release();
            }
        }

        async void StartOneTask()
        {
            Interlocked.Increment(ref pendingCount);
            try
            {
                var task = taskFactory(linkedCTS.Token);
                await task.ConfigureAwait(false);
                Interlocked.Increment(ref successfullyCompletedCount);
            }
            catch (OperationCanceledException) { } // Ignore
            catch (Exception ex)
            {
                exceptions.Enqueue(ex);
                linkedCTS.Cancel();
            }
            finally
            {
                if (Interlocked.Decrement(ref pendingCount) == 0)
                    completionSemaphore.Release();
                concurrencySemaphore.Release();
            }
        }

    }
    return Interlocked.Read(ref successfullyCompletedCount);
}
await StressTestAsync(
    taskFactory: async ct => await doIOStuffAsync(ct),
    duration: TimeSpan.FromSeconds(30),
    concurrencyLimit: 1000,
    tasksStartedPerSecondLimit: 1000);