В 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 # и созданным мной планировщиком.