ilovecomputer66 发表于 2023-5-18 08:03

朋友托我问个问题,这个C#代码,为什么会导致winForm界面卡死

他写了一个用于持续写入log的工具类。用到异步锁,目的是保证上次异步写完,后面再写,否则前面没写入,后面又调用要写入,.NET就会抛出IOException了
    public class LogAsyncWriter
    {
      private readonly SemaphoreSlim _Semaphore = new SemaphoreSlim(1, 1);
      private readonly StreamWriter _StreamWriter;

      public LogAsyncWriter(string filePath)
      {
            _StreamWriter = new StreamWriter(filePath, true, Encoding.UTF8);
      }

      public LogAsyncWriter(string filePath, Encoding encoding)
      {
            _StreamWriter = new StreamWriter(filePath, true, encoding);
      }

      public async Task WriteLogAsync(string logText, bool isAppendLine = true)
      {
            await _Semaphore.WaitAsync();
            try
            {
                if (isAppendLine == true)
                  await _StreamWriter.WriteLineAsync(logText);
                else
                  await _StreamWriter.WriteAsync(logText);
            }
            finally
            {
                _Semaphore.Release();
            }
      }

      public async Task CloseAsync()
      {
            await _Semaphore.WaitAsync();
            try
            {
                _StreamWriter.Flush();
                _StreamWriter.Close();
            }
            finally
            {
                _Semaphore.Release();
            }
      }
    }


然后他又写了一个测试代码,在Form中点击一个按钮的事件

private LogAsyncWriter _LogAsyncWriter;
private string _OnceWriteText;
private void BtnTestLogAsyncWriter_Click(object sender, EventArgs e)
{
    _LogAsyncWriter = new LogAsyncWriter(@"D:\TestLogAsyncWriter.txt");
    StringBuilder lotsOfTextSB = new StringBuilder();
    for (int i = 0; i < 2048; i++)
      lotsOfTextSB.Append("a");
    _OnceWriteText = lotsOfTextSB.ToString();

    UIUtil.AppendTextToRichTextBox(RtxConsole, $"[{DateTime.Now.ToString("HH:mm:ss")}]准备写入Log");
    _StartWriteLog();
    // 做其他事情
    UIUtil.AppendTextToRichTextBox(RtxConsole, $"[{DateTime.Now.ToString("HH:mm:ss")}]运行到后续代码");
}

private async void _StartWriteLog()
{
    // 连续3秒写入大量内容
    DateTime endTime = DateTime.Now.AddSeconds(3);
    int writeCount = 0;
    while (DateTime.Now < endTime)
    {
      await _LogAsyncWriter.WriteLogAsync(_OnceWriteText);
      writeCount++;
    }
    AppendTextToRichTextBox(RtxConsole, $"[{DateTime.Now.ToString("HH:mm:ss")}]完成3秒的写入,共写入{writeCount}次");
    await _LogAsyncWriter.CloseAsync();
    AppendTextToRichTextBox(RtxConsole, $"[{DateTime.Now.ToString("HH:mm:ss")}]完成文件关闭");
}

public static void AppendTextToRichTextBox(RichTextBox rtx, string text, bool isAppendNewLine = true)
{
    // 让富文本框获取焦点
    rtx.Focus();
    // 设置光标的位置到文本结尾
    rtx.Select(rtx.TextLength, 0);
    // 滚动到富文本框光标处
    rtx.ScrollToCaret();
    // 追加内容
    rtx.AppendText(text);
    // 换行
    if (isAppendNewLine == true)
      rtx.AppendText(Environment.NewLine);
    // 刷新UI
    rtx.Refresh();
}





但是发现,在点击按钮,3秒持续写入过程中,整个form界面都是卡死的,他不明白这是为什么。以下是它提问的原话:
_StartWriteLog是异步方法,我在button点击事件中,并没有await 这个函数,而运行看,也确实在开始时候,就已经打印出:“运行到后续代码”,说明确实没有阻塞。但是winfrom界面为什么会卡呢?

zbr878458173 发表于 2023-5-18 08:17

让我们来看一下AI的回答
这可能是由于在 WinForms 中,所有 UI 更新都需要在 UI 线程上进行。在你的 _StartWriteLog 方法中,你在执行写入操作的同时,也在更新 UI(将写入计数和完成时间添加到 RichTextBox)。虽然你的写入操作是异步的,但 UI 更新是在 UI 线程上进行的,所以在写入操作完成之前,UI 线程可能会被阻塞。

你可以尝试使用 Task.Run 来将 _StartWriteLog 的内容放在一个新的线程中运行,这样 UI 线程就不会被阻塞。然后,你可以使用 Control.Invoke 或 Control.BeginInvoke 来在 UI 线程上更新 UI。以下是一个示例:
private void BtnTestLogAsyncWriter_Click(object sender, EventArgs e)
{
    _LogAsyncWriter = new LogAsyncWriter(@"D:\TestLogAsyncWriter.txt");
    StringBuilder lotsOfTextSB = new StringBuilder();
    for (int i = 0; i < 2048; i++)
      lotsOfTextSB.Append("a");
    _OnceWriteText = lotsOfTextSB.ToString();

    UIUtil.AppendTextToRichTextBox(RtxConsole, $"[{DateTime.Now.ToString("HH:mm:ss")}]准备写入Log");
    Task.Run(() => _StartWriteLog());
    // 做其他事情
    UIUtil.AppendTextToRichTextBox(RtxConsole, $"[{DateTime.Now.ToString("HH:mm:ss")}]运行到后续代码");
}

private async void _StartWriteLog()
{
    // 连续3秒写入大量内容
    DateTime endTime = DateTime.Now.AddSeconds(3);
    int writeCount = 0;
    while (DateTime.Now < endTime)
    {
      await _LogAsyncWriter.WriteLogAsync(_OnceWriteText);
      writeCount++;
    }
    this.Invoke((Action)(() =>
    {
      AppendTextToRichTextBox(RtxConsole, $"[{DateTime.Now.ToString("HH:mm:ss")}]完成3秒的写入,共写入{writeCount}次");
    }));
    await _LogAsyncWriter.CloseAsync();
    this.Invoke((Action)(() =>
    {
      AppendTextToRichTextBox(RtxConsole, $"[{DateTime.Now.ToString("HH:mm:ss")}]完成文件关闭");
    }));
}

在这个修改后的代码中, _StartWriteLog 的内容在一个新的线程中运行,而更新 UI 的代码在 UI 线程中运行,因此 UI 不会在写入操作进行时被阻塞。

赵爽 发表于 2023-5-18 08:35

zbr878458173 发表于 2023-5-18 08:17
让我们来看一下AI的回答
这可能是由于在 WinForms 中,所有 UI 更新都需要在 UI 线程上进行。在你的 _Star ...

这真是ai干的

Likey 发表于 2023-5-18 09:05

此类问题丢给GPT,能快速解决

Broadm 发表于 2023-5-18 09:12

我认为是while循环放在UI线程导致的, 把while循环放在一个单独的线程应该可以解决问题,   while等待了三秒, 也就是阻塞了主线程3秒, 虽然内部用了await, 但是await的执行时间很短, 所以和死循环等同, 导致主线程卡顿3秒
while (DateTime.Now < endTime)
    {
      await _LogAsyncWriter.WriteLogAsync(_OnceWriteText);
      writeCount++;
    }

pjy612 发表于 2023-5-18 09:19

我记得如果 你要 在 其他 线程 更新 UI元素的话 要注意两点。
1.是耗时的任务 用异步或线程处理。
2.更新页面UI 要用委托或回调的形式 让 UI主线程 执行。
不然 就有可能出现 界面假死 直到 任务完成。

oxf5deb3 发表于 2023-5-18 09:23

非UI线程更新UI,请使用begininvoke或者invoke

a952135763 发表于 2023-5-18 09:25

本帖最后由 a952135763 于 2023-5-17 22:36 编辑

private void BtnTestLogAsyncWriter_Click(object sender, EventArgs e)
这句代码改成
private async void BtnTestLogAsyncWriter_Click(object sender, EventArgs e)
就行了{:1_918:}
调用_StartWriteLog的时候 使用await

对了
private async void _StartWriteLog()
的void改成 task


原因就是 async void 和 async task的行为问题
async void在事件程序中并不会切换上下文,会让函数在调用方的上下文运行.
所以你调用方是主线程,当主线程的事件循环完成了一次 接下来就要运行你的_StartWriteLog方法
_StartWriteLog是堵塞的 界面当然卡死了

larryliao 发表于 2023-5-18 09:26

while 循环中加上 : Application.DoEvents(); 也可以防止界面假死

while (DateTime.Now < endTime)
    {
      await _LogAsyncWriter.WriteLogAsync(_OnceWriteText);
      writeCount++;
         Application.DoEvents();   //让界面响应事件
    }

ilovecomputer66 发表于 2023-5-18 11:01

a952135763 发表于 2023-5-18 09:25
private void BtnTestLogAsyncWriter_Click(object sender, EventArgs e)

但按你说这个该,照样卡住的呀
页: [1] 2
查看完整版本: 朋友托我问个问题,这个C#代码,为什么会导致winForm界面卡死