Go 与 C# 对比 第一篇: Goroutines 与 Asy

2019-03-10  本文已影响0人  雨生_

Go 与 C# 对比 第一篇: Goroutines 与 Async-Await

我将写一个系列的文章,来对比C#与GO(译者:就两篇),Go的核心特性是goroutines,这是一个非常棒的起点,C#的替代方案是使用Async/Await 来支持这个特性。

但是实现的方式上还是有一些差异的:

接下来,我会做一些简单的测试:

Go代码如下:

package main

import (
    "flag";
    "fmt";
    "time"
)

func measure(start time.Time, name string) {
    elapsed := time.Since(start)
    fmt.Printf("%s took %s", name, elapsed)
    fmt.Println()
}

var maxCount = flag.Int("n", 1000000, "how many")

func f(output, input chan int) {
    output <- 1 + <-input
}

func test() {
    fmt.Printf("Started, sending %d messages.", *maxCount)
    fmt.Println()
    flag.Parse()
    defer measure(time.Now(), fmt.Sprintf("Sending %d messages", *maxCount))
    finalOutput := make(chan int)
    var left, right chan int = nil, finalOutput
    for i := 0; i < *maxCount; i++ {
        left, right = right, make(chan int)
        go f(left, right)
    }
    right <- 0
    x := <-finalOutput
    fmt.Println(x)
}

func main() {
    test()
    test()
}

C# 代码:

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks.Channels;

namespace ChannelsTest
{
    class Program
    {
        public static void Measure(string title, Action<int, bool> test, int count, int warmupCount = 1)
        {
            test(warmupCount, true); // Warmup
            var sw = new Stopwatch();
            GC.Collect();
            sw.Start();
            test(count, false);
            sw.Stop();
            Console.WriteLine($"{title}: {sw.Elapsed.TotalMilliseconds:0.000}ms");
        }

        static async void AddOne(WritableChannel<int> output, ReadableChannel<int> input)
        {
            await output.WriteAsync(1 + await input.ReadAsync());
        }

        static async Task<int> AddOne(Task<int> input)
        {
            var result = 1 + await input;
            await Task.Yield();
            return result;
        }

        static void Main(string[] args)
        {
            if (!int.TryParse(args.FirstOrDefault(), out var maxCount))
                maxCount = 1000000;
            Measure($"Sending {maxCount} messages (channels)", (count, isWarmup) => {
                var firstChannel = Channel.CreateUnbuffered<int>();
                var output = firstChannel;
                for (var i = 0; i < count; i++) {
                    var input = Channel.CreateUnbuffered<int>();
                    AddOne(output.Out, input.In);
                    output = input;
                }
                output.Out.WriteAsync(0);
                if (!isWarmup)
                    Console.WriteLine(firstChannel.In.ReadAsync().Result);
            }, maxCount);
            Measure($"Sending {maxCount} messages (Task<int>)", (count, isWarmup) => {
                var tcs = new TaskCompletionSource<int>();
                var firstTask = AddOne(tcs.Task);
                var output = firstTask;
                for (var i = 0; i < count; i++) {
                    var input = AddOne(output);
                    output = input;
                }
                tcs.SetResult(-1);
                if (!isWarmup)
                    Console.WriteLine(output.Result);
            }, maxCount);
        }
    }
}

Go输出内容:

C:\Projects\GoTest\src>go run ChannelsTest.go
Started, sending 1000000 messages.
1000000
Sending 1000000 messages took 3.5034779s
Started, sending 1000000 messages.
1000000
Sending 1000000 messages took 808.9572ms

C# 输出内容:

C:\Projects\ChannelsTest>dotnet run -c Release -f netcoreapp1.1
1000000
Sending 1000000 messages (channels): 3545.006ms
1000000
Sending 1000000 messages (Task<int>): 1693.675ms

在我们讨论结果之前,关于我们的测试代码,有几个注意点我们在这里讨论一下:

原始结果对比:

所以为啥Go第二次执行这么快嘞? 解释起来很简单,当你启动goroutine的时候,Go需要分配8K的堆内存给他,而这些内存可以重用,所以第二次的时候,不需要分配更多的内存给他,证明图如下:

Memory Detail

Go为1M数量的goroutine 分配了近9GB的内存,假设每个goroutine consimes 至少需要8KB,那么这些内存就大概达到了8GB。

如果我们增加测试数量到2M的话,我的机器直接挂了(内存不足)。

所以两者的差距显而易见,让我们来思考一下为啥C#生成的这么慢:

现在让我们修改一下测试的内容,将传递的消息降低到20K,这个值比较接近于我们现实应用的最大值(服务器上Socket的套接字接近20K)

C:\Projects\ChannelsTest>go run ChannelsTest.go
Started, sending 20000 messages.
20000
Sending 20000 messages took 75.0496ms
Started, sending 20000 messages.
20000
Sending 20000 messages took 18.0513ms

C:\Projects\ChannelsTest>dotnet run -c Release -f netcoreapp1.1
20000
Sending 20000 messages (channels): 49.297ms
20000
Sending 20000 messages (Task<int>): 28.702ms

结果不难看出,两者很接近了:

数量换到5K的时候:

C:\Projects\ChannelsTest>go run C:\Projects\GoTest\src\ChannelsTest.go
Started, sending 5000 messages.
5000
Sending 5000 messages took 15.0399ms
Started, sending 5000 messages.
5000
Sending 5000 messages took 8.0213ms

C:\Projects\ChannelsTest>dotnet run -c Release -f netcoreapp1.1
5000
Sending 5000 messages (channels): 15.027ms
5000
Sending 5000 messages (Task<int>): 6.881ms

在这里,可以看到C#基于task的效果,比Go还要好,C#的通道测试,比Go最好的状态慢2倍左右。

为啥C#使用Task的效果更好?

Goroutines vs async-await 结论:

我们看一下最关键的不同点:

总的来说,实现方式差异很明显,所以影响也是非常显著的。在后续的文章中,我会用 async-await-goroutines 编写更加健壮/真实的测试。

翻译

原文链接:Go vs C#, part 1: Goroutines vs Async-Await

译者:JYSDeveloper

上一篇 下一篇

猜你喜欢

热点阅读