dotnet / csharplang

The official repo for the design of the C# programming language
11.36k stars 1.02k forks source link

Next big C# language should focus on supporting building distributed system and concurrency programming #502

Closed asydneylover closed 4 years ago

asydneylover commented 7 years ago

The next version of C# language should be focus on making it easier in building distributed system and concurrency programming instead of introducing more syntax sugar. That's the only way make C# really a big programming language in comparing with the others such as Java. One of the feature I think is a native support to implement Golang's co-routines.

HaloFour commented 7 years ago

C# already has asynchronous coroutines in the form of async/await. How would implementing goroutines improve on that?

Actual distributed programming can already be solved through libraries such as Akka.

smoothdeveloper commented 7 years ago

@uyhung what feature of Java do you feel makes it more suitable than C# in this use case? I'd think it is more about libraries or middleware than language level.

sharwell commented 7 years ago

@uyhung I'm curious if you mentioned Java because of experience working on distributed/concurrent programming in that environment, or if you just listed it as an example. As someone who spent a great deal of time on asynchronous and concurrent (but not so much distributed) programming in Java and C# over the past several years, I came to essentially the opposite conclusion.

HaloFour commented 7 years ago

Indeed, Java's advantage here is, at best, ecosystem. The language and JDK, if anything, actively fight you at every step.

Having to maintain both a C# WebAPI site as well as a JAX-RS site the experience isn't even remotely comparable. It doesn't help that J2EE is still stuck in Java7 land which is all blocking on futures and callback hell. The experience is somewhat less awful using alternate JVM languages, but not by much. To my knowledge Spring has some support for coroutines via AOP and bytecode rewriters but only at the method level. There's also EA's Orbit library which does something similar. I'm actively working to incorporate the latter into my projects because I find it appalling to have to manually write callback/lambda continuations anymore, but none of that even remotely comes close to just how well async/await works out of the box. I expect Java to copy it wrong by Java 10 or so, in a few decades from now.

Funnily enough probably the best concurrency library on the JVM right now is RxJava. It irks me to no end that Microsoft's own framework is getting more love there than at home.

benaadams commented 7 years ago

@uyhung This should do the same thing as the "A Tour Of Go: Goroutines" example does

using System;
using System.Threading.Tasks;

public class Program
{
    public static async Task Say(string s)
    {
        for (var i = 0; i < 5; i++)
        {
            await Task.Delay(100);

            Console.WriteLine(s);
        }
    }

    private static async Task MainAsync(string[] args)
    {
        var go = Task.Run(() => Say("world"));
        var t = Say("hello");

        await Task.WhenAll(go, t);
    }

    public static void Main(string[] args)
    {
        MainAsync(args).Wait();
    }
}

edit improved example here https://github.com/dotnet/csharplang/issues/502#issuecomment-312449000

AlgorithmsAreCool commented 7 years ago

@uyhung Here are a couple of things that you might find interesting:

Corefxlab Channels (prototype)

Orleans

TPL Dataflow

asydneylover commented 7 years ago

@sharwell @smoothdeveloper , I had 7 years experienced working with C#, then the last two years with Java. And yes, in term of language design C# is far better than Java, especially Java's Generic is implemented so bad. You're correct, Java as a language itself does not have any special features to support better concurrency / distributed system, but it's ecosystem is amazing . In Java they have Akka, Karaf, Kafka, Loghom, Vert.X, Spark, Elastic Search, Hadoop, Spring etc. Even Rx is originally created in our .NET world but now the world seems only know the existence of Rx Java, people even believe that Rx.NET is ported from RxJava :)

Hopefully that Microsoft can find a way to make .NET Core a really big ecosystem.

HaloFour commented 7 years ago

@uyhung

This repo is specifically for changes to the C# language. If you want to make specific suggestions for inclusions in the BCL to support distributed systems you can try the CoreFX repo. But unless you're expecting Microsoft to build out that ecosystem on their own I'm not sure what you're expecting to happen. Microsoft is providing the environment and trying to expand it's reach. It's up to others to populate the ecosystem. It's not like Sun/Oracle have anything to do with any of those projects that you've mentioned. If anything they flourished due to the massive gaps in functionality left by the JDK.

benaadams commented 7 years ago

You can specify you own custom Task scheduler it just uses the ThreadPool by default to maximise the CPU usage

HaloFour commented 7 years ago

@sgf

It sounds like you don't understand how async/await works. It has nothing at all to do with threading. It is purely user-mode and is completely agnostic to whatever switching mechanism is employed, if one at all. It is completely disconnected from Task, especially in C# 7.0 where even the requirement to use Task as a proxy return value has been lifted. There's no requirement to use a ThreadPool or any of the rest of the TPL.

async/await is very elegant and very simple. It serves as the prototype for coroutines for a number of other languages, including JavaScript, Kotlin and C++.

yaakov-h commented 7 years ago

@sgf async/await does not always use the threadpool and does not always switch threads, or other contexts. You have a lot more control than you think.

HaloFour commented 7 years ago

@sgf

Do you actually have a concrete proposal to improve the language or are you just here to complain that you don't like C#? If you don't care to listen to how a feature that you are disparaging incorrectly actually works then there is little reason for anyone to attempt to hold a dialog with you.

I have nothing against Go's Goroutines. I think that they work quite well in that language. However Go was designed very explicitly around its threading/synchronization model with its baked-in user-mode switching and kernel-mode backing of fibers. I don't believe that attempting to add such to an existing language/framework would be met with as much success since nothing that already exists would understand how the cooperative multithreading would work nor would they understand how the fiber mechanism would work. Any existing code that blocked the kernel thread would block every Goroutine scheduled to be handled by that thread. That is the issue with fiber models in general unless you build everything around that from day one.

sharwell commented 7 years ago

@sgf I can say with very high confidence that there are many people working on or around C# for whom performance is a very important priority across a wide variety of scenarios. For example, there are people like @stephentoub (and to a lesser degree myself) who are specifically interested in improving behavior of multithreaded applications through smarter algorithms (e.g. lock-free approaches), and people like @benaadams who never let us hear the end of it when anything is slow (he's a fun guy to have dinner with and talk about what can be better!). We also have people like @gafter and @jaredpar working on the language itself to both enable and encourage practices with the C# language that lead to better performing and more reliable applications for end users.

This is not to say we're the best. Of course I may have an opinion on that, but it's not the point I'm trying to make. What I am saying, is if you believe you've found specific areas where improvements to the language, the runtime, or the tooling will enable end users to have a better-performing product, there will be multiple people eager to consider the ideas regardless of which subsystem they lie in. Just remember that the ecosystem is very large so even when people are working with you to make a change it can take a bit of time to actually ship out. đź‘Ť

HaloFour commented 7 years ago

@sgf

Again, are you planning on actually proposing something here? Your comments do nothing to continue the discussion. In fact I'd go so far to say that they simply demonstrate that you don't understand what you're talking about.

native,coroutine is the current mainstream technology.

C# has native coroutines. That is what async/await and yield are. It is becoming mainstream in that other languages are copying it.

No, they're not the same as goroutines. Only Go has goroutines. The only reason they work in Go is because Go was designed very specifically around them. You cannot take fiber-based cooperative multitasking (a very old concept, by the way) and hack that into existing languages while protecting it from native threading concerns and blocking (which has devastating consequences). You just can't.

Java is also progressing more good.

How? What does the language offer you here that C# doesn't? Nothing. Absolutely nothing. From a language and framework point of view Java is well over a decade behind C# in terms of distributed and concurrent programming.

mattwar commented 7 years ago

If anyone has any ideas on how to make the language friendlier to distributed and concurrent programming feel free to speak up.

HaloFour commented 7 years ago

@mattwar

88

Support for IObservable<T> in #43 Possibly some kind of async await support in match or with pattern matching in general

mattwar commented 7 years ago

88 is somewhat interesting because it removes some of the boiler plate of writing and coordinating event handling logic. Like the inverse of async/await. Not sure what it means to keep all those parameter values around between separate API invocations, and what happens to the object's state when API's calls are not made in the expected order, or the concurrency of the object itself with regards to overlapping calls, etc.

Maybe the object is generated with locks that only let new invocations through when the previous initiated sequences are complete. Or maybe if the API's are async, they wouldn't need to block.

Interesting to think about.

HaloFour commented 7 years ago

@mattwar

The point is to avoid all blocking. The mailbox methods always return instantly, and the chorded methods are always async. The parameters captured from the mailbox methods are available in the chorded methods in the order in which they were invoked. They're pretty similar to BufferBlock<T> using the Post and ReceiveAsync extension methods.

Where it gets interesting is when multiple mailbox methods are used with multiple chorded methods. As I describe you can do this with Dataflow today using JoinBlock<T1, T2>s but of course it requires a lot more boilerplate code.

mattwar commented 7 years ago

@HaloFour I'm still trying to get my head around it. I understood it a lot better years ago when C-omega work was ongoing, but now I am having a hard time understanding if this is a generally interesting capability or a narrowly focused one. For instance, in order for all the parameters from all the mailbox functions to be available to the async (chorded) method, each mailbox function can only be called once. That's great make in straight forward to understand how everything combines, but may only map to a narrow subset of interesting dialog/protocol patterns. Even if it doesn't solve everything is it an adequate building block? Not sure yet. I'm trying to model it and it seems like every chord grouping has a lot of expensive data structures going on, so it is a bit deceptive on the cost of making the synchronization work.

John0King commented 7 years ago

Goroutines don’t suffer from “async all the way” problem. Async-await implies that if you make a chain of function calls (A calls B, B calls C, … Y calls Z), and both A and Z are async functions, B … Y also have to be async functions, otherwise the model won’t work (non-async Y can’t await for Z, non-async X can’t await for Y, etc. — which means they either have to “start and forget” corresponding async functions, or wait for them synchronously, or become async as well). On contrary, there is no such constraint in Go: you can read from a channel in any function, and no matter what, it’s always an asynchronous operation. That’s actually a big advantage, since you don’t have to plan on what’s going to be asynchronous ahead of time. In particular, you can write a query(…) method invoking some query provider to get the result, and this provider can do this either synchronously or asynchronously dependently on the implementation — but you, as an author of query(…) method, don’t have to think about this while you write it.

I read from this article

I think the key point here is to make any synchronously method can be use as asynchronously method without reimplement it with async and Task 。

benaadams commented 7 years ago

@John0King Go does it by not giving you the option of using dedicated threads, having transferable thread stacks and having all functions interruptable (by runtime); so no function is really sync; even CPU bound ones and you can't control your scheduling (though you can call LockOSThread to move to blocking mode).

Simplifying, the async keyword means "I don't need to own this thread, and allow this function's stack to be able to saved and resumed", and await are the explicit save/restore points. This causes the "async all the way" as functions that are not-async "own" the thread while they are running; so its about where you mark the jumping off point for thread flexibility. Though "async all the way" is a misnomer and Task(-like) returning is probably better; much like a "sync" is often incorrectly used for "blocking".

.NET gives you more power and control than Go, but can be more complicated as a result. With the most common pitfall being using blocking methods on a threadpool thread; when you don't care about thread ownership; but since you need to be explicit its often missed.

You can even serialize the execution of async methods, transfer it to a different machine and then pick up the execution later...

CyrusNajmabadi commented 7 years ago

@sgf: Do you have a concrete proposal on what you would like to see improved?

benaadams commented 7 years ago

Updated the comparison for C#7.1 and example from the "A Tour Of Go: Goroutines" and matched the formatting to better

Go version

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

C#7.1 version

using System.Threading.Tasks;
using static System.Console;
using static System.Threading.Tasks.Task;

public class Program
{
    static async Task Say(string s) {
        for (var i = 0; i < 5; i++) {
            await Delay(100);
            WriteLine(s);
        }
    }

    static Task Main() => WhenAll(
        Run(() => Say("world")),
        Say("hello")
    );
}
skynode commented 6 years ago

Kinda late to the party but interesting thread right here. Unfortunately @sgf is unable to clearly articulate his concerns or make constructive proposals for improvement of C#.

I personally have had cases where I had to think about the program flow while mixing async/await methods with non-async/await methods but I guess it's probably due to some knowledge deficit on my part. For instance, I just learned lately that using Task.Run(() => ) is not quite as performant as the standard procedure? Will work more towards fully understanding the inner mechanics of it.

Any suggested resources would be greatly appreciated @HaloFour @mattwar @benaadams. Thanks all!

svick commented 6 years ago

@skynode

For instance, I just learned lately that using Task.Run(() => ) is not quite as performant as the standard procedure? Will work more towards fully understanding the inner mechanics of it.

What "standard procedure"? If you're comparing something like Task.Run(() => M()) with just M(), then of course Task.Run() is going use more CPU, because it has to do more. (Depending on how exactly you use it, it might make your code faster, due to parallelization.)

HaloFour commented 6 years ago

There's yet to be a proposal here. Goroutines can't be implemented in C#. They can't be implemented in any language that isn't Go.

Channels can be implemented in libraries and require no language changes. There already is Rx, TPL DataFlow, Akka.NET, and likely many others that fill that need, as well as the concurrent collections in the BCL.

masonwheeler commented 6 years ago

@HaloFour

There's yet to be a proposal here. Goroutines can't be implemented in C#. They can't be implemented in any language that isn't Go.

This is trivially untrue by the principle of Turing Equivalence.

A goroutine is really nothing more than a thread, in a language with a built-in syntax keyword for spawning threads. You can already create threads in C#, and Channels are essentially just a blocking queue. There's really nothing there that we don't already have access to. (Yes, it's technically a fiber, but show me one thing you can do with goroutines and channels that you can't trivially accomplish with threads and blocking queues.)

AlgorithmsAreCool commented 6 years ago

@masonwheeler I think @HaloFour's point is that the concept of a goroutine exists only in go. Similar or equivalent constructs may exist in other languages but they are not strictly speaking goroutines.

I believe the point is that asking for C# to embed a core go concept is the wrong question to ask, instead a proposal for a construct that is native to C# is what is needed to move forward.

masonwheeler commented 6 years ago

@AlgorithmsAreCool And what new construct would that be?

Again, what can Go do that C# can't do just as easily with threads (or Task.Run, if you prefer) and blocking queues? What problem exists here that we need a solution to?

AlgorithmsAreCool commented 6 years ago

I don't think i was clear about my own opinion. I believe that async/await + channels are nearly equivalent to goroutines.

masonwheeler commented 6 years ago

Actually, now that I think about it, you shouldn't even need that.

Channels are a mechanism to return zero or more values of a certain type from a coroutine. Doesn't that sound a lot like yield to you?

scalablecory commented 6 years ago

TPL and async/await are far more powerful than goroutines. Speaking as someone who used to code for I/O completion ports in C++ and all the various models in .NET, async/await really is the holy grail of async.

And async/await is not appreciably more difficult to use than goroutines on the common path. This topic is a non-starter until someone gives a clear demonstration of what C# is lacking that Go/Java/etc. provides, and proposes an idiomatic solution that applies it to C#.

That said, async/await can be a part of a concurrent system, but it is not intended to help solve a complex concurrency problem directly. Essentially, the same as goroutines. @HaloFour listed out a number of libraries built to assist in concurrent programming -- TPL Dataflow is a pretty great one. I don't know what more the language itself could do to make things easier for concurrent code.

HaloFour commented 6 years ago

@masonwheeler

Goroutines are not threads. They are fiber-backed cooperative multitasking which require coordination of every aspect of the language and runtime to work correctly. Go was designed from scratch around that concept specifically because it's not feasible to retrofit on an existing language. In any other language it is way too easy to block the current thread, and that would be a massive problem for "goroutines" as one blocked thread could mean thousands of blocked fibers.

Remember Windows 3.x? Block that thread and everything goes unresponsive. That would be goroutines in C#, or any other non-Go language.

masonwheeler commented 6 years ago

@HaloFour You're talking about implementation details. This still does not answer the question: what thing can you do with goroutines that it is not trivial to do with existing C# and .NET Framework features?

Without an answer to this question, there is no problem and thus no need for a solution. And as someone who works in Go as a part of my job, I have yet to find anything that answers this question. In fact, IME many times Go's system gets in the way and makes things that should be simple much more complicated.

HaloFour commented 6 years ago

@masonwheeler

This still does not answer the question: what thing can you do with goroutines that it is not trivial to do with existing C# and .NET Framework features?

Spin up a bajillion "threads" and write a bunch of synchronous-looking code without bringing the system to its knees.

Without an answer to this question, there is no problem and thus no need for a solution.

I totally agree with this. Go offers a very different model. There's nothing wrong with it, but I feel that it's incompatible with that of C#.

masonwheeler commented 6 years ago

Spin up a bajillion "threads" and write a bunch of synchronous-looking code without bringing the system to its knees.

I'm not so sure. As I pointed out above, is there any way that "goroutine + channel" is not equivalent to an iterator method? (The IEnumerable<T> return value is the channel, the yield return operation is the "insert to channel" operation, and the caller getting the next value from the enumerable is the "read from channel" operation.) You could "spin up" any number of iterators without bringing everything down.

Rattenkrieg commented 6 years ago

Goroutines can't be implemented in C#. They can't be implemented in any language that isn't Go.

Actually here is .net implementation of green threads and channels even more powerfull and expressive (yet syntactically close) than goroutines: https://github.com/Hopac/Hopac

omidkrad commented 6 years ago

@benaadams already gave a comparison of goroutine in Go and async/await in C#. Now that System.Threading.Channels is released (as cited here) how would the "A Tour Of Go: Goroutines" look like with .NET channels?

stephentoub commented 6 years ago

how would the "A Tour Of Go: Goroutines" look like with .NET channels?

Many of the samples can be translated in a line-by-line approach using System.Threading.Channels, e.g. the example:

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

can be translated as something like:

using System;
using System.Threading.Channels;
using System.Threading.Tasks;

class Example
{
    static async Task Fibonacci(int n, Channel<int> c)
    {
        int x = 0, y = 1;
        for (int i = 0; i < n; i++)
        {
            await c.Writer.WriteAsync(x);
            (x, y) = (y, x + y);
        }
        c.Writer.Complete();
    }

    static async Task Main()
    {
        var c = Channel.CreateBounded<int>(10);
        var t = Task.Run(() => Fibonacci(10, c));
        while (await c.Reader.WaitToReadAsync())
            if (c.Reader.TryRead(out int i))
                Console.WriteLine(i);
    }
}

Note that the while loop in there will be writable as a foreach await loop once that language feature is available (https://github.com/dotnet/csharplang/issues/43), e.g.

foreach await (int i in c.Reader) Console.WriteLine(i);
masonwheeler commented 6 years ago

@stephentoub Wow, that example is ugly! As I asked above, is there any reason at all why anyone would want to use that over a much simpler implementation that uses yield return instead of a channel?

CyrusNajmabadi commented 6 years ago

that uses yield return instead of a channel?

'yield return' is fine when you are just dealing with a unidirectional stream of data. Channels are great when you have bidi streams.

'yield return' (in the current shipped form) is fine if you are fine with synchronous production. It's not great if you want to involve async work.

'yield return' is great if you are just generating values, and have no contract between producer and consumer about those values. But say you want to do thigns in a bounded fashion, that's non trivial with yield-return, and simple with channels.

stephentoub commented 6 years ago

over a much simpler implementation that uses yield return instead of a channel?

Huh? I believe your suggestion was to add language features that would let you replace this:

    static async Task Fibonacci(int n, Channel<int> c)
    {
        int x = 0, y = 1;
        for (int i = 0; i < n; i++)
        {
            await c.Writer.WriteAsync(x);
            (x, y) = (y, x + y);
        }
        c.Writer.Complete();
    }

with this:

    static Channel<int> Fibonacci(int n)
    {
        int x = 0, y = 1;
        for (int i = 0; i < n; i++)
        {
            yield return x;
            (x, y) = (y, x + y);
        }
    }

am I understanding your suggestion correctly? So your "wow, that is ugly" is about a call to "c.Writer.Complete()" to signal that the channel is done? Or writing "await c.Writer.WriteAsync(x)" instead of "yield return x"? If that's your concern, I disagree that it's so much more ugly. Plus the library based solution is significantly more flexible, allowing you to control channel policies (is it bounded? what's the bound? does it support concurrent consumption or is it optimized for a single consumer? etc.), allowing you to have multiple producers all going into the same channel, etc.

The request was for a translation of a Go example. I provided that. The channels library is intended for multi-threaded producer/consumer scenarios; IEnumerable<T> and yield return are not good for that.

masonwheeler commented 6 years ago

@stephentoub My suggestion was that adding stuff to support goroutines and channels isn't necessary at all when we have async and yield. After working with Go professionally, I haven't seen anything they do in real-world code that couldn't be done just as easily (if not more easily!) with the coroutines we already have in C#.

CyrusNajmabadi commented 6 years ago

@stephentoub My suggestion was that adding stuff to support goroutines

Nothing was added here wrt goroutines. As you can see in the example, it's just a simple Task.Run :)

and channels isn't necessary at all

I don't see how that's teh case. channels have their use like many other collections and other async building blocks. i.e. IEnumerable is great, but i still often need Lists and whatnot. I still may need blocking collections, etc. etc.

Now, if you don't find channels useful or desirable... def don't use them :)

stephentoub commented 6 years ago

async is a key piece of the story, but it alone is insufficient for some scenarios. Look around many concurrent/parallelized code bases, and you'll find producer/consumer scenarios where a producer needs to hand off data to a consumer and keep running without waiting for the consumer, or only waiting based on certain policies. All of these end up needing some kind of data structure to store that handed off items, along with some kind of synchronization to coordinate appropriately, e.g. since we're talking here about C#, here's an example from Roslyn: https://github.com/dotnet/roslyn/blob/6c5c45b1c5520a91436aac0bc311e55f09e1abd7/src/Compilers/Core/Portable/DiagnosticAnalyzer/AsyncQueue.cs#L16. That's all System.Threading.Channels is, a set of base classes for such data structures and a few concrete implementations providing support for the most common needs. It also provides APIs for reading and writing that data using async APIs. Can you do all this without System.Threading.Channels? Of course. But the purpose of corefx is to provide libraries of commonly needed functionality, which this is.

masonwheeler commented 6 years ago

Look around many concurrent/parallelized code bases, and you'll find producer/consumer scenarios where a producer needs to hand off data to a consumer and keep running without waiting for the consumer, or only waiting based on certain policies ... That's all System.Threading.Channels is, a set of base classes for such data structures and a few concrete implementations providing support for the most common needs. It also provides APIs for reading and writing that data using async APIs. Can you do all this without System.Threading.Channels? Of course. But the purpose of corefx is to provide libraries of commonly needed functionality, which this is.

But don't we already have that in corefx as well? I'm having trouble seeing what new capabilities Channels adds to our toolbox.

benaadams commented 6 years ago

Wow, that example is ugly!

Not sure what direction you are after... If you wanted to go more terse, could add some using static

using System.Threading.Channels;
using System.Threading.Tasks;
using static System.Console;
using static System.Threading.Tasks.Task;
using static System.Threading.Channels.Channel;

class Example
{
    static async Task Fibonacci(int n, ChannelWriter<int> c)
    {
        (int x, int y) = (0, 1);
        for (int i = 0; i < n; i++) {
            await c.WriteAsync(x);
            (x, y) = (y, x + y);
        }
        c.Complete();
    }

    static async Task Main()
    {
        var c = CreateBounded<int>(10);
        var r = c.Reader;

        _ = Run(() => Fibonacci(10, c.Writer));

        while (await r.WaitToReadAsync())
            if (r.TryRead(out int i))
                WriteLine(i);
    }
}

There is a lot of flexibility in the language, whereas golang dictates a single form of formatting.

But don't we already have that in corefx as well? I'm having trouble seeing what new capabilities Channels adds to our toolbox.

BlockingCollection blocks the thread and holds onto it while waiting; Channels is async and releases the thread to do other things while waiting.

orthoxerox commented 6 years ago

@masonwheeler you cannot await Dequeue() a concurrent queue and even if you could, there's no way to tell apart a queue that doesn't contain any elements right now, but will later and a queue that you'll never dequeue a new item from. That's what channels give you.

CyrusNajmabadi commented 6 years ago

How can i asynchronously pull a value from many different ienumerable streams and operate on the first value i get to any stream? :)

This is one of the things that is super trivial with channels (and we do this heavily on my current project). I do love ienumerables, but it's not always hte right approach, especially when composings lots of streams of data (esp in today's modern async world).

masonwheeler commented 6 years ago

@benaadams That's a good point. I hadn't considered the sync/async distinction.

@CyrusNajmabadi That's not "super trivial" at all in Go. It could be if Go had support for generics, but since it doesn't, you have to code up support for that manually every time.