У меня есть следующая консольная программа 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(), пользователь даже не может видеть свой ввод после первых трех строк.

Я хотел бы сделать это асинхронно, чтобы пользователь видел все свои входные данные, но я хочу сохранить поведение без изменений. Это означает, что вывод должен отправлять 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
Juan Perez 15 Апр 2020 в 05:09

1 ответ

Лучший ответ

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

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

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

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

Производитель считывает вводимые пользователем данные, пока пользователь не уведомит об остановке (пустая строка). Весь ввод отправляется в буфер без ожидания (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. Пока это не изменится, очевидно, никого не интересует мой произведенный результат.

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

Я не уверен, что вы хотели сделать со списком команд. Поэтому сделаю потребителя немного интереснее. В этом примере Consumer имеет два выхода: один со строкой «Вы ввели ...» и один с 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 аналогичен.

Свяжите все вместе:

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 отправляет полученные данные в источник commandProcessor. CommandProcessor обрабатывает ввод и отправляет результат источнику youTypedConsumer и источнику commandTimeConsumer.

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

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);

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

-1
Harald Coppoolse 16 Апр 2020 в 06:10