Как добиться того же результата, что и Thread.Sleep (), но с asyn c, и сохранить то же поведение? - PullRequest
0 голосов
/ 15 апреля 2020

У меня есть следующая C# консольная программа:

using System;
using System.Collections.Generic;
using System.Threading;

namespace Flood
{
    class Program
    {
        private static List<DateTime> CommandsList = new List<DateTime>();
        private static double SecondsPassed = 0;

        public static void Main()
        {
            while (true)
            {
                // Invoke Control method
                Control(Console.ReadLine());
            }
        }

        private static void Control(string command)
        {
            // Calculate total seconds passed since first entry of the list until now
            if (CommandsList.Count != 0)
                SecondsPassed = DateTime.Now.Subtract(CommandsList[0]).TotalSeconds;

            // If less than 10 seconds passed
            if (SecondsPassed < 10)
            {
                // If list contains more than 2 entries
                if (CommandsList.Count >= 2)
                {
                    // Wait for the amount of time left to complete 10 seconds, then clear the list
                    Thread.Sleep((10 - Convert.ToInt32(SecondsPassed)) * 1000);
                    CommandsList.Clear();
                }
            }

            // If more than 10 seconds passed, clear the list
            else
                CommandsList.Clear();

            // Add current time to list
            CommandsList.Add(DateTime.Now);

            // Repeat the command to the user
            Console.WriteLine("You typed: " + command);
        }
    }
}

Она делает именно то, что я ожидаю. По сути, если пользователь быстро вставляет 6 строк, номера от 1 до 6, он выводит следующее:

1
You typed: 1
2
You typed: 2
3
[Wait ~10 seconds]
You typed: 3
4
You typed: 4
5
[Wait ~10 seconds]
You typed: 5
6
You typed: 6

Однако есть очевидная проблема: поскольку я использую Thread.Sleep(), пользователь не может даже смотрите их ввод после первых 3 строк.

Я хотел бы сделать это асинхронно, чтобы пользователь видел все свои входные данные, но я хочу сохранить поведение неизменным. Это означает, что выходные данные должны отправлять 2 записи за раз, каждые ~ 10 секунд.

Я попытался использовать await Task.Delay() и преобразовать метод в async, но даже если пользователь теперь видит их ввод, консоль выводит это:

1
You typed: 1
2
You typed: 2
3
4
5
6
[Wait ~10 seconds]
You typed: 3
You typed: 6
You typed: 4
You typed: 5

Таким образом, он ждет ~ 10 секунд, а затем записывает все входы сразу. Я хотел бы подождать ~ 10 секунд между каждыми 2 записями.

Есть идеи для этого?

1 Ответ

0 голосов
/ 15 апреля 2020

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

Для этого вы ' Я буду использовать пакет nuget Microsoft TPL Dataflow

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

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

Источник читает пользователя ввод, пока пользователь не уведомит об остановке (пустая строка). Все входные данные отправляются в буфер без ожидания (Post). Источник немедленно прочитает следующую строку.

class UserInputProducer
{
    // Reads user input until user enters empty line
    // User input is sent to output buffer
    public async Task ProduceAsync()
    {
         string userInput = Console.ReadLine();
         while (!String.IsNullOrEmpty(userInput))
         {
             // send the user input to the output buffer
             this.producedUserInput.Post(userInput);
             userInput = Console.ReadLine();
         }

         // no more inpu expected:
         this.ProducedUserInput.Complete();
    }

    public ITargetBlock<string> ProducedUserInput {get; set;} = DataflowBlock<string>.NullTarget;
}

ProducedUserInput инициализируется с помощью NullTarget. Пока это не изменилось, то, очевидно, никто не заинтересован в моем произведенном выводе.

Потребитель будет ожидать входные строки и обрабатывать их.

Я не уверен, что вы хотели сделать с помощью списка команд. Поэтому я сделаю потребителя немного интереснее. Для этого примера у Потребителя есть два выхода: один со строкой «Вы ввели ...» и один с DateTimes, который вы использовали для добавления в CommandsList. Фактически, этот Потребитель также является Производителем.

class CommandProcessor
{
    public IDataFlowBlock<string> Source {get; } = new BufferBlock<string>();

    public ITargetBlock<string> YouTypedTarget<string> {get; set;} = DataFlowBlock.NullTarget<string>;
    public ITargetBlock<DateTime> CommandTimeTarget {get; set;} = DataFlowBlock.NullTarget<DateTime>;

    public async Task ProcessInput()
    {
        // wait for source output available:
        while (await this.Source.OutputAvailableAsync())
        {
             // fetch the input and the time that this input was received;
             string dataToProcess = await this.Source.ReceiveAsync();
             DateTime time = DateTime.UtcNow;

             // process the output to the two Destinations
             this.YouTypedTarget.Post("You typed "+ dataToProcess);
             this.CommandTimeTarget.Post(time);
        }

        // if here: no data to produce anymore
        this.YouTypedTarget.Complete();
        this.CommandTimeTarget.Complete();
    }
}

Вам необходимо два Потребителя: один для вывода YouTyped и один для CommandTime. Я дам один:

class YouTypedConsumer
{
    public IDataFlowBlock<string> Source {get; } = new BufferBlock<string>();

    public async Task ConsumeAsync()
    {
        // wait for source output available:
        while (await this.Source.OutputAvailableAsync())
        {
             // fetch the input and display it on the console
             string txtToDisplay= await this.Source.ReceiveAsync();
             Console.WriteLine(txtToDisplay);

             // this Consumer does not produce anything
        }
    }
}

CommandTimeConsumer похож.

T ie все вместе:

var youTypedConsumer = new YouTypedConsumer();
var conmmandTimeConsumer = new CommandTimeConsumer();

// youTypedTarget of the command processor is input youTypedConsumer
var commandProcessor = new CommandProcessor
{
    YouTypedTarget = youTypedConsumer.Source,
    CommandTimeTarget = commandTimeConsumer.Source,
}

var userInputProducer = new UserInputProducer
{
    ProducedUserInput = commandProcessor.Source,
};

Таким образом, userInputProducer отправляет полученные данные Исходнику команды Processor. CommandProcessor обрабатывает ввод и отправляет результат источнику youTypedConsumer и источнику commandTimeConsumer.

Теперь запустите все asyn c и дождитесь завершения всех задач:

var tasks = new Task[]
{
    youTypedConsumer.ConsumeAsync(),
    commandTimeConsumer.ConsumeAsync(),

    commandProcessor.ProcessAsync(),
    userInputProducer.ProduceAsync(),
};

// await until all tasks are finished:
await Task.WhenAll(tasks);

// or if this procedure is not async:
Task.WaitAll(tasks);

Приятно то, что у каждого блока есть небольшая задача. Легко понять задачу. Легко использовать для других конфигураций, например, для модульного тестирования. Нет проблем, если кто-то из производителей или потребителей иногда работает немного медленнее или быстрее, чем один блок до или после него. И это полностью асин c.

...