使用 while 循环取消 backgroundworker
cancelling a backgroundworker with while loop
我知道使用 eventwaithandles 取消 backgroundworker 的常见方法...
但我想知道使用 while 循环来捕获和暂停 backgroundworker 的工作是否正确?我这样编码:
Bool stop = false;
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
progressBar1.Minimum = 0;
progressBar1.Maximum = 100000;
progressBar1.Value = 0;
for (int i = 0; i < 100000; i++)
{
progressBar1.Value++;
if (i == 50000)
stop = true;
while (stop)
{ }
}
}
private void button1_Click(object sender, EventArgs e)
{
stop = !stop;
}
你试过了吗?发生了什么?这是你想要发生的吗?您是否注意到计算机的风扇正在加速运转,以在紧密的 "do-nothing" 循环中处理来自 CPU 的所有热量?
事实是,您首先不应该 "pause" 后台任务;如果您不想保留 运行,请打断它。如果您希望以后能够恢复,请提供一种允许这样做的机制。即使让您的线程高效地等待 WaitHandle
对象也是错误的做法,因为它浪费了一个线程池线程。
您在此处发布的代码是最糟糕的实施方式 "pausing"。不是等待某个同步对象,例如 WaitHandle
,而是让当前线程循环而不中断,不断检查标志的值。即使忽略你是否使用 volatile
的问题(代码示例没有显示,但它也不会编译,所以......),它是 可怕的 迫使 CPU 核心做这么多工作却一事无成。
首先不要暂停您的 BackgroundWorker.DoWork
处理程序。真的。只是不要那样做。但是,如果您坚持,那么至少使用某种可等待对象而不是像您在此处发布的示例中那样使用 "spin-wait" 循环。
下面是一个示例,说明如果您想避免在 "paused" 时完全占用线程,您的代码将如何工作。首先,不要使用 BackgroundWorker
,因为它没有优雅的方式来执行此操作。其次,一定要使用 await
……它可以满足您的具体需求:它允许当前方法 return,但不会丢失其进度。当它等待的事情指示完成时,该方法将恢复执行。
在下面的示例中,我试图猜测调用 RunWorkerAsync()
的代码是什么样的。或者更确切地说,我只是假设您有一个 button2
,单击它时您将调用该方法来启动您的辅助任务。如果这还不足以让您指出正确的方向,请通过包含 a good, minimal, complete code example 显示您实际在做什么来改进您的问题。
// These fields will work together to provide a way for the thread to interrupt
// itself temporarily without actually using a thread at all.
private TaskCompletionSource<object> _pause;
private readonly object _pauseLock = new object();
private void button2_Click(object sender, DoWorkEventArgs e)
{
// Initialize ProgressBar. Note: in your version of the code, this was
// done in the DoWork event handler, but that handler isn't executed in
// the UI thread, and so accessing a UI object like progressBar1 is not
// a good idea. If you got away with it, you were lucky.
progressBar1.Minimum = 0;
progressBar1.Maximum = 100000;
progressBar1.Value = 0;
// This object will perform the duty of the BackgroundWorker's
// ProgressChanged event and ReportProgress() method.
Progress<int> progress = new Progress<int>(i => progressBar1.Value++);
// We do want the code to run in the background. Use Task.Run() to accomplish that
Task.Run(async () =>
{
for (int i = 0; i < 100000; i++)
{
progress.Report(i);
Task task = null;
// Locking ensures that the two threads which may be interacting
// with the _pause object do not interfere with each other.
lock (_pauseLock)
{
if (i == 50000)
{
// We want to pause. But it's possible we lost the race with
// the user, who also just pressed the pause button. So
// only allocate a new TCS if there isn't already one
if (_pause == null)
{
_pause = new TaskCompletionSource<object>();
}
}
// If by the time we get here, there's a TCS to wait on, then
// set our local variable for the Task to wait on. In this way
// we resolve any other race that might occur between the time
// we checked the _pause object and then later tried to wait on it
if (_pause != null)
{
task = _pause.Task;
}
}
if (task != null)
{
// This is the most important part: using "await" tells the method to
// return, but in a way that will allow execution to resume later.
// That is, when the TCS's Task transitions to the completed state,
// this method will resume executing, using any available thread
// in the thread pool.
await task;
// Once we resume execution here, reset the TCS, to allow the pause
// to go back to pausing again.
lock (_pauseLock)
{
_pause.Dispose();
_pause = null;
}
}
}
});
}
private void button1_Click(object sender, EventArgs e)
{
lock (_pauseLock)
{
// A bit more complicated than toggling a flag, granted. But it achieves
// the desirable goal.
if (_pause == null)
{
// Creates the object to wait on. The worker thread will look for
// this and wait if it exists.
_pause = new TaskCompletionSource<object>();
}
else if (!_pause.Task.IsCompleted)
{
// Giving the TCS a result causes its corresponding Task to transition
// to the completed state, releasing any code that might be waiting
// on it.
_pause.SetResult(null);
}
}
}
请注意,上面的内容与您的原始示例一样人为设计。如果您真的只有一个简单的单循环变量,从 0 迭代到 100,000 并在中途停止,那么几乎不需要像上面那样复杂的东西。您只需将循环变量存储在某个数据结构中,退出 运行 任务线程,然后当您想要恢复时,传入当前循环变量值,以便该方法可以在正确的索引处恢复。
但我假设您的真实示例并非如此简单。并且上述策略适用于 any 有状态处理,编译器会为您完成存储中间状态的所有繁重工作。
我知道使用 eventwaithandles 取消 backgroundworker 的常见方法... 但我想知道使用 while 循环来捕获和暂停 backgroundworker 的工作是否正确?我这样编码:
Bool stop = false;
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
progressBar1.Minimum = 0;
progressBar1.Maximum = 100000;
progressBar1.Value = 0;
for (int i = 0; i < 100000; i++)
{
progressBar1.Value++;
if (i == 50000)
stop = true;
while (stop)
{ }
}
}
private void button1_Click(object sender, EventArgs e)
{
stop = !stop;
}
你试过了吗?发生了什么?这是你想要发生的吗?您是否注意到计算机的风扇正在加速运转,以在紧密的 "do-nothing" 循环中处理来自 CPU 的所有热量?
事实是,您首先不应该 "pause" 后台任务;如果您不想保留 运行,请打断它。如果您希望以后能够恢复,请提供一种允许这样做的机制。即使让您的线程高效地等待 WaitHandle
对象也是错误的做法,因为它浪费了一个线程池线程。
您在此处发布的代码是最糟糕的实施方式 "pausing"。不是等待某个同步对象,例如 WaitHandle
,而是让当前线程循环而不中断,不断检查标志的值。即使忽略你是否使用 volatile
的问题(代码示例没有显示,但它也不会编译,所以......),它是 可怕的 迫使 CPU 核心做这么多工作却一事无成。
首先不要暂停您的 BackgroundWorker.DoWork
处理程序。真的。只是不要那样做。但是,如果您坚持,那么至少使用某种可等待对象而不是像您在此处发布的示例中那样使用 "spin-wait" 循环。
下面是一个示例,说明如果您想避免在 "paused" 时完全占用线程,您的代码将如何工作。首先,不要使用 BackgroundWorker
,因为它没有优雅的方式来执行此操作。其次,一定要使用 await
……它可以满足您的具体需求:它允许当前方法 return,但不会丢失其进度。当它等待的事情指示完成时,该方法将恢复执行。
在下面的示例中,我试图猜测调用 RunWorkerAsync()
的代码是什么样的。或者更确切地说,我只是假设您有一个 button2
,单击它时您将调用该方法来启动您的辅助任务。如果这还不足以让您指出正确的方向,请通过包含 a good, minimal, complete code example 显示您实际在做什么来改进您的问题。
// These fields will work together to provide a way for the thread to interrupt
// itself temporarily without actually using a thread at all.
private TaskCompletionSource<object> _pause;
private readonly object _pauseLock = new object();
private void button2_Click(object sender, DoWorkEventArgs e)
{
// Initialize ProgressBar. Note: in your version of the code, this was
// done in the DoWork event handler, but that handler isn't executed in
// the UI thread, and so accessing a UI object like progressBar1 is not
// a good idea. If you got away with it, you were lucky.
progressBar1.Minimum = 0;
progressBar1.Maximum = 100000;
progressBar1.Value = 0;
// This object will perform the duty of the BackgroundWorker's
// ProgressChanged event and ReportProgress() method.
Progress<int> progress = new Progress<int>(i => progressBar1.Value++);
// We do want the code to run in the background. Use Task.Run() to accomplish that
Task.Run(async () =>
{
for (int i = 0; i < 100000; i++)
{
progress.Report(i);
Task task = null;
// Locking ensures that the two threads which may be interacting
// with the _pause object do not interfere with each other.
lock (_pauseLock)
{
if (i == 50000)
{
// We want to pause. But it's possible we lost the race with
// the user, who also just pressed the pause button. So
// only allocate a new TCS if there isn't already one
if (_pause == null)
{
_pause = new TaskCompletionSource<object>();
}
}
// If by the time we get here, there's a TCS to wait on, then
// set our local variable for the Task to wait on. In this way
// we resolve any other race that might occur between the time
// we checked the _pause object and then later tried to wait on it
if (_pause != null)
{
task = _pause.Task;
}
}
if (task != null)
{
// This is the most important part: using "await" tells the method to
// return, but in a way that will allow execution to resume later.
// That is, when the TCS's Task transitions to the completed state,
// this method will resume executing, using any available thread
// in the thread pool.
await task;
// Once we resume execution here, reset the TCS, to allow the pause
// to go back to pausing again.
lock (_pauseLock)
{
_pause.Dispose();
_pause = null;
}
}
}
});
}
private void button1_Click(object sender, EventArgs e)
{
lock (_pauseLock)
{
// A bit more complicated than toggling a flag, granted. But it achieves
// the desirable goal.
if (_pause == null)
{
// Creates the object to wait on. The worker thread will look for
// this and wait if it exists.
_pause = new TaskCompletionSource<object>();
}
else if (!_pause.Task.IsCompleted)
{
// Giving the TCS a result causes its corresponding Task to transition
// to the completed state, releasing any code that might be waiting
// on it.
_pause.SetResult(null);
}
}
}
请注意,上面的内容与您的原始示例一样人为设计。如果您真的只有一个简单的单循环变量,从 0 迭代到 100,000 并在中途停止,那么几乎不需要像上面那样复杂的东西。您只需将循环变量存储在某个数据结构中,退出 运行 任务线程,然后当您想要恢复时,传入当前循环变量值,以便该方法可以在正确的索引处恢复。
但我假设您的真实示例并非如此简单。并且上述策略适用于 any 有状态处理,编译器会为您完成存储中间状态的所有繁重工作。