-1

Edit a few years on: this was clearly a terrible approach, at the start of me using C#/.NET. Hopefully this question helps another noob with the same "problem".

Is this the best way to approach this scenario?

while(true)
{
    if(Main.ActiveForm != null)
    {
        Main.ActiveForm.Invoke(new MethodInvoker(Main.SomeMethod));
        break;
    }
}

This is performed on a second thread.

4

6 に答える 6

37

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.

于 2013-01-21T15:50:59.907 に答える
9

All you need to do is attach an event handler to the Activated event of your form. Add the following inside that form's constructor:

Activated += SomeMethod;

And it will be fired whenever you re-activate the form after previously using another application.

The primary advantage of this approach is that you avoid creating a new thread just to have it sitting around doing a spinwait (using up a lot of CPU cycles).

于 2013-01-21T15:41:23.487 に答える
3

If you want to use this approach, note that you have a race condition: someone else might set Main.ActiveForm to null between your test and your Invoke() call. That would result in an exception.

Copy the variable locally before doing any tests to make sure that the variable cannot be made null.

while(true)
{
    var form = Main.ActiveForm;

    if(form != null)
    {
        form.Invoke(new MethodInvoker(Main.SomeMethod));
        break;
    }
}
于 2013-01-21T15:26:56.120 に答える
3

When you use a loop You are waste CPU. The beter way to do this is use events:

// make event object in some shared place
            var ev = new ManualResetEvent(false);

// do when form loaded
            ev.Set();

// wait in thread
            ev.WaitOne();
于 2013-01-21T15:28:28.187 に答える
2

use :

while(Main.ActiveForm == null) { }
于 2013-01-21T15:25:52.480 に答える
1

I would do it like that.

while(Main.ActiveForm == null)
{
    //maybe a sleep here ?!
}
Main.ActiveForm.Invoke(new MethodInvoker(Main.SomeMethod));
于 2013-01-21T15:28:07.360 に答える