首页 > 解决方案 > C# 在 Environment.Exit 调用上创建窗口句柄时出错

问题描述

我创建了一个简单的线程控制器类来管理线程的执行,这里是它的代码:

 public class ThreadController {
  int waitCount;
  Thread myThread;

  public ThreadController() {
    //
  }

  public void StartThread() {
    waitCount = 0;
    // launch a thread to show an alert when conditions are met!
    myThread = new Thread(new ThreadStart(ThreadAction));
    myThread.IsBackground = true;
    myThread.Start();
  }

  // method is async as it call an async method itself!
  void ThreadAction() {
    while (myThread.IsAlive) {
      Thread.Sleep(5000);
      bool doStop = DoStopTest().Result; // some async function testing stop criterion
      if (doStop) {            
        MainForm.BeginInvoke(new MethodInvoker(delegate() {
            MessageBox.Show("Thread stopped!");
          }));
        // 
        myThread.Abort();
      }
      ++waitCount;
      if (waitCount >= 15) {
        myThread.Abort();
      }
      Thread.Sleep(5000);
    }
  }
}

现在,我想确保当我关闭时,上面创建的线程(可能有几个)被杀死MainForm,我读到的应该在FormClosing事件中完成如下:

void Main_FormClosing(object Sender, FormClosingEventArgs e) {
  // unfortunately, an error is thrown when I call following line...
  Environment.Exit(Environment.ExitCode);
}

Environment.Exit调用实际上产生了一些奇怪的异常......有时“vhost32.exe停止工作”,有时出现错误System.ComponentModel.Win32Exception(0x80004005):创建窗口句柄或其他使用“无效参数”的绘画事件时出错......

我在这里错过了什么吗?用所有相关线程干净地关闭表单而不遇到错误的建议方法是什么?

标签: c#multithreadingkill-process

解决方案


如果您使用任务和async/await. DoStopTest()似乎已经返回了一个任务,所以不需要使用原始线程。

代码可以像循环一样简单:

public async Task MyTestAndWait()
{
    await Task.Delay(5000);
    var waitCount=0;
    while( waitCount++ < 15 && !(await DoStopTest()))
    {
        await Task.Delay(10000);

    }
    MessageBox.Show("Thread stopped!");
}

在每次调用await执行后在原始同步上下文中恢复。对于桌面应用程序,这就是 UI 线程。这意味着没有必要使用BeginInvoke

不应中止线程。正确的方法是检查线程安全信号,例如在线程需要退出时引发的 ManualResetEvent。当发出信号时,线程的代码本身应该退出。

使用大量事件可能会有点混乱,这就是为什么 .NET 4.5 添加了 CancellationToken 和 CancellationTokenSource 类,它们可用于通知线程和任务它们需要优雅地取消和退出。

public async Task MyTestAndWait(CancellationToken ct,int initialDelay,int pollDelay)
{
    await Task.Delay(initialDelay,ct);
    var waitCount=0;
    while(!ct.IsCancellationRequested && waitCount++ < 15 && !(await DoStopTest()))
    {
        await Task.Delay(pollDelay,ct);

    }
    MessageBox.Show("Poll stopped!");
}

这将取消延迟和循环,但不会取消对DoStepTest(). 该方法也必须接受 CancellationToken 参数

CancellationTokens 由CancellationTokenSource类创建。其中一个重载接受超时,可用于取消整个操作:

public async void SendSMS_Click(object sender, EventArgs args)
{
    var cts=new CancellationTokenSource(TimeSpan.FromMinutes(15));
    await MyTestAndAwait(cts.Token,5000,10000);       
}

cts 可以存储在一个字段中,以允许由于另一个事件(如按钮单击)而取消:

CancellationTokenSource _cts;
public async void SendSMS_Click(object sender, EventArgs args)
{
    SendSMS.Enabled=false;
    Cancel.Enabled=true;

    _cts=new CancellationTokenSource(TimeSpan.FromMinutes(15);
    await MyTestAndAwait(cts.Token,5000,10000);       
    _cts=null;

    SendSMS.Enabled=true;
    Cancel.Enabled=false;    
}

public async void Cancel_Click(object sender, EventArgs args)
{
    _cts?.Cancel();
}

关闭表单时,可以使用相同的代码表示取消:

void Main_FormClosing(object Sender, FormClosingEventArgs e) 
{
    _cts.?Cancel();
}

顺便说一句,没有理由调用Environment.Exit()表单的 Closing 或 Closed 事件。关闭主窗体将结束应用程序,除非有另一个线程正在运行

更新

看起来实际的问题是如何通过轮询其发送状态来验证是否发送了 SMS。这种情况下的代码会有所不同,但仍然使用任务。该方法不应该对 UI 有任何引用,因此可以将其移至单独的服务层类。毕竟,更改提供程序不应该导致更改 UI

假设使用了 HttpClient,它可能看起来像这样:

//In an SmsService class

public async Task<(bool ok,string msg)> SendSmsAsync(string phone,string message,CancellationToken ct)
{
    var smsMsg=BuildSmsContent(phone,string);
    await _httpClient.PostAsync(smsMsg,ct);

    //wait before polling
    await Task.Delay(_initialDelay,ct);
    for(int i=0;i<15 && !ct.IsCancellationRequested;i++)
    {
        var checkMsg=CheckStatusContent(phone,string);
        var response=await _httpClient.GetAsync(check,ct);

        if (ct.IsCancellationRequested) break;

        //Somehow check the response. Assume it has a flag and a Reason
        var status=ParseTheResponse(response);
        switch(status.Status)
        {
            case Status.OK:
                return (ok:true,"Sent");
            case Status.Error:
                return (ok:failed,status.Reason);
            case Status.Pending:
                await Task.Delay(_pollDelay,ct);
                break;
        }
    }
    return (ok:false,"Exceeded retries or cancelled");    
}

此方法可用于按钮事件:

CancellationTokenSource _cts;

public async void SendSMS_Click(object sender, EventArgs args)
{
    DisableSending();

    var phone=txtPhone.Text;
    var message=txtMessage.Text;
    _cts=new CancellationTokenSource(TimeSpan.FromMinutes(15);

    var (ok,reason)=await _smsService.SendSmsAsync(phone,message,cts.Token);       
    _cts=null;

    if (ok)
    {
        MessageBox.Show("OK");
    }
    else
    {
        MessageBox.Show($"Failed: {reason}");
    }

    EnableSending();
}

public void EnableSending()
{
    SendSMS.Enabled=true;
    Cancel.Enabled=false;    
}

public void DisableSending()
{
    SendSMS.Enabled=false;
    Cancel.Enabled=true;    
}

推荐阅读