.NET 压榨性能之并发处理

2020/01/15 58

并发

优秀软件的一个关键特征就是具有并发性。过去的几十年,我们可以进行并发编程,但是难度很大。以前,并发性软件的编写、调试和维护都很难,这导致很多开发人员为图省事放弃了并发编程。新版 .NET 中的程序库和语言特征,已经让并发编程变得简单多了。自从 Visual Studio 2012 的发布,微软明显降低了并发编程的门槛。以前只有专家才能做并发编程,而今天,每一个开发人员都能够(而且应该)接受并发编程。

什么是并发编程

让程序同时做多件事情,只要你希望程序同时做多件事情,你就需要并发,几乎每个软件程序都会受益于并发。提到“并发”,很多人就会想到“多线程”,但是对于这两个概念,我们仍然要区分一下。

“多线程”是并发的一种形式,但不是唯一的形式,事实上,直接使用底层线程类型在现代程序中基本不起作用,比起老式的多线程机制,采用高级的抽象机制会让程序功能更加强大、效率更高。因此,本文不会设计一些过时的技术。

让我们开始并发编程吧。

从HTTP请求开始

第一步我们打算把 Worktile 中的所有项目(简单来说就是一个列表)抓取出来,这一步相当简单,代码如下:

定义Model

class ProjectNav
{
    [JsonProperty("_id")]
    public string Id { get; set; }
    public string Name { get; set; }
    public string Color { get; set; }
}

请求接口并解析数据

static async Task<List<ProjectNav>> GetProjectNavAsync()
{
    string url = "api/mission-vnext/project-nav";
    var json = await _htttpClient.GetStringAsync(url);
    return JObject.Parse(json)["data"]["projects"].ToObject<List<ProjectNav>>();
}

这样,我们就得到了当前账户下所有的项目了,这个例子似乎不能很好展示出并发编程,别急,咱们接着往下来。

接下来,我们需要获取每个项目中的 Addon 名称。(根据列表的每个元素的ID,进行新的请求,获取详情)按照以前的编程模式,你可能会有以下代码:

Stopwatch stopwatch = Stopwatch.StartNew();
stopwatch.Start();
var navs = await GetProjectNavAsync();
foreach (var item in navs)
{
    var addons = await GetAddonsByProjectIdAsync(item.Id);
    foreach (var addon in addons)
    {
        Console.WriteLine(addon.Name);
    }
}
stopwatch.Stop();

Console.WriteLine($"Total: {stopwatch.Elapsed.TotalSeconds}");

result1

由于上述代码是有很多个HTTP请求,因此每次速度可能不一样,但是我测试的平均值是 2.7s,那么如果使用并发编程呢?

Stopwatch stopwatch = Stopwatch.StartNew();
stopwatch.Start();
var navs = await GetProjectNavAsync();
navs.AsParallel().ForAll(item =>
{
    var addons = GetAddonsByProjectIdAsync(item.Id).GetAwaiter().GetResult();
    addons.AsParallel().ForAll(addon => Console.WriteLine(addon.Name));
});
stopwatch.Stop();

Console.WriteLine($"Total: {stopwatch.Elapsed.TotalSeconds}");

result2

通过对比我们会发现,并发编程能极大的压榨机器,你甚至不需要知道什么是多线程,也不需要知道处理器性能与并发的关系。

再进一步查看日志,我们发现了打印的顺序是不一样的,需要注意的是并发不是按照顺序发请求。

现代设计

大多数并发编程技术有一个类似点:它们本质上都是函数式(functional)的。这里 “functional”的意思不是“实用,能完成任务”,而是把它作为一种基于函数组合的编程模式。如果你接受函数式的编程理念,并发编程的设计就会简单得多。 函数式编程的一个原则就是简洁(换言之,就是避免副作用)。 函数式编程的另一个原则是不变性。不变性是指一段数据是不能被修改的。在并发编程中 使用不可变数据的原因之一,是程序永远不需要对不可变数据进行同步。数据不能修改, 这一事实让同步变得没有必要。不可变数据也能避免副作用。

Parallel

本文的示例代码使用的是 Linq 提供的扩展方法 AsParallel()。如果你需要并发执行多个函数请了解 Parallel 类型。

Parallel 是一个静态类型,有 InvokeForForeach 方法,帮助我们在更多的场景下进行并发编程。

评论