Recently I was dealing with some bugs in C # about timeout behavior. The solution is very interesting, so I am here to share to the general Bo friends.
Here are some of the things I want to deal with:
We have an application that has a module in it that displays a message dialog box to the user and closes the dialog box automatically after 15 seconds. However, if the user closes the dialog box manually, we do not need to do any processing at timeout.
There is a lengthy execution in the program. If the operation lasts more than 5 seconds, terminate the operation.
Our application has operations that are unknown in execution time. When the execution time is too long, we need to display a "in progress" pop-up window to prompt the user to wait patiently. We cannot estimate how long this operation will last, but generally it lasts less than a second. To avoid pop-ups flashing, we only want to show this pop-up window in 1 seconds. Conversely, if the operation is completed within 1 seconds, you do not need to display this pop-up window.
These problems are similar. After the timeout, we must perform the x operation unless y occurs at that time.
To find a way to solve these problems, I created a class during the experiment:
public class OperationHandler{ private IOperation _operation; public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { //在超时后需要调用 "_operation.DoOperation()" } public void StopOperationIfNotStartedYet() { //在超时期间需要停止"DoOperation" }}
My Operation class:
public class MyOperation : IOperation{ public void DoOperation() { Console.WriteLine("Operation started"); }}public class MyOperation : IOperation{ public void DoOperation() { Console.WriteLine("Operation started"); }}
My test program:
static void Main(string[] args){ var op = new MyOperation(); var handler = new OperationHandler(op); Console.WriteLine("Starting with timeout of 5 seconds"); handler.StartWithTimeout(5 * 1000); Thread.Sleep(6 * 1000); Console.WriteLine("Starting with timeout of 5 but cancelling after 2 seconds"); handler.StartWithTimeout(5 * 1000); Thread.Sleep(2 * 1000); handler.StopOperationIfNotStartedYet(); Thread.Sleep(4 * 1000); Console.WriteLine("Finished..."); Console.ReadLine();}
The result should be:
Starting with timeout of 5 seconds Operation started Starting with timeout of 5 and cancelling after 2 seconds Finished ... |
Now we can start experimenting!
Solution 1: Hibernate on another thread
My initial plan was to hibernate on a different thread and use a Boolean value to mark whether stop was invoked.
public class OperationHandler{ private IOperation _operation; private bool _stopCalled; public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { Task.Factory.StartNew(() => { _stopCalled = false; Thread.Sleep(timeoutMillis); if (!_stopCalled) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { _stopCalled = true; }}
For normal thread execution steps, this code runs without problems, but always feels awkward. After careful exploration, I found some of the tricks. First, during the timeout period, one thread was removed from the thread pool and nothing was done, apparently the thread was wasted. Second, if the program stops executing, the thread will continue to hibernate until the timeout is over, wasting CPU time.
But these are not the worst things about our code, and in fact there is an obvious bug in our program:
If we set a timeout of 10 seconds, after starting the operation, 2 seconds stop and then start again within 2 seconds.
When we start the second time, our _stopcalled flag will turn false. Then, when our first thread.sleep () finishes, even if we cancel it, it will call dooperation.
After that, the second thread.sleep () is completed and a second call to Dooperation is made. The result is that Dooperation was called two times, which is obviously not what we expected.
If you have such a timeout of 100 times per minute, I will find it hard to catch this error.
When Stopoperationifnotstartedyet is called, we need some way to cancel the dooperation call.
What if we try to use a timer?
Solution 2: Use the timer
. NET has three different types of time-registers, namely:
- The timer control under the System.Windows.Forms namespace, which inherits directly from Componet.
- The Timer class under the System.Timers namespace.
- System.Threading.Timer class.
Of these three kinds of timers, System.Threading.Timer is sufficient to meet our needs. Here is the code that uses the timer:
public class OperationHandler{ private IOperation _operation; private Timer _timer; public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { if (_timer != null) return; _timer = new Timer( state => { _operation.DoOperation(); DisposeOfTimer(); }, null, timeoutMillis, timeoutMillis); } public void StopOperationIfNotStartedYet() { DisposeOfTimer(); } private void DisposeOfTimer() { if (_timer == null) return; var temp = _timer; _timer = null; temp.Dispose(); }}
The results of the implementation are as follows:
Starting with timeout of 5 seconds Operation started Starting with timeout of 5 and cancelling after 2 seconds Finished ... |
Now when we stop the operation, the timer is discarded, which avoids the operation being performed again. This has already achieved our initial idea, and there is, of course, another way to deal with the problem.
Solution 3:manualresetevent or AutoResetEvent
Manualresetevent/autoresetevent literally means resetting events manually or automatically. AutoResetEvent and ManualResetEvent are classes that help you handle multi-threaded communication. The basic idea is that a thread can wait until another thread finishes an operation, and then the waiting thread can "release" and continue running.
ManualResetEvent classes and AutoResetEvent classes see MSDN:
ManualResetEvent class: Https://msdn.microsoft.com/zh-cn/library/system.threading.manualresetevent.aspx
AutoResetEvent class: Https://msdn.microsoft.com/zh-cn/library/system.threading.autoresetevent.aspx
To this end, in this case, until the manual reset event signal appears, MRe. WaitOne () will always wait. MRe. Set () resets the token to the event signal. ManualResetEvent will release all threads that are currently waiting. AutoResetEvent will only release a waiting thread and immediately become a no-signal. WaitOne () can also accept timeouts as parameters. If set () is not called during the timeout period, the thread is freed and WaitOne () returns false.
The following is the implementation code for this feature:
public class OperationHandler{ private IOperation _operation; private ManualResetEvent _mre = new ManualResetEvent(false); public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { _mre.Reset(); Task.Factory.StartNew(() => { bool wasStopped = _mre.WaitOne(timeoutMillis); if (!wasStopped) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { _mre.Set(); }}
Execution Result:
Starting with timeout of 5 seconds Operation started Starting with timeout of 5 and cancelling after 2 seconds Finished ... |
Personally, I'm very inclined to this solution, which is cleaner and more concise than the solution we use with the timer.
For our simple features, ManualResetEvent and timer solutions work fine. Now let's add a bit of challenge.
New requirements for improvement
Suppose we can now call Startwithtimeout () several times in a row instead of waiting for the first timeout to complete.
But what is the expected behavior here? There are actually several possibilities:
- When calling Startwithtimeout during a previous startwithtimeout timeout: ignores the second boot.
- When calling Startwithtimeout during the previous startwithtimeout timeout period: Stop the initial session start and use the new startwithtimeout.
- When calling Startwithtimeout during a previous startwithtimeout timeout: Dooperation is called in two launches. Stop all operations that have not yet started (within the timeout period) in Stopoperationifnotstartedyet.
- When calling Startwithtimeout during a previous startwithtimeout timeout: Dooperation is called in two launches. At Stopoperationifnotstartedyet stop a random operation that has not yet started.
possibility 1 can be easily implemented by timer and ManualResetEvent. In fact, we've covered this in our timer solution.
public void StartWithTimeout(int timeoutMillis){ if (_timer != null) return; ... public void StartWithTimeout(int timeoutMillis) { if (_timer != null) return; ...}
possibility 2 can also be implemented easily. This place, please allow me to sell a Moe, the code to write their own ha ^_^
possibility 3 cannot be achieved by using a timer. We will need to have a set of timers. Once the operation is stopped, we need to check and process all the subkeys in the timer set. This method is feasible, but by ManualResetEvent we can do it very concisely and easily!
probability 4 is similar to probability 3 and can be achieved by a set of timers.
Possibility 3: Stop all operations with a single ManualResetEvent
Let's take a look at the difficulties encountered in this:
Suppose we call startwithtimeout for a 10-second timeout.
After 1 seconds, we call another startwithtimeout again, the timeout time is 10 seconds.
After another 1 seconds, we call the other startwithtimeout again, and the timeout time is 10 seconds.
The expected behavior is that these 3 operations will start in 10 seconds, 11 seconds, and 12 seconds in turn.
If we call Stop () after 5 seconds, the expected behavior is that all the pending operations will stop and subsequent operations will not work.
I changed the Program.cs slightly so that I could test the operation. This is the new code:
class Program{ static void Main(string[] args) { var op = new MyOperation(); var handler = new OperationHandler(op); Console.WriteLine("Starting with timeout of 10 seconds, 3 times"); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(13 * 1000); Console.WriteLine("Starting with timeout of 10 seconds 3 times, but cancelling after 5 seconds"); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(1000); handler.StartWithTimeout(10 * 1000); Thread.Sleep(5 * 1000); handler.StopOperationIfNotStartedYet(); Thread.Sleep(8 * 1000); Console.WriteLine("Finished..."); Console.ReadLine(); }}
Here's a solution for using ManualResetEvent:
public class OperationHandler{ private IOperation _operation; private ManualResetEvent _mre = new ManualResetEvent(false); public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { Task.Factory.StartNew(() => { bool wasStopped = _mre.WaitOne(timeoutMillis); if (!wasStopped) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { Task.Factory.StartNew(() => { _mre.Set(); Thread.Sleep(10);//This is necessary because if calling Reset() immediately, not all waiting threads will ‘proceed‘ _mre.Reset(); }); }}
The output is the same as expected:
Starting with timeout of seconds, 3 times Operation started Operation started Operation started Starting with timeout of ten seconds 3 times, but cancelling after 5 seconds Finished ... |
It's Kai Sen, isn't it?
When I checked the code, I found that Thread.Sleep (10) was essential, which was clearly beyond my expectation. Without it, only 1-2 threads are in progress, except for the 3 pending threads. Obviously, because reset () happens too fast, the third thread will stay on WaitOne ().
Possibility 4: Single AutoResetEvent stop a random operation
Suppose we call startwithtimeout for a 10-second timeout. After 1 seconds, we call another startwithtimeout again, the timeout time is 10 seconds. After another 1 seconds, we call the other startwithtimeout again, and the timeout time is 10 seconds. Then we call Stopoperationifnotstartedyet ().
There are currently 3 operation timeouts, waiting to start. The expected behavior is that one is stopped and the other 2 operations should be able to start normally.
Our Program.cs can remain the same as before. Operationhandler has made some adjustments:
public class OperationHandler{ private IOperation _operation; private AutoResetEvent _are = new AutoResetEvent(false); public OperationHandler(IOperation operation) { _operation = operation; } public void StartWithTimeout(int timeoutMillis) { _are.Reset(); Task.Factory.StartNew(() => { bool wasStopped = _are.WaitOne(timeoutMillis); if (!wasStopped) _operation.DoOperation(); }); } public void StopOperationIfNotStartedYet() { _are.Set(); }}
The execution results are:
Starting with timeout of seconds, 3 times Operation started Operation started Operation started Starting with timeout of ten seconds 3 times, but cancelling after 5 seconds Operation started Operation started Finished ... |
Conclusion
When processing thread communication, it is common to continue to perform certain operations after a time-out. We have tried some good solutions. Some solutions may look good and even work in a specific process, but it's also possible to hide fatal bugs in your code. When this happens, we need to be particularly careful when dealing with it.
AutoResetEvent and ManualResetEvent are very powerful classes, and I use them all the time when I'm dealing with thread communication. These two classes are very practical. Friends who are dealing with thread communication, join them in the project!
Practice of multithreading Timeout processing in C #