几年后编辑:这显然是一种糟糕的方法,在我开始使用 C#/.NET 时。希望这个问题能帮助另一个有同样“问题”的菜鸟。
这是处理这种情况的最佳方法吗?
while(true)
{
if(Main.ActiveForm != null)
{
Main.ActiveForm.Invoke(new MethodInvoker(Main.SomeMethod));
break;
}
}
这是在第二个线程上执行的。
几年后编辑:这显然是一种糟糕的方法,在我开始使用 C#/.NET 时。希望这个问题能帮助另一个有同样“问题”的菜鸟。
这是处理这种情况的最佳方法吗?
while(true)
{
if(Main.ActiveForm != null)
{
Main.ActiveForm.Invoke(new MethodInvoker(Main.SomeMethod));
break;
}
}
这是在第二个线程上执行的。
Is this the best way to approach this scenario?
Just to clarify, the scenario is "I have a property of reference type; as soon as the property is not null I wish to invoke one of its methods", and the technique is "spin up another thread, busy-wait until the value is not null, invoke, and stop waiting".
The answer to your question is no, this is not the best way to approach this scenario. This is a terrible way to solve this problem for several reasons.
First, the code is simply wrong. The C# language makes no guarantee that this code works. If it works, then it is working by accident, and it can stop working at any time.
There are three reasons that this code is wrong.
The first reason it is wrong is because of the way threads work on modern operating systems. It is possible that the two threads are each assigned to their own processor. When a processor accesses memory on a modern machine, it does not go out to main memory every time. Rather, it fetches hundreds or thousands of nearby values into a cache the first time you hit an address. From then on, it accesses the local cache rather than taking the expensive bus ride back to main memory. The implications of that should be obvious: if one thread is writing and another thread is reading, then one thread might be writing to one processor cache and the other might be reading from an entirely different processor cache. They can be inconsistent forever if nothing forces them to be consistent, and therefore your loop can run forever even if the property has been set on another thread.
(And the "backwards" case is also possible; if the value of the property is now null, and was set at some time in the past, then it is possible that the second thread is reading the old, stale value and not the fresh null value. It therefore could decide to not wait at all, and invoke the method on a stale value of the property.)
The second reason this code is wrong is because it has a race condition. Suppose someone assigns the property to non-null on thread one, and then thread two reads it as non-null so you enter the body of the "if", and then thread three assigns it back to null, and then thread two reads it as null and crashes.
The third reason this code is wrong is because the compiler -- either the C# compiler or the jitter -- is permitted to "optimize" it so that it stays in the loop forever without doing the test a second time. The optimizer is allowed to analyze the code and realize that after the first time through the loop, if the test fails then nothing in the rest of the loop can cause it to succeed. It is permitted to then skip the test the next time through because it "knows" that it cannot succeed. Remember, the optimizer is permitted to make any optimization that would be invisible in a single-threaded program.
The optimizer does not actually make this optimization (to my knowledge) but it is permitted to, and a future version could do so. The optimizer can and does make similar optimizations in similar situations.
In order to make this code correct there must be a memory barrier in place. The most common technique for introducing a barrier is to make the access "volatile". The memory barrier forces the processor to abandon its cache and go back to main memory, and discourages the compiler from making aggressive optimizations. Of course, properties may not be volatile, and this technique utterly wrecks performance because it eliminates the one of the most important optimizations in modern processors. You might as well be accessing main memory by carrier pigeon, the cost is so onerous compared to hitting the cache.
Second, the code is bad because you are burning an entire processor sitting there in a tight loop checking a property. Imagine a processor is a car. Maybe your business owns four cars. You are taking one of them and driving it around the block non-stop, at high speed, until the mailman arrives. That is a waste of a valuable resource! It will make the entire machine less responsive, on laptops it will chew through battery like there is no tomorrow, it'll create waste heat, it's just bad.
I note however that at least you are marshalling the cross-thread call back to the UI thread, which is correct.
The best way to solve this problem is to not solve it. If you need something to happen when a property becomes non-null, then the best solution is to handle a change event associated with that property.
If you cannot do that then the best solution is to make the action the responsibility of the property. Change the setter so that it does the action when it is set to non-null.
If you can't make it the responsibility of the property, then make it the responsibility of the user who is setting the property. Require that every time the property be set to non-null, that the action be performed.
If you can't do that then the safest way to solve this problem is to NOT spin up another thread. Instead, spin up a timer that signals the main thread every half second or so, and have the timer event handler do the check and perform the action.
Busy-waiting is almost always the wrong solution.
您需要做的就是将事件处理程序附加到Activated
表单的事件。在该表单的构造函数中添加以下内容:
Activated += SomeMethod;
每当您在之前使用另一个应用程序后重新激活表单时,它就会被触发。
这种方法的主要优点是您避免创建一个新线程只是让它坐在那里做一个旋转等待(使用大量的 CPU 周期)。
如果您想使用这种方法,请注意您有一个竞争条件:其他人可能会Main.ActiveForm
在您的测试和Invoke()
呼叫之间设置为 null。这将导致异常。
在进行任何测试之前将变量复制到本地,以确保变量不能为空。
while(true)
{
var form = Main.ActiveForm;
if(form != null)
{
form.Invoke(new MethodInvoker(Main.SomeMethod));
break;
}
}
当你使用循环时,你是在浪费 CPU。更好的方法是使用事件:
// make event object in some shared place
var ev = new ManualResetEvent(false);
// do when form loaded
ev.Set();
// wait in thread
ev.WaitOne();
利用 :
while(Main.ActiveForm == null) { }
我会那样做。
while(Main.ActiveForm == null)
{
//maybe a sleep here ?!
}
Main.ActiveForm.Invoke(new MethodInvoker(Main.SomeMethod));