在 C# .NET 中对任务和线程使用并行性的 6 种方法

作者:微信公众号:【架构师老卢】
3-18 9:59
26

概述:在任何时候,开发人员都会陷入某种情况,您需要使用并行性,无论是提高服务的性能,还是作业,无论您的程序目标是什么。但是,当我们需要减少流程的时间时,我们总是会考虑将流程分成多个部分,并在同一时刻执行它。在那一刻,我们想起了并行性,但我们面临着一个问题:有什么更好的方法来实现它?您将在本文中学到什么?如何在 C# .NET 中使用 SemaphoreSlim 调用的 .NET 类在 Threads 和 Tasks 中实现并行性。在本课程中,我们将帮助应用信号量的概念。线程与任务哪个更好?好吧,本文中使用的证据显示了使用 Threads 的实现,执行时间要短得多。1 – 使用线程在此示例中,我们将每

在任何时候,开发人员都会陷入某种情况,您需要使用并行性,无论是提高服务的性能,还是作业,无论您的程序目标是什么。但是,当我们需要减少流程的时间时,我们总是会考虑将流程分成多个部分,并在同一时刻执行它。在那一刻,我们想起了并行性,但我们面临着一个问题:有什么更好的方法来实现它?

您将在本文中学到什么?

如何在 C# .NET 中使用 SemaphoreSlim 调用的 .NET 类在 Threads 和 Tasks 中实现并行性。在本课程中,我们将帮助应用信号量的概念。

线程与任务哪个更好?

好吧,本文中使用的证据显示了使用 Threads 的实现,执行时间要短得多。

1 – 使用线程

在此示例中,我们将每个线程添加到一个列表中,在最后一个示例中,我们正在等待所有线程完成。此外,我们还使用锁将变量计数放在线程安全中,并使用 anyObject 辅助工具来确保锁 (anyObject) 中的所有代码保持安全而不会损坏最终结果。在该实现中花费的执行时间为 2 秒,与下面的其他实现相比更好。

var count = 0;
var anyObject = new object();
UseThreads();

void UseThreads()
{
    var threads = new List<Thread>();

    for (int i = 0; i < 100; i++)
    {
        var thread = new Thread(() =>
        {
            AnyMethod();
            lock (anyObject) count++;
        });

        thread.Start();
        threads.Add(thread);
    }

    threads.ForEach(t => t.Join());
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 2 seconds

2 – 使用任务

在这个例子中,如何给出上面的例子,我们将每个任务添加到一个列表中,在最后,我们正在等待所有任务完成。此外,我们还使用锁将变量计数放在线程安全中,并使用 anyObject 辅助工具来确保锁 (anyObject) 中的所有代码保持安全而不会损坏最终结果。在该实现中花费的执行时间为 11 秒,与第一次相比,我们可以看到多 9 秒的差异。到目前为止,这不是最好的选择。

var count = 0;
var anyObject = new object();
UseTasks();

void UseTasks()
{
    var tasks = new List<Task>();

    for (int i = 0; i < 100; i++)
    {
        var task = new Task(() =>
        {
            AnyMethod();
            lock (anyObject) count++;
        });

        task.Start();
        tasks.Add(task);
    }

    Task.WaitAll(tasks.ToArray());
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 11 seconds

3 – 使用 Parallel for

在这种情况下,收益是减少线代码,它贡献了干净的代码。但是在该实现中花费的执行时间为 12 秒。

var count = 0;
var anyObject = new object();
UseParallelFor();

void UseParallelFor()
{
    Parallel.For(0, 100, (i) =>
    {
        AnyMethod();
        lock (anyObject) count++;
    });
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 12 seconds

4 – 使用 Parallel ForEach

在此示例中,按照上面的示例,区别在于遵循 ForEach 的相同想法,并且花费的执行时间相同为 12 秒。

var count = 0;
var anyObject = new object();
UseParallelForEach();

void UseParallelForEach()
{
    Parallel.ForEach(new int[100], (i) =>
    {
        AnyMethod();
        lock (anyObject) count++;
    });
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 12 seconds

5 – 将信号量与线程一起使用

在这里,我们使用了信号量概念,其中我们定义了将在类 SemaphoreSlim 中使用的线程数。如何参数我们获取我的处理器中包含的逻辑处理器的数量。例如:如果您的处理器有 8 个逻辑处理器,您将有 8 个线程,但如果您想要的线程数多于处理器的逻辑处理器数量,则可能会失去每个线程的处理能力。
通过此实现,我们在执行时间上花费了 13 秒。我们会记住,我们执行的是相同的 AnyMethod() 函数。

var count = 0;
var anyObject = new object();
UseSemaphoreWithThreads();

void UseSemaphoreWithThreads()
{
    var semaphoreSlim = new SemaphoreSlim(Environment.ProcessorCount);
    var threads = new List<Thread>();

    for (int i = 0; i < 100; i++)
    {
        var thread = new Thread(() =>
        {
            semaphoreSlim.Wait();

            AnyMethod();
            lock (anyObject) count++;

            semaphoreSlim.Release();
        });

        thread.Start();
        threads.Add(thread);
    }

    threads.ForEach(t => t.Join());
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 13 seconds

6 – 将信号量与任务一起使用

在这里,我们使用了与上面相同的想法,但使用 Tasks,结果是一样的,13 秒。

var count = 0;
var anyObject = new object();
UseSemaphoreWithTasks();

void UseSemaphoreWithTasks()
{
    var semaphoreSlim = new SemaphoreSlim(Environment.ProcessorCount);
    var tasks = new List<Task>();

    for (int i = 0; i < 100; i++)
    {
        var task = new Task(() =>
        {
            semaphoreSlim.Wait();

            AnyMethod();
            lock (anyObject) count++; 

            semaphoreSlim.Release();
        });

        task.Start();
        tasks.Add(task);
    }

    Task.WaitAll(tasks.ToArray());
}

void AnyMethod() => Thread.Sleep(1000);

//Time spent: 13 seconds

关于Thread Safe

当我们使用线程时,我们可以对每个线程的外部上下文变量进行一些风险,例如如何计算,结果是否有变化并且不会给出确切的结果,对我们来说不感兴趣。为了解决这个问题,我们可以使用锁并确保我们的结果安全。下面是一个计数的例子,如果我没有实现锁(anyObject),计数就不能完全得到它的结果,因为线程之间的竞争。

lock (anyObject) count++; 

// Or you can use "Interlocked.Increment(ref count);" but you cannot
// use in this case "count++;" directly because it is not thread safe
// and the value of count will be wrong.

结论

这些选项,更好的性能是使用 Threads 在 2 秒内执行,但更好的实现是使用 Parallel 的示例,代码行更少,有利于干净的代码。好吧,您知道为您的问题做出更好的选择。

相关留言评论
昵称:
邮箱:
阅读排行