一个更好的线程同步类
使用 WaitTimeout 轻松清晰地等待、超时和评估早期线程释放条件
引言
我一直不喜欢 AutoResetEvent
和 ManuaResetEvent
。它们是微软为我们提供的替代 while
循环、Thread.Sleep(n)
和一些早期超时条件的方案。但是这两个类让你的代码变得丑陋和复杂,而且实际上并没有提供我一直期望的功能和清晰度。
例如,提前释放等待线程的唯一方法是确保工作线程可以访问 AutoResetEvent
对象,并调用 .Set()
。这意味着它必须被传递给正在运行的线程,或者你必须确保它在足够高的范围内,以便线程可见 - 也许可以通过将其设为 private
实例变量来实现。
此外,要评估提前释放等待线程,因为工作线程花费的时间超出了你愿意允许的时间,你需要启动另一个线程来监视工作线程的状态并将其传递给 AutoResetEvent
对象,以便它可以调用 .Set()
并为你释放等待线程。
在我看来,这现在应该很简单。简单明了。
有了 WaitTimeout
,我们正在接近目标。
Using the Code
WaitTimeout
并非 AutoResetEvent
的替代品。相反,它提供了一种不同的(更清晰的)方式,用于在某些使用场景中使用线程同步事件类来可视化你的代码流程。
AutoResetEvent
的 Set()
和 Reset()
函数并没有描述我在应用程序中正在做什么,它们描述了该类内部发生的事情。
我对这个不感兴趣。
我正在编写自己的应用程序或库,当我查看我的代码时,我不希望看到 .Set()
或 Reset()
。我想知道线程是否正在等待或被释放。我希望能够一目了然地看到线程将等待多长时间,以及提前超时条件是什么,而无需深入研究我的代码来查找我运行的用于处理该问题的监视线程。如果我正在等待多个线程完成,我想知道我正在等待多少个,并且我不一定需要创建 AutoResetEvent
对象的数组来处理它。
它应该比这更简单、更容易、更清晰。
WaitTimeout
旨在处理三个特定的场景
- 带有超时的简单等待/释放情况(类似于
AutoResetEvent
的功能)private WaitTimeout SimpleWaitTimeout; private int timeoutDuration; // Simple Wait() with timeout example: ////////////////////////////////// private void button1_Click(object sender, EventArgs e) { try { // Get the timeout duration you specified: timeoutDuration = int.Parse(tbSimpleWaitSeconds.Text.Trim()); } catch (Exception) { MessageBox.Show("The value you type into the 'Wait Seconds' textbox must be numerical.", "Typo?", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } yourComplicatedReleaseEarlyCriteria = false; SimpleWaitTimeout = new WaitTimeout(timeoutDuration); // Start the work thread: new Thread(delegate() { // Do some work here... SimpleWaitTimeout.Wait(); // Update the UI when we're finished waiting: this.Invoke((MethodInvoker)delegate() { this.lblStatus.Text = "We're out."; }); }).Start(); // Updating the UI with the remaining time until timeout from a background thread: new Thread(delegate() { while (!SimpleWaitTimeout.IsTimedOut()) { this.Invoke((MethodInvoker)delegate() { this.lblStatus.Text = "Remaining time: " + SimpleWaitTimeout.GetRemainingSeconds() + " seconds."; }); Thread.Sleep(1); } }).Start(); } private void button2_Click(object sender, EventArgs e) { // Simply call .release() when you wish to stop Waiting, for // AutoResetEvent .Set() type functionality. SimpleWaitTimeout.Release(); }
- 你希望评估早期释放条件而无需麻烦和混乱的等待/释放场景,例如
// Wait() with timeout and end user code evaluated for early release example: // The idea here is that you have a thread waiting on one or more threads to complete. // You construct your WaitTimeout specifying timeoutDuration as the // maximum amount of milliseconds you will allow this thread to wait, // while having WaitTimeout evaluate the criteria in your anonymous delegate // to determine if it should release early. For our example, // the anonymous delegate is only checking to see if the yourComplicatedReleaseEarlyCriteria // bool has been set to true by you, after clicking the release early button. private void btWaitTimeoutWithDelegate_Click(object sender, EventArgs e) { try { // Get the timeout duration you specified: timeoutDuration = int.Parse(tbWaitSecondsWithDelegate.Text.Trim()); } catch (Exception) { MessageBox.Show("The value you type into the 'Wait Seconds' textbox must be numerical.", "Typo?", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // You can add any early release evaluation code here, but it must return a bool // indicating the early release status. true = release now and false = continue to wait. yourComplicatedReleaseEarlyCriteria = false; WaitTimeout waitTimeout = new WaitTimeout(timeoutDuration, delegate() { return yourComplicatedReleaseEarlyCriteria; }); new Thread(delegate() { // Do some work here... waitTimeout.Wait(); this.Invoke((MethodInvoker)delegate() { this.lblStatus.Text = "We're out."; }); }).Start(); // Using .IsTimmedOut() and .GetRemainingSeconds() to display the remaining time: new Thread(delegate() { while (!waitTimeout.IsTimedOut()) { this.Invoke((MethodInvoker)delegate() { this.lblStatus.Text = "Remaining time: " + waitTimeout.GetRemainingSeconds() + " seconds."; }); Thread.Sleep(1); } }).Start(); } private void btWaitTimeoutWithDelegateExitEarly_Click(object sender, EventArgs e) { yourComplicatedReleaseEarlyCriteria = true; }
- 以及你启动了多个工作线程,你需要等待它们完成的场景 - 但你希望在它们花费太长时间、全部报告或满足某些早期释放条件时停止等待它们(例如,如果你的最终用户因沮丧而抓狂并猛按取消按钮)
// WaitOn() with timeout and end user code evaluated for early release // while waiting on 15 threads to complete. // Use this when you have started multiple threads, and you need to wait for them // all to finish before execution can continue // on this thread. The value you set in timeoutDuration is the // maximum amount of time you are willing to allow before // continuing execution on this thread, weather your worker threads complete or not. // WaitOn() returns true if all threads // expected to report in have done so before WaitTimeout has timed out or // been released early, and false otherwise. // I know Microsoft has provided functionality that accomplishes // this in the Task Parallel Library, but I find // it to be overly complicated, cumbersome and ugly. This class makes it trivial and clear. private void btStartMultipleThreadEval_Click(object sender, EventArgs e) { try { timeoutDuration = int.Parse(tbWaitTimeoutMultipleThreads.Text.Trim()); } catch (Exception) { MessageBox.Show("The value you type into the 'Wait Seconds' textbox must be numerical.", "Typo?", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // You can add any early release evaluation code here, but it must return a bool // indicating the early release status. true = release immediately and // false = continue to wait. yourComplicatedReleaseEarlyCriteria = false; WaitTimeout waitTimeout = new WaitTimeout(timeoutDuration, delegate() { return yourComplicatedReleaseEarlyCriteria; }); bool succeeded = false; String msg = ""; new Thread(delegate() { this.Invoke((MethodInvoker)delegate() { this.lblStatus.Text = "Threads completed: 0"; }); // Start up 15 threads, and have them report in as they complete: for (int threads = 1; threads < 16; threads++) { // Only create new threads if we haven't been released early already: if (!waitTimeout.IsTimedOut()) { new Thread(delegate(Object o) { // We box this and unbox it here to // make a local copy for our anonymous delegate. int thisThreadNumber = (int)o; // We need to simulate some work being done, so // we'll stagger thread completion over 7500 ms // and bail if we are released early by you, our end user. // This is a great place to use WaitTimeout. So: new WaitTimeout(500 * thisThreadNumber, delegate() { return waitTimeout.IsTimedOut(); }).Wait(); // What's happening there? We're waiting (blocking) // for (500 * thisThreadNumber) milliseconds, // and specifying that // if waitTimeout.IsTimedOut() we should release early and // continue with execution on this thread, // All in a single line of code. // Only report in if we haven't been // released early already: if (!waitTimeout.IsTimedOut()) { // Report in when complete. waitTimeout.ReportThreadComplete(); // Update the UI: this.Invoke((MethodInvoker)delegate() { this.lblStatus.Text = "Threads completed: " + waitTimeout.GetNumReportedComplete().ToString(); }); } }).Start(threads); } } // We're waiting on 15 threads to complete. // If they don't complete within the time you specified, // We will time out and move on. If we time out // before all 15 have completed, WaitOn() will return false. succeeded = waitTimeout.WaitOn(15); msg = "We're out. Threads completed:" + waitTimeout.GetNumReportedComplete().ToString(); msg += succeeded ? " (Success! All threads complete before timeout.)." : " (Failure! Timed out or released early before all threads completed.)"; this.Invoke((MethodInvoker)delegate() { this.lblStatus.Text = msg; }); }).Start(); } private void btMultipleThreadEvalExitEarly_Click(object sender, EventArgs e) { yourComplicatedReleaseEarlyCriteria = true; }
关注点
是的,我知道微软的 TPL 和 Parallel Foreach 也可以在我们的“多线程报告”场景中使用,但我想如果你看看所涉及的复杂性,你会同意这个实现更清晰、更简单。它使这种类型的需求变得微不足道。
历史
- 2016 年 4 月 23 日:初始版本