c# - 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):创建窗口句柄或其他使用“无效参数”的绘画事件时出错......
我在这里错过了什么吗?用所有相关线程干净地关闭表单而不遇到错误的建议方法是什么?
解决方案
如果您使用任务和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;
}
推荐阅读
- java - 为什么for循环更新语句不能在循环内更新
- postgresql - postgres 用户的 watchbog 命令不断占用 100% CPU
- c# - 如何在 stimulsoft 报告中将数据传递给 BusinessObject?
- plot - 支撑位和阻力位的价格标签没有显示在正确的位置
- ios - 来自@Environment var Changes 的不一致 ContentView 更新
- vue.js - 如何在 vue js 中编写退格 html
- ios - 无法在其父视图之外平移手势较小的 UIView
- python - 结合不同周期频率的 Pandas 数据帧
- c# - 有人能解释一下我怎么能用这段代码调用 ridgedbody2d 吗?
- azure-devops - 探索托管代理上的文件系统