FileStream.ReadAsync блокирует интерфейс пользователя, если useAsync имеет значение true, но не блокирует интерфейс пользователя, если он равен false - PullRequest
1 голос
/ 06 ноября 2019

Я читал о параметре useAsync в этом конструкторе FileStream:

FileStream(String, FileMode, FileAccess, FileShare, Int32, Boolean)

Я пытался использовать метод FileStream.ReadAsync() вПриложение Winforms, например:

byte[] data;
FileStream fs;
public Form1()
{
    InitializeComponent();
    fs = new FileStream(@"C:\Users\iP\Documents\Visual Studio 2015\Projects\ConsoleApplication32\ConsoleApplication32\bin\Debug\hello.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 4096);
     data = new byte[(int)fs.Length];
}

private async void button1_Click(object sender, EventArgs e)
{
    await change();
}

async Task change()
{
    textBox1.Text = "byte array made";
    await fs.ReadAsync(data, 0, data.Length);
    textBox1.Text = "finished";
}

С учетом вышеизложенного значение, установленное для свойства textBox1.Text как до, так и после вызова ReadAsync(), отображается в форме. Но если я добавлю useAsync: true к вызову конструктора FileStream, текстовое поле покажет только "Закончено" . Текст "byte array made" никогда не отображается.

Длина файла составляет 1 ГБ.

Я ожидаю, что при включенном асинхронном вводе-выводе *Метод 1026 * завершится асинхронно, что позволит потоку пользовательского интерфейса обновить текстовое поле перед завершением операции ввода / вывода. И наоборот, когда асинхронный ввод-вывод не включен, я ожидаю, что метод ReadAsync() завершится синхронно, блокируя поток пользовательского интерфейса и не позволяя обновлять текстовое поле до завершения операции ввода-вывода.

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

Почему это так?

1 Ответ

1 голос
/ 06 ноября 2019

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

Другими словами, при работе с Windows,разрешенные «асинхронные» операции (т. е. «перекрывающийся ввод-вывод») - это способ сообщить Windows, что ваш код способен работать с асинхронными результатами. Он не обещает асинхронные результаты, это просто означает, что если Windows решит, что операция должна быть завершена асинхронно, она может положиться на ваш код, чтобы справиться с этим изящно. В противном случае он будет скрывать любое асинхронное поведение от вашего кода.

В данном примере весь контент вашего файла (очевидно ... это было в моих тестах) доступен в кеше файловой системы. Кэшированные данные читаются синхронно (см. Асинхронный дисковый ввод-вывод отображается как синхронный в Windows ), и, следовательно, ваша предположительно «асинхронная» операция завершается синхронно.

Когда выпередав useAsync: false конструктору FileStream, вы указываете объекту FileStream работать без перекрывающихся операций ввода-вывода. Вопреки тому, что вы думаете - вы говорите, что все операции должны выполняться синхронно, - это не так. Вы просто отключаете базовое асинхронное поведение в операционной системе. Поэтому, когда вы вызываете асинхронный метод, такой как BeginRead() или ReadAsync() (первый, по сути, просто вызывает второй), объект FileStream по-прежнему обеспечивает асинхронное поведение. Но вместо этого он делает это, используя рабочий поток из пула потоков, который, в свою очередь, читает из файла синхронно.

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

Обратите внимание, что даже с useAsync: true в конструкторе есть по крайней мере несколько способов, которые вы все равно увидите ожидаемое асинхронное поведение, оба из которых включают в себя отсутствие файла в кэше. Первое очевидно: протестируйте код, не прочитав файл ни разу с момента последней загрузки. Второе не так очевидно. Оказывается, что в дополнение к определенным значениям для FileOptions есть еще одно значение (и только еще одно значение), которое разрешено в флагах: 0x20000000. Это соответствует флагу нативной функции CreateFile() с именем FILE_FLAG_NO_BUFFERING.

Если вы используете этот флаг вместе со значением FileOptions.Asynchronous, вы обнаружите, что ReadAsync() фактически завершится асинхронно.

Будьте осторожны, хотя: это дорого. Операции кэшированного ввода-вывода обычно намного быстрее, чем некэшированные. В зависимости от вашего сценария отключение кэширования может значительно снизить общую производительность. Аналогично отключение асинхронного ввода-вывода. Разрешить Windows использовать перекрывающийся ввод-вывод - это, как правило, хорошая идея, и она улучшит производительность.

Если у вас есть проблемы с тем, что пользовательский интерфейс перестает отвечать на запросы из-за синхронного завершения операций ввода-вывода, выполняемых синхронно, вероятно, лучше переместить этот ввод-вывод в рабочий поток, но все равно передавать useAsync: true при создании FileStream объектов. Вы будете нести издержки рабочего потока, но для любых значительно более длинных операций ввода-вывода это будет несущественным по сравнению с улучшением производительности, полученным благодаря разрешению кэшированных операций ввода-вывода с перекрытием.

Для чего это стоит, поскольку у меня не было файла объемом 1 ГБ для тестирования, и потому что я хотел немного больше контролировать информацию о тестировании и состоянии, я написал тестовую программу с нуля. Приведенный ниже код выполняет следующие действия:

  • Создает файл, если он еще не существует
  • Когда программа закрывается, удаляет файл, если он был создан в каталоге temp
  • Отображает текущее время дня, что дает некоторую обратную связь относительно того, заблокирован ли пользовательский интерфейс.
  • Отображает некоторый статус о пуле потоков, который позволяет увидеть, когда рабочий поток становится активным. (т. е. для обработки файловых операций ввода-вывода)
  • Имеет пару флажков, позволяющих изменять режим работы без перекомпиляции кода

Полезные вещи для наблюдения:

  • Когда оба флажка сняты, ввод-вывод всегда выполняется асинхронно, и отображается сообщение с количеством считываемых байтов. Обратите внимание, что число активных рабочих потоков в этом случае увеличивается.
  • Когда отмечен useAsync, но disable cache не отмечен, ввод-вывод почти всегда выполняется синхронно, с текстом состояния не обновляется
  • Если установлены оба флажка, ввод-вывод всегда выполняется асинхронно;нет очевидного способа отличить это от операции, выполняемой асинхронно в пуле потоков, но он отличается тем, что используется перекрывающийся ввод-вывод, а не не перекрывающийся ввод-вывод в рабочем потоке. ПРИМЕЧАНИЕ. Как правило, если вы тестируете с отключенным кэшированием, то даже если вы снова включите кэширование (снимите флажок «отключить кэширование»), следующий тест будет по-прежнему выполняться асинхронно, поскольку кэш еще не восстановлен.

Вот пример кода (сначала код пользователя, затем код, сгенерированный дизайнером в конце):

public partial class Form1 : Form
{
    //private readonly string _tempFileName = Path.GetTempFileName();
    private readonly string _tempFileName = "temp.bin";
    private const long _tempFileSize = 1024 * 1024 * 1024; // 1GB

    public Form1()
    {
        InitializeComponent();
    }

    protected override void OnFormClosed(FormClosedEventArgs e)
    {
        base.OnFormClosed(e);
        if (Path.GetDirectoryName(_tempFileName).Equals(Path.GetTempPath(), StringComparison.OrdinalIgnoreCase))
        {
            File.Delete(_tempFileName);
        }
    }

    private void _InitTempFile(IProgress<long> progress)
    {
        Random random = new Random();
        byte[] buffer = new byte[4096];
        long bytesToWrite = _tempFileSize;

        using (Stream stream = File.OpenWrite(_tempFileName))
        {
            while (bytesToWrite > 0)
            {
                int writeByteCount = (int)Math.Min(buffer.Length, bytesToWrite);

                random.NextBytes(buffer);
                stream.Write(buffer, 0, writeByteCount);
                bytesToWrite -= writeByteCount;
                progress.Report(_tempFileSize - bytesToWrite);
            }
        }
    }

    private void timer1_Tick(object sender, EventArgs e)
    {
        int workerThreadCount, iocpThreadCount;
        int workerMax, iocpMax, workerMin, iocpMin;

        ThreadPool.GetAvailableThreads(out workerThreadCount, out iocpThreadCount);
        ThreadPool.GetMaxThreads(out workerMax, out iocpMax);
        ThreadPool.GetMinThreads(out workerMin, out iocpMin);
        label3.Text = $"IOCP: active - {workerMax - workerThreadCount}, {iocpMax - iocpThreadCount}; min - {workerMin}, {iocpMin}";
        label1.Text = DateTime.Now.ToString("hh:MM:ss");
    }

    private async void Form1_Load(object sender, EventArgs e)
    {
        if (!File.Exists(_tempFileName) || new FileInfo(_tempFileName).Length == 0)
        {
            IProgress<long> progress = new Progress<long>(cb => progressBar1.Value = (int)(cb * 100 / _tempFileSize));

            await Task.Run(() => _InitTempFile(progress));
        }

        button1.Enabled = true;
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        label2.Text = "Status:";
        label2.Update();

        // 0x20000000 is the only non-named value allowed
        FileOptions options = checkBox1.Checked ?
            FileOptions.Asynchronous | (checkBox2.Checked ? (FileOptions)0x20000000 : FileOptions.None) :
            FileOptions.None;

        using (Stream stream = new FileStream(_tempFileName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, options /* useAsync: true */))
        {
            await _ReadAsync(stream, (int)stream.Length);
        }
        label2.Text = "Status: done reading file";
    }

    private async Task _ReadAsync(Stream stream, int bufferSize)
    {
        byte[] data = new byte[bufferSize];

        label2.Text = $"Status: reading {data.Length} bytes from file";

        while (await stream.ReadAsync(data, 0, data.Length) > 0)
        {
            // empty loop
        }
    }

    private void checkBox1_CheckedChanged(object sender, EventArgs e)
    {
        checkBox2.Enabled = checkBox1.Checked;
    }
}

#region Windows Form Designer generated code

/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
    this.components = new System.ComponentModel.Container();
    this.button1 = new System.Windows.Forms.Button();
    this.progressBar1 = new System.Windows.Forms.ProgressBar();
    this.label1 = new System.Windows.Forms.Label();
    this.timer1 = new System.Windows.Forms.Timer(this.components);
    this.label2 = new System.Windows.Forms.Label();
    this.label3 = new System.Windows.Forms.Label();
    this.checkBox1 = new System.Windows.Forms.CheckBox();
    this.checkBox2 = new System.Windows.Forms.CheckBox();
    this.SuspendLayout();
    // 
    // button1
    // 
    this.button1.Enabled = false;
    this.button1.Location = new System.Drawing.Point(13, 13);
    this.button1.Name = "button1";
    this.button1.Size = new System.Drawing.Size(162, 62);
    this.button1.TabIndex = 0;
    this.button1.Text = "button1";
    this.button1.UseVisualStyleBackColor = true;
    this.button1.Click += new System.EventHandler(this.button1_Click);
    // 
    // progressBar1
    // 
    this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) 
    | System.Windows.Forms.AnchorStyles.Right)));
    this.progressBar1.Location = new System.Drawing.Point(13, 390);
    this.progressBar1.Name = "progressBar1";
    this.progressBar1.Size = new System.Drawing.Size(775, 48);
    this.progressBar1.TabIndex = 1;
    // 
    // label1
    // 
    this.label1.AutoSize = true;
    this.label1.Location = new System.Drawing.Point(13, 352);
    this.label1.Name = "label1";
    this.label1.Size = new System.Drawing.Size(93, 32);
    this.label1.TabIndex = 2;
    this.label1.Text = "label1";
    // 
    // timer1
    // 
    this.timer1.Enabled = true;
    this.timer1.Interval = 250;
    this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
    // 
    // label2
    // 
    this.label2.AutoSize = true;
    this.label2.Location = new System.Drawing.Point(13, 317);
    this.label2.Name = "label2";
    this.label2.Size = new System.Drawing.Size(111, 32);
    this.label2.TabIndex = 3;
    this.label2.Text = "Status: ";
    // 
    // label3
    // 
    this.label3.AutoSize = true;
    this.label3.Location = new System.Drawing.Point(13, 282);
    this.label3.Name = "label3";
    this.label3.Size = new System.Drawing.Size(93, 32);
    this.label3.TabIndex = 4;
    this.label3.Text = "label3";
    // 
    // checkBox1
    // 
    this.checkBox1.AutoSize = true;
    this.checkBox1.Location = new System.Drawing.Point(13, 82);
    this.checkBox1.Name = "checkBox1";
    this.checkBox1.Size = new System.Drawing.Size(176, 36);
    this.checkBox1.TabIndex = 5;
    this.checkBox1.Text = "useAsync";
    this.checkBox1.UseVisualStyleBackColor = true;
    this.checkBox1.CheckedChanged += new System.EventHandler(this.checkBox1_CheckedChanged);
    // 
    // checkBox2
    // 
    this.checkBox2.AutoSize = true;
    this.checkBox2.Enabled = false;
    this.checkBox2.Location = new System.Drawing.Point(13, 125);
    this.checkBox2.Name = "checkBox2";
    this.checkBox2.Size = new System.Drawing.Size(228, 36);
    this.checkBox2.TabIndex = 6;
    this.checkBox2.Text = "disable cache";
    this.checkBox2.UseVisualStyleBackColor = true;
    // 
    // Form1
    // 
    this.AutoScaleDimensions = new System.Drawing.SizeF(16F, 31F);
    this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
    this.ClientSize = new System.Drawing.Size(800, 450);
    this.Controls.Add(this.checkBox2);
    this.Controls.Add(this.checkBox1);
    this.Controls.Add(this.label3);
    this.Controls.Add(this.label2);
    this.Controls.Add(this.label1);
    this.Controls.Add(this.progressBar1);
    this.Controls.Add(this.button1);
    this.Name = "Form1";
    this.Text = "Form1";
    this.Load += new System.EventHandler(this.Form1_Load);
    this.ResumeLayout(false);
    this.PerformLayout();

}

#endregion

private System.Windows.Forms.Button button1;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Timer timer1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.CheckBox checkBox1;
private System.Windows.Forms.CheckBox checkBox2;

Чтобы ответить на последующие вопросы, опубликованные в виде комментариев:

  1. В чем различия между useAsync и FileOption.Asyncronous

Нет. Перегрузка с параметром bool просто для удобства. Он делает то же самое.

когда я должен использовать Async: false с асинхронными методами и useAsync: true?

Если вы хотите увеличить производительность ввода-вывода с перекрытием, вы должны указать useAsync: true.

"Если вы используете этот флаг вместе со значением FileOptions.Asynchronous, вы обнаружите, что ReadAsync () фактически завершится асинхронно."финиш

Это не совсем вопрос, но…

Похоже, вы оспариваете мое утверждение о том, что включение FILE_FLAG_NO_BUFFERING в параметр FileOptions приведет к ReadAsync() завершить асинхронно (что можно было бы сделать, отключив использование кэша файловой системы).

Я не могу сказать вам, что происходит на вашем компьютере. Как правило, я ожидаю, что он будет таким же, как на моем компьютере, но нет никаких гарантий. Что я могу сказать вам, так это то, что отключение кэширования с помощью FILE_FLAG_NO_BUFFERING на 100% надежно в моих тестах, заставляя ReadAsync() завершаться асинхронно.

Важно отметить, чтофактическое значение , означающее флага, не является «причиной ReadAsync() асинхронного завершения» . Это просто побочный эффект , который я наблюдаю при использовании этого флага. Кэширование - не единственное условие, которое заставляет ReadAsync() завершаться синхронно, поэтому вполне возможно, что даже при использовании этого флага вы все равно увидите, что ReadAsync() завершится синхронно.

Несмотря на это, я думаю, что это всене беспокойсяЯ не думаю, что использование FILE_FLAG_NO_BUFFERING на самом деле хорошая идея. Я включил, что в этом обсуждении только 1130 * только 1131 *, чтобы выяснить причину синхронного завершения ReadAsync(). Я не предполагаю, что в общем случае стоит использовать этот флаг.

На самом деле вы, как правило, предпочитаете повышенную производительность перекрывающихся операций ввода-вывода, поэтому следует использовать useAsync: true без отключения кэширования (поскольку отключение кэширования приведет к снижению производительности). Но вам следует объединить это с и , выполняющими ввод-вывод в рабочем потоке (например, с Task.Run()), по крайней мере, когда вы имеете дело с очень большими файлами, чтобы не блокировать пользовательский интерфейс.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...