Реализуем кооперативную многозадачность на C#


    Когда речь заходит о многозадачности в .Net, то в подавляющем большинстве случаев предполагается вытесняющая многозадачность на основе потоков операционной системы. Но в этой статье речь пойдёт о реализации кооперативной многозадачности, с помощью которой можно создать видимость одновременной работы нескольких методов, используя всего один единственный поток.


    Вот наша простенькая заготовка:


    static void Main()
    {
        DoWork("A", 4);
        DoWork("B", 3);
        DoWork("C", 2);
        DoWork("D", 1);
    }
    
    static void DoWork(string name, int num)
    {
        for (int i = 1; i <= num; i++)
        {
            Console.WriteLine($"Work {name}: {i}");
        }
        Console.WriteLine($"Work {name} is completed");
    }

    Статический метод вызывается последовательно 4 раза и выводит символы A, B, C, D заданное количество раз. Символы, очевидно, также выводятся последовательно, и наша задача здесь добиться того, что бы они выводились попеременно, но при этом сильно не меняя исходный код и не используя дополнительные потоки.


    Заголовок этой статьи как бы намекает, что тут нужно реализовать кооперативную многозадачность, в которой исполняемый код время от времени сообщает, что: “Я пока работу приостанавливаю, и кто-нибудь еще может воспользоваться текущем потоком, чтобы сделать свои дела, но я надеюсь, что рано или поздно, мне это поток вернут обратно, ведь у меня еще куча дел”.


    Вопрос: "Какая конструкция C# позволяет прервать работу метода на какое-то заранее неизвестное время?" Правильно! await! Давайте сделаем наш метод асинхронным и добавим await после вывода в консоль очередного символа:


    static async ValueTask DoWork(string name, int num)
    {
        for (int i = 1; i <= num; i++)
        {
            Console.WriteLine($"Work {name}: {i}");
            await /*Something*/
        }
        Console.WriteLine($"Work {name} is completed");
    }

    Этот await обозначает, что метод решил прерваться, чтобы другие вызовы тоже смогли вывести свои символы. Но что именно этот метод будет await-ить? Task.Delay(), Task.Yield() не подходят, так как они подразумевают переключение на другие потоки. Тогда создадим свой класс, который можно использовать с await, и который не будет иметь ничего общего с многопоточкой. Назовем его CooperativeBroker:


    private class CooperativeBroker : ICooperativeBroker
    {
        private Action? _continuation;
    
        public void GetResult() 
            => this._continuation = null;
    
        public bool IsCompleted 
            => false;//Preventing sync completion in async method state machine
    
        public void OnCompleted(Action continuation)
        {
            this._continuation = continuation;
            this.InvokeContinuation();
        }
    
        public ICooperativeBroker GetAwaiter() 
            => this;
    
        public void InvokeContinuation() 
            => this._continuation?.Invoke();
    }

    Компилятор C# преобразует исходный код асинхронных методов в виде конечного автомата, каждое состояние которого соответствует вызову await внутри этого метода. Код перехода в следующее состояние передается в виде делегата continuation в метод OnCompleted. В реальной жизни предполагается, что continuation будет вызван, когда будет завершена асинхронная операция, но в нашем случае никаких асинхронных операций нет и для работы программы надо бы было вызвать это continuation немедленно, но тогда методы опять будут работать последовательно, а мы этого не хотим. Лучше сохраним этот делегат на будущее и дадим поработать другим вызовам. Чтобы было где хранить делегаты давайте добавим класс CooperativeContext:


    private class CooperativeBroker
    {
        private readonly CooperativeContext _cooperativeContext;
    
        private Action? _continuation;
    
        public CooperativeBroker(CooperativeContext cooperativeContext)
            => this._cooperativeContext = cooperativeContext;
    
        ...
    
        public void OnCompleted(Action continuation)
        {
            this._continuation = continuation;
            this._cooperativeContext.OnCompleted(this);
        }
    
    }
    
    public class CooperativeContext
    {
        private readonly List<CooperativeBroker> _brokers = 
            new List<CooperativeBroker>();
    
        void OnCompleted(CooperativeBroker broker)
        {
            ...
        }
    }

    где метод OnCompleted собственно и будет отвечать за поочередный вызов методов:


    private void OnCompleted(CooperativeBroker broker)
    {
        //Пропускает вызовы делегатов пока все брокеры не добавлены.
        if (this._targetBrokersCount == this._brokers.Count)
        {
            var nextIndex = this._brokers.IndexOf(broker) + 1;
            if (nextIndex == this._brokers.Count)
            {
                nextIndex = 0;
            }
    
            this._brokers[nextIndex].InvokeContinuation();
        }
    }

    Обратите внимание на первое условие – делегаты не вызываются до тех пор, пока все брокеры для всех кооперативных вызовов не будут добавлены в контекст (_targetBrokersCount — количество кооперативных вызовов). Если начать вызывать их сразу, то вызовы опять пойдут последовательно, так как наш контекст не сможет получить "продолжение" всех вызовов сразу.


    Так как нам нужно заранее знать общее количество кооперативных вызовов, то перепишем эти вызовы следующим образом:


    static void Main()
    {
        CooperativeContext.Run(
            b => DoWork(b, "A", 4),
            b => DoWork(b, "B", 3),
            b => DoWork(b, "C", 2),
            b => DoWork(b, "D", 1)
        );
    }
    
    static async ValueTask DoWork(CooperativeBroker broker, string name, int num, bool extraWork = false)
    {
        for (int i = 1; i <= num; i++)
        {
            Console.WriteLine($"Work {name}: {i}, Thread: {Thread.CurrentThread.ManagedThreadId}");
            await broker;
        }
    
        Console.WriteLine($"Work {name} is completed, Thread: {Thread.CurrentThread.ManagedThreadId}");
    }
    
    public class CooperativeContext
    {
        public static void Run(params Func<CooperativeBroker, ValueTask>[] tasks)
        {
            CooperativeContext context = new CooperativeContext(tasks.Length);
            foreach (var task in tasks)
            {
                task(context.CreateBroker());
            }
    
            ...
        }
    
        ...
    
        private int _targetBrokersCount;
    
        private CooperativeContext(int maxCooperation)
        {
            this._threadId = Thread.CurrentThread.ManagedThreadId;
            this._targetBrokersCount = maxCooperation;
        }
    
        ...
    }

    Вроде бы можно уже и запускать, чтобы проверить как это работает, но осталась еще одна маленькая деталь – если один из вызовов завершается раньше других, то он перестает вызывать метод OnCompleted и вся наша цепочка прерывается. Что бы исправить эту ситуацию форсируем вызов оставшихся "продолжений", и заодно удалим ненужный уже брокер:


    public class CooperativeContext
    {
        public static void Run(params Func<ICooperativeBroker, ValueTask>[] tasks)
        {
            CooperativeContext context = new CooperativeContext(tasks.Length);
            foreach (var task in tasks)
            {
                task(context.CreateBroker());
            }
    
            // Программа приходит сюда когда один из методов завершен, но надо
            // закончить и остальные
            while (context._brokers.Count > 0)
            {
                context.ReleaseFirstFinishedBrokerAndInvokeNext();
            }
        }
    
        ...
        private void ReleaseFirstFinishedBrokerAndInvokeNext()
        {
            // IsNoAction означает что асинхронный метод завершен
            var completedBroker = this._brokers.Find(i => i.IsNoAction)!;
    
            var index = this._brokers.IndexOf(completedBroker);
            this._brokers.RemoveAt(index);
            this._targetBrokersCount--;
    
            if (index == this._brokers.Count)
            {
                index = 0;
            }
    
            if (this._brokers.Count > 0)
            {
                this._brokers[index].InvokeContinuation();
            }
        }    
    }
    
    private class CooperativeBroker : ICooperativeBroker
    {
        ...
        public bool IsNoAction
            => this._continuation == null;
        ...
    }

    Вот теперь можно и запускать (чуть усложним нам задачу введя дополнительную работу):


    static void Main()
    {
        CooperativeContext.Run(
            b => DoWork(b, "A", 4),
            b => DoWork(b, "B", 3, extraWork: true),
            b => DoWork(b, "C", 2),
            b => DoWork(b, "D", 1)
        );
    }
    
    static async ValueTask DoWork(
        ICooperativeBroker broker, 
        string name, 
        int num, 
        bool extraWork = false)
    {
        for (int i = 1; i <= num; i++)
        {
            Console.WriteLine(
                   $"Work {name}: {i}, Thread: {Thread.CurrentThread.ManagedThreadId}");
            await broker;
            if (extraWork)
            {
                Console.WriteLine(
                       $"Work {name}: {i} (Extra), Thread: {Thread.CurrentThread.ManagedThreadId}");
                await broker;
            }
        }
    
        Console.WriteLine(
               $"Work {name} is completed, Thread: {Thread.CurrentThread.ManagedThreadId}");
    }

    Результат:


    Work A: 1, Thread: 1
    Work B: 1, Thread: 1
    Work C: 1, Thread: 1
    Work D: 1, Thread: 1
    Work A: 2, Thread: 1
    Work B: 1 (Extra), Thread: 1
    Work C: 2, Thread: 1
    Work D is completed, Thread: 1
    Work A: 3, Thread: 1
    Work B: 2, Thread: 1
    Work C is completed, Thread: 1
    Work A: 4, Thread: 1
    Work B: 2 (Extra), Thread: 1
    Work A is completed, Thread: 1
    Work B: 3, Thread: 1
    Work B: 3 (Extra), Thread: 1
    Work B is completed, Thread: 1

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




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


    Исходный код вы можете найти на github.


    [Update] Посмотрите комментарии от DistortNeo, он предлагает более практичный вариант решения этой задачи.

    Комментарии 38

      +4

      Вижу следующую проблему в реализации: всё резко перестанет работать, если управление перейдёт в другой поток, например, если вы сделаете await Task.Delay(10).


      Самое правильное решение, на мой взгляд — это написать кастомный однопоточный планировщик (SynchronizationContext). Занимает около 30 строчек. После чего можно спокойно использовать Task.Yield, а не изобретать велосипед.

        0

        Работать не перестанет — в статье я про это не стал упоминать, но в коде есть проверка этой ситуации.

          +1

          Я бы не стал об этом писать, если бы сам не попробовал. У меня получается в итоге так:


          Work A: 1, Thread: 1
          Work B: 1, Thread: 1
          Work C: 1, Thread: 1
          Work D: 1, Thread: 1
            0

            Надо добавить в конец Thread.Sleep(...) поскольку ваш Delay никто не ждет и программа завершается раньше.
            У меня вышло так:


            Заголовок спойлера
            Work A: 1, Thread: 1
            Work B: 1, Thread: 1
            Work C: 1, Thread: 1
            Work D: 1, Thread: 1
            Work D is completed, Thread: 4
            Work B: 1 (Extra), Thread: 5
            Work C: 2, Thread: 7
            Work A: 2, Thread: 6
            Work B: 2, Thread: 5
            Work A: 3, Thread: 4
            Work B: 2 (Extra), Thread: 10
            Work B: 3, Thread: 10
            Work C is completed, Thread: 11
            Work A: 4, Thread: 11
            Work B: 3 (Extra), Thread: 10
            Work B is completed, Thread: 10
            Work A is completed, Thread: 10
              +1

              А надо бы ждать, а не придумывать костыль в виде Thread.Sleep. Короче, вот по-быстрому написал на коленке:


              CooperativeContext.cs
              using System;
              using System.Collections.Concurrent;
              using System.Threading;
              using System.Threading.Tasks;
              
              namespace CooperativeMultitasking
              {
                  public sealed class CooperativeContext : SynchronizationContext
                  {
                      ConcurrentQueue<Action> queue = new ConcurrentQueue<Action>();
                      volatile int taskCount;
                      AutoResetEvent evt = new AutoResetEvent(true);
              
                      public void Post(Action action)
                      {
                          queue.Enqueue(action);
              
                          if (!ReferenceEquals(Current, this))
                              evt.Set();            
                      }
              
                      public void PostTask(Func<Task> func)
                      {
                          Interlocked.Increment(ref taskCount);
              
                          Post(async () =>
                          {
                              await func();
              
                              Interlocked.Decrement(ref taskCount);
                          });
                      }
              
                      public override void Post(SendOrPostCallback d, object? state)
                      {
                          Post(() => d(state));
                      }
              
                      public void Run()
                      {
                          var oldContext = Current;
              
                          try
                          {
                              SetSynchronizationContext(this);
              
                              while (true)
                              {
                                  if (queue.TryDequeue(out var action))
                                  {
                                      action();
                                  }
                                  else if (taskCount == 0)
                                  {
                                      break;
                                  }
                                  else
                                  {
                                      evt.WaitOne();
                                  }
                              }
                          }
                          finally
                          {
                              SetSynchronizationContext(oldContext);
                          }
                      }
                  }
              }

              Program.cs
              using System;
              using System.Threading;
              using System.Threading.Tasks;
              
              namespace CooperativeMultitasking
              {
                  class Program
                  {
                      static void Main()
                      {
                          var context = new CooperativeContext();
                          context.PostTask(() => DoWork("A", 4));
                          context.PostTask(() => DoWork("B", 4, extraWork: true));
                          context.PostTask(() => DoWork("C", 2));
                          context.PostTask(() => DoWork("D", 1));
                          context.Run();
                      }
              
                      static async Task DoWork(string name, int delay, bool extraWork = false)
                      {
                          for (int i = 1; i <= delay; i++)
                          {
                              Console.WriteLine($"Work {name}: {i}, Thread: {Thread.CurrentThread.ManagedThreadId}");
              
                              await Task.Delay(10);
                              await Task.Yield();
              
                              if (extraWork)
                              {
                                  Console.WriteLine($"Work {name}: {i} (Extra), Thread: {Thread.CurrentThread.ManagedThreadId}");
                                  await Task.Yield();
                              }
                          }
              
                          Console.WriteLine($"Work {name} is completed, Thread: {Thread.CurrentThread.ManagedThreadId}");
                      }
                  }
              }

              Результат
              Work A: 1, Thread: 1
              Work B: 1, Thread: 1
              Work C: 1, Thread: 1
              Work D: 1, Thread: 1
              Work B: 1 (Extra), Thread: 1
              Work B: 2, Thread: 1
              Work A: 2, Thread: 1
              Work D is completed, Thread: 1
              Work C: 2, Thread: 1
              Work B: 2 (Extra), Thread: 1
              Work B: 3, Thread: 1
              Work A: 3, Thread: 1
              Work B: 3 (Extra), Thread: 1
              Work C is completed, Thread: 1
              Work B: 4, Thread: 1
              Work A: 4, Thread: 1
              Work B: 4 (Extra), Thread: 1
              Work B is completed, Thread: 1
              Work A is completed, Thread: 1
                0

                AutoReset WaitOne это обращение к ядру и перформанс тут сильно проседает. Мое решение вообще никак не связанно с многопоточкой и async/await используется просто как синтаксический сахар

                  +1
                  AutoReset WaitOne это обращение к ядру и перформанс тут сильно проседает

                  Этот вызов произойдёт только в одном случае: если есть незавершённые фоновые задачи. В остальных случаях дело до него не дойдёт.


                  Если же это вам так мозолит глаза, тогда можете удалить весь связанный с AutoResetEvent код:


                  Будет примерно так
                  namespace CooperativeMultitasking
                  {
                      public sealed class CooperativeContext : SynchronizationContext
                      {
                          Queue<Action> queue = new Queue<Action>();
                  
                          public void Post(Action action)
                          {
                              queue.Enqueue(action);    
                          }
                  
                          public override void Post(SendOrPostCallback d, object? state)
                          {
                              Post(() => d(state));
                          }
                  
                          public void Run()
                          {
                              var oldContext = Current;
                  
                              try
                              {
                                  SetSynchronizationContext(this);
                  
                                  while (queue.Count > 0)
                                  {
                                      queue.Dequeue().Invoke();
                                  }
                              }
                              finally
                              {
                                  SetSynchronizationContext(oldContext);
                              }
                          }
                      }
                  }

                  Код становится совсем простым, не правда ли?

                    0

                    В целом да, соглашусь. Хоть это все и прибито гвоздями к таскам.

                      0
                      Хоть это все и прибито гвоздями к таскам.

                      Тоже не совсем так. SynchronizationContext к таскам вообще не имеет никакого отношения. Task.Yield, как это ни странно, тоже — вы можете вместо него свою реализацию подсунуть:


                      Пример
                      partial class Async
                      {
                          public static YieldAwaitable Yield() => new(Scheduler);
                      
                          public readonly struct YieldAwaitable
                          {
                              readonly IAsyncScheduler scheduler;
                      
                              public YieldAwaitable(IAsyncScheduler scheduler)
                              {
                                  this.scheduler = scheduler;
                              }
                      
                              public Awaiter GetAwaiter() => new(scheduler);
                      
                              public readonly struct Awaiter
                                  : INotifyCompletion
                              {
                                  readonly IAsyncScheduler scheduler;
                      
                                  public Awaiter(IAsyncScheduler scheduler)
                                  {
                                      this.scheduler = scheduler;
                                  }
                      
                                  public bool IsCompleted => false;
                      
                                  public void OnCompleted(Action continuation) => scheduler.Post(continuation);
                      
                                  public void GetResult()
                                  {
                                  }
                              }
                          }
                      }

                      Примечание: IAsyncScheduler — это обёртка над SynchronizationContext, чтобы не делать двойное преобразование Action → SendOrPostCallback → Action.


                      Прибитость гвоздями к таскам вылезает, когда вы используете синтаксический сахар async/await. Функция должна возвращать Task. Правда, потом в C# разрешили использовать и свои реализации, используя атрибут AsyncMethodBuilder: ValueTask — один из примеров (правда, под капотом там все равно Task сидит).

                        0
                        Прибитость гвоздями к таскам вылезает, когда вы используете синтаксический сахар async/await. Функция должна возвращать Task. Правда, потом в C# разрешили использовать и свои реализации, используя атрибут AsyncMethodBuilder: ValueTask — один из примеров (правда, под капотом там все равно Task сидит).

                        Эту статью я задумывал как логическое продолжение цикла про то, как можно использовать этот MetodBuilder. Вот например Монада «Maybe» через async/await в C# (без Task-oв!) или Writing “Lazy Task” Using New Features of C# 7 В данном случае Method Builder не пригодился, но акцент я хотел сделать именно на конструкциях языка C#, нежели на практической стороне вопроса.

                          0
                          Прибитость гвоздями к таскам вылезает, когда вы используете синтаксический сахар async/await. Функция должна возвращать Task

                          Вовсе нет. Она может возвращать вообще все что угодно у чего есть метод GetAwaiter. Тут аналогия с foreach в котором вовсе не обязан быть строго IEnumerable (достаточно просто наличия метода GetEnumerator).

                            0
                            Она может возвращать вообще все что угодно у чего есть метод GetAwaiter

                            Нет. await-тить можно всё, что угодно, что имеет метод GetAwaiter. А вот сама асинхронная функция должна возвращать хитрый тип, который должен следовать определённым правилам.

                              0

                              Да, на самом деле, я уже ниже написал — как по мне, так async/await тут как пришей кобыле хвост. Ну нет тут асинхронности. Асинхронность это когда мы вызвали что-то и "ждем" от него коллбека, что оно там что-то свое сделало. А тут мы пытаемся сами что-то сделать и потом ждать непонятно чего. Собственно, статья с того почти и начинается, что надо что-то await-ом подождать, а вот чего ждать вообще непонятно. И начинается какое-то шаманство, чтобы это "что-то чего ждать" искусственно сделать.


                              Я прекрасно понимаю, что async/await это КА, и кооперативка это тоже КА, но, например, регекспы это тоже КА, но никому же в голову не придет регекспы делать на async/await (хотя, уверен, что при большом желании извратиться это возможно).

                                0

                                Тем не менее, кооперативная многозадачность через async/await реализуется проще всего.


                                потом ждать непонятно чего

                                Ждать, пока не отработают другие задачи в очереди.


                                Мне, правда, тоже не очень понятно, затем ждать исключительно вычислительный код, но оставим это на совести автора.

                                  0

                                  С а/а можно, пожалуй, как-то так:


                                  Program.cs
                                  internal static class Program
                                  {
                                      private static async Task Main() => await new CooperativeLoop().Run(
                                              l => DoWork(l, "A", 4),
                                              l => DoWork(l, "B", 3),
                                              l => DoWork(l, "C", 2),
                                              l => DoWork(l, "D", 1));
                                  
                                      private static async Task DoWork(CooperativeLoop cooperativeLoop, string name, int num)
                                      {
                                          for (var i = 0; i < num;)
                                          {
                                              Console.WriteLine($"{name}: {++i}. Thread: {Thread.CurrentThread.ManagedThreadId}.");
                                              await cooperativeLoop.Yield();
                                          }
                                  
                                          Console.WriteLine($"{name}: Completed. Thread: {Thread.CurrentThread.ManagedThreadId}.");
                                      }
                                  }
                                  
                                  internal class CooperativeLoop
                                  {
                                      private Queue<(TaskCompletionSource taskCompletionSource, Task task)> _tasks = new();
                                  
                                      public Task Run(params Func<CooperativeLoop, Task>[] job)
                                      {
                                          var allTaks = Task.WhenAll(job.Select(j => j(this)));
                                  
                                          while (_tasks.TryDequeue(out var t))
                                          {
                                              t.taskCompletionSource.SetResult();
                                          }
                                  
                                          return allTaks;
                                      }
                                  
                                      public Task Yield()
                                      {
                                          TaskCompletionSource src = new();
                                          _tasks.Enqueue((taskCompletionSource: src, task: src.Task));
                                          return src.Task;
                                      }
                                  }

                                  Мне кажется, что это как-то менее извилисто, чем в статье.

                  0
                  У меня вышло так:

                  То есть у вас функции в итоге разбегаются по разным потокам? Забавно.
                  И где же в таком случае кооперативная многозадачность?

                    0

                    Изначально я вообще хотел в этом случае исключение кидать, поскольку не надо там Delay использовать, подход вообще не про это.

            +1
            В реальной жизни предполагается, что continuation будет вызван, когда будет завершена асинхронная операция, но в нашем случае никаких асинхронных операций нет

            А куда, собственно, делись все асинхронные операции?

              0

              Их там никогда и не было. Все работает синхронно.

                –1

                Ну и зачем такая "многозадачность" нужна, когда любое чтение из файла или сокета останавливает все задачи?

                  –1

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

                    +2

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


                    Я в комментарии выше привёл пример полнофункционального планировщика. С ним вы можете спокойно использовать асинхронную работу с таймерами, сокетами. При этом все задачи будут работать поочерёдно в одном потоке.

                      +1

                      Многие каждый день пользуются кооперативом даже не зная об этом. Потому что любой асинхронный вызов I/O это уже, по сути, кооператив.

              –3
              А зачем такие сложности, если в C# есть генераторы?
                +2
                yield return не предназначен для решения подобных задач.
                Если пытаться использовать IEnumerable для создания кооперативной многозадачности — получатся корутины из Unity.
                –1

                Парни, я не вникал прямо очень уж детально в код статьи, но, поправьте, если я путаю — все это уже реализовано в классе "TaskCompletionSource" ("TaskCompletionSource"). Автор изобрел просто какой-то лютый велосипед.

                  0

                  Вообще-то я глупость сказал. Но, все-таки, непонятно к чему тут вообще таски и async/await. Таски это, как бы, асинхронность, а кооперативная многозадачность непонятно каким к ней вообще боком. Вот так мне видится это вообще без тасков:


                  Program.cs
                  internal static class Program
                  {
                      private static Queue<Action> _actions = new();
                  
                      private static void Main()
                      {
                          DoWork("A", 4);
                          DoWork("B", 3);
                          DoWork("C", 2);
                          DoWork("D", 1);
                  
                          while (_actions.TryDequeue(out var action))
                          {
                              action();
                          }
                      }
                  
                      private static void DoWork(string name, int num)
                      {
                          var i = 0;
                  
                          void printName()
                          {
                              var tid = Thread.CurrentThread.ManagedThreadId;
                              Console.WriteLine($"{name}: {++i}. Thread: {tid}.");
                  
                              if (i < num)
                              {
                                  _actions.Enqueue(printName);
                              }
                              else
                              {
                                  Console.WriteLine($"{name}: Completed. Thread: {tid}.");
                              }
                          }
                  
                          _actions.Enqueue(printName);
                      }
                  }
                    0

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


                    Но, все-таки, непонятно к чему тут вообще таски и async/await

                    Тасков здесь и нет, я намерено использовал ValueTask, который хоть и связан с Тасками, но может быть легко заменен своей реализаций (через Method Builder), и тогда зависимости от System.Threading.Tasks вообще не будет.


                    async/await преобразует исходный метод в виде конечного автомата и больше он ничего не делает.

                      0

                      Ну я, как бы, намеренно попроще писал, все это точно так же можно завернуть в объекты и тогда будет не менее читаемо чем у тебя. Мой посыл просто был в том, что, по-моему кооперативная многозадачность и асинхронность это настолько разные вещи, что будь там хоть таск, хоть велюетаск, хоть просто асинк/евейт вообще без тасков — это все равно как молотком ямы копать.

                        +1

                        Я думаю, что смущение вызывает ключевое слово “async”, но никакой асинхронности здесь нет.
                        Эта конструкция была изначально введена для упрощения работы с тасками, но поскольку Таск я является, по сути, монадой, то async/await удивительным образом подошёл и для других монад, что я показывал в своих предыдущих статьях. Асинхронность — это только частный случай.

                      +1
                      Таски это, как бы, асинхронность, а кооперативная многозадачность непонятно каким к ней вообще боком.

                      Вообще-то как раз понятно: кооперативная многозадачность отличается от вытесняющей наличием в коде явной передачи управления планировщику, и оператор await как раз такой передачей управления и занимается. Так что таски как раз являются формой кооперативной многозадачности (точнее, смешанной многозадачности, но чистую кооперативную тоже можно сделать при желании).

                    +1

                    Если что-то пойдет не так, что увидим в стектрейсе? Портянку внутренних деталей реализации? Как сложно отлаживать async/await?

                      +1

                      Это синхронный однопоточный код и все исключения ловятся в нем как и в любом другом синхронном однопоточном коде. (try/catch/finally). Стектрейсы будут не самые простые, но вполне анализируемые.

                      0
                      Только начинаю изучать Task и async/await. Вроде даже что-то понял.
                      Сохраню, может пригодится.
                      Комментарии ток не удаляйте, там много всего не понятного, но весьма интересного)
                        +1

                        Я помню это ощущение из питона, когда ничего не потокобезопасно, а тебе пофиг — из-за GIL все в один поток, и можно не думать о разделении ресурсов. Ну и добавление асинхронщины ничего не ломает.
                        В шарпе же можно ограничить тредпул одним потоком, или свой шедулер как выше предложили, необязательно хакать именно async/await, но прикольно, да

                          +1

                          Вот только это автоматически ограничит производительность приложения, так что лучше всё-таки в один поток всё не засовывать.


                          когда ничего не потокобезопасно, а тебе пофиг — из-за GIL все в один поток, и можно не думать о разделении ресурсов

                          А вот это, кстати, неправда: GIL далеко не от всех проблем спасает.

                            0

                            Производительность разная бывает, от задачи зависит. А от чего gil не спасет? Ну кроме ситуаций когда из питона запускают модуль на каком-нибудь c++, который создает отдельный тред и что-нибудь шатает в общей памяти?

                          +1

                          Не совсем я понял проблему, Task.Yield необязательно переключает потоки, зависит от того есть или нет SynchronizationContext и какой TaskScheduler используется. Можно даже не писать своего, а использовать готовый SynchronizationContext из UI фреймворка, он будет все выполнять на одном треде.

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

                          Самое читаемое