В 2015 году моей команде сказали, что мы собираемся начать использовать новый язык для внутренней обработки, круто ...

Когда я только что закончил информатику, меня раздражала идея выучить совершенно чужой язык, потому что последние 4 года я потратил на изучение абсолютно чужих языков.

Также язык назывался «Go».

Короче говоря, я влюбился. Go - очень красивый язык, и независимо от того, устраивает ли вас параллелизм или нет, Go очень затрудняет написание ужасного кода.

Самыми большими коммерческими аргументами для меня в Go были «Горутины» и «Каналы».

Для тех, кто не знает, вот краткое описание этих двух:

Goroutines are functions or methods that run concurrently with other functions or methods. Goroutines can be thought of as light weight threads. The cost of creating a Goroutine is tiny when compared to a thread. Hence its common for Go applications to have thousands of Goroutines running concurrently
A channel is a communication object using which goroutines can communicate with each other. Technically, a channel is a data transfer pipe where data can be passed into or read from. Hence one goroutine can send data into a channel, while other goroutines can read that data from the same channel.

Настоящая прелесть Go заключается в том, что любая простая функция запускается асинхронно с помощью простого ключевого слова: go.

Темы, горутины и TPL.

В C # потоки не такие уж и дешевые, размер их стека указывается и выделяется заранее и никогда не изменится.

Горутины намного дешевле. Размер их стека является динамическим и изменяется в соответствии с требованиями приложения (мы говорим о КБ).

С появлением платформы .NET 4 была представлена ​​библиотека параллельных задач (TPL), чтобы упростить параллелизм для разработчиков C #. TPL будет динамически масштабировать степень параллелизма и обеспечивать эффективное использование всех доступных процессоров.

Хотя, если бы задачи C # обрабатывались как горутины, краткосрочные операции не достигли бы желаемой эффективности.
Фактически, эти операции привели бы к таким большим накладным расходам на распараллеливание, что вся программа работала бы гораздо медленнее.

Эта ловушка параллелизма существует просто потому, что создание нового потока - дорогостоящая работа, особенно по сравнению с горутинами, создание которых намного дешевле.

Go C#

Моя цель сейчас - имитировать поведение ключевого слова go и каналов.

Я создал статический класс, который будет запускать операцию одновременно с одним вызовом, и другой класс для обработки сообщений в очереди.

Я назвал их Concurrent и Pipeline. Оба они по умолчанию обрабатывают строковые типы.

Я обрабатываю общие типы с помощью PipelineType ‹T› и ConcurrentType ‹T›.

Конвейер - это канал C #.

Конвейер имеет три метода:

pipeline = new Pipeline();
pipeline.Send("Hello"); // send a new message
pipeline.Read;          // wait for a new message.
pipeline.Close;         // close the pipeline

В горутине есть три похожих метода:

pipeline := make(chan string)
var s string = <-pipeline [Read]
pipeline <- "Hello"       [Send]
close(pipeline)           [Close]

Можно читать конвейер, пока он не будет закрыт.

string s = pipeline.Read
while (s != Pipeline.Close)
{
  // do something
  s = pipeline.Read;
}

Горутину можно читать, пока она не будет закрыта.

for s := range pipeline {
  // do something
}

Класс Concurrent - это C # Goroutine.

Предположим, у меня есть функция, которая отправляет запрос GET в некоторый REST API.

void GetWriter()
{
    httplib.Get("medium.com/api?writer=Nalon");
}

Теперь предположим, что я не хочу ждать возврата этой функции, тогда я просто вызываю ее с помощью вызова Concurrent.Operation.

Concurrent.Operation(GetWriter);

Теперь предположим, что я действительно забочусь о возвращаемом значении, но все еще не хочу ждать этого GET.

pipeline = new Pipeline();
void GetWriter()
{
  pipeline.Send(httplib.Get("medium.com/api?writer=Nalon"));
}
Concurrent.Operation(Get);
...
// do some stuff
...
string response = pipeline.Read;

Пройдя еще один шаг, я могу сделать обе эти операции, отправку и чтение, происходящими одновременно.

void GetWriter()
{
  pipeline.Send(httplib.Get("medium.com/restful?writer=Nalon"));
}
void ReadWriter()
{
  string response = p.Read;
}
main()
{
  Concurrent.Operation(ReadWriter);
  Concurrent.Operation(GetWriter);
}

Это поведение очень похоже на то, как реализованы горутины и каналы.

Высматривать! Накладные расходы резьбы!

Несмотря на то, что мы достигли синтаксиса параллелизма, подобного Go, нам по-прежнему приходится расплачиваться за накладные расходы на потоки C #.

Но, используя только то, что у нас есть на данный момент, мы можем добиться «непрерывности» с новыми классами Параллелизм и Конвейер; все основано на общих значениях C # System.Type.

Я могу создать новый конвейер, который принимает простой тип Действие.

PipelineType<Action> pipeline = new PipelineType<Action>();
Concurrent.Operation(()=>{
    Action action = pipeline.Read;
    while (action != Pipeline.Closed)
    {
        action();
        action = pipeline.Read;
    }
});
pipeline.Send(GetWriter);

Этот конвейер считывает делегатов действий из своей очереди и запускает их в том же порядке, в котором они были отправлены.

Теперь мы устранили накладные расходы на создание нового потока, сделав это только один раз при создании экземпляра.

Это здорово!

Единственная проблема сейчас заключается в том, что, хотя эти операции действительно будут выполняться одновременно, основная Concurrent.Operation по-прежнему выполняет их синхронно.

Разве не было бы замечательно, если бы все эти операции могли выполняться асинхронно без накладных расходов и не были привязаны к какому-либо порядку?

Что ж, тогда давайте копать.

Используя тот же шаблон проектирования, мы запускаем n Concurrent.Operation ’, все считывающие из одного и того же конвейера, ожидая некоторого действия.

PipelineType<Action> pipeline = new PipelineType<Action>();
int i = 0;
while (i < n) {
    Concurrent.Operation(()=>{
        Action action = pipeline.Read;
        while (action != Pipeline.Closed)
        {
            action();
            action = pipeline.Read;
        }
    });
    
    i++;
}

Теперь у нас есть планировщик горутин на C #.

Я удачно назвал этот класс Планировщиком.

Планировщик возвращает конвейер типа Action, из которого методы отправляются в возвращаемый конвейер.

var channel = Scheduler.Pipeline();
channel.Send(GetWriter);
channel.Close();

Создание Планировщика во время выполнения позволяет пользователю выполнять несколько одновременных операций, не платя каждый раз за создание потока.

Возможно, если есть какой-то интерес, я смогу найти время, чтобы собрать некоторые данные о дельтах между ThreadPool в C # и созданным мной планировщиком.