Anonymous Method
The anonymous method is. NET 2.0 introduces the advanced features, the word "anonymous" indicates that it can write the implementation in a method, so as to form a delegate object, without a clear local legal name, for example:
Static void Test ()
{
Action <string> action = delegate (string value)
{
Console. WriteLine (value );
};
Action ("Hello World ");
}
However, the key to the anonymous method is not only the word "anonymous. The most powerful feature is that the anonymous method forms a closure, which can be passed as a parameter to another method, but can also access the local variables of the method and other members of the current class. For example:
Class TestClass
{
Private void Print (string message)
{
Console. WriteLine (message );
}
Public void Test ()
{
String [] messages = new string [] {"Hello", "World "};
Int index = 0;
Action <string> action = (m) =>
{
This. Print (index ++) + "." + m );
};
Array. ForEach (messages, action );
Console. WriteLine ("index =" + index );
}
}
As shown above, in the Test method of TestClass, the action delegate calls the private method Print in the same TestClass class, and reads and writes the local variable index in the Test method. In addition to the new features of Lambda expressions in C #3.0, the use of anonymous methods has been greatly promoted. However, if used improperly, the anonymous method may also cause hard-to-detect problems.
Case Study
A brother recently read data from a text file in a simple data import program, analyzes and restructured the data, and then writes the data to the database. The logic is roughly as follows:
Static void Process ()
{
List <Item> batchItems = new List <Item> ();
Foreach (var item in ...)
{
BatchItems. Add (item );
If (batchItems. Count & gt; 1000)
{
DataContext db = new DataContext ();
Db. Items. InsertAllOnSubmit (batchItems );
Db. SubmitChanges ();
BatchItems = new List <Item> ();
}
}
}
After reading data from the data source each time, add the data to the batchItems List and submit the data once when there are 1000 batchItems logs. This code is running normally, but it is a pity that the time is stuck in the database submission. Data is obtained and processed quickly, but it takes a long time to submit the data. So think about it. There will be no resource conflicts between data submission and Data Processing. Then, put the data commit on another thread for processing! Therefore, ThreadPool is used to rewrite the Code:
Static void Process ()
{
List <Item> batchItems = new List <Item> ();
Foreach (var item in ...)
{
BatchItems. Add (item );
If (batchItems. Count & gt; 1000)
{
ThreadPool. QueueUserWorkItem (o) =>
{
DataContext db = new DataContext ();
Db. Items. InsertAllOnSubmit (batchItems );
Db. SubmitChanges ();
});
BatchItems = new List <Item> ();
}
}
}
Now, we hand over the data submission operation to ThreadPoll for execution. When there are additional threads in the thread pool, the data submission operation will be initiated. The data submission operation does not block data processing. Therefore, the data will be processed continuously according to the brother's intention. In the end, you only need to wait until all databases are submitted. The idea is good. Unfortunately, it is found that the code that runs normally (when multithreading is not used) will throw an exception "inexplicably. What's even more strange is that the data in the database is lost: 1 million pieces of data are processed and "submitted", but a portion of the database is missing. So I looked at the code from the left to the right, and I was puzzled.
Do you see the cause?
Cause Analysis
To identify the problem, we must understand how the anonymous method is implemented in the. NET environment.
There is no "anonymous Method" in. NET, and there are no similar new features. The "anonymous method" is completely magic played by the compiler. It includes all the Members to be accessed in the anonymous method in the closure to ensure that all member calls comply with. NET standards. For example, in the first section of the article, the 2nd example is actually changed to the following after being processed by the compiler (the natural field name is "friendly ):
Class TestClass
{
...
Private sealed class AutoGeneratedHelperClass
{
Public TestClass m_testClassInstance;
Public int m_index;
Public void Action (string m)
{
This. m_index ++;
This. m_testClassInstance.Print (m );
}
}
Public void TestAfterCompiled ()
{
AutoGeneratedHelperClass helper = new AutoGeneratedHelperClass ();
Helper. m_testClassInstance = this;
Helper. m_index = 0;
String [] messages = new string [] {"Hello", "World "};
Action <string> action = new Action <string> (helper. Action );
Array. ForEach (messages, action );
Console. WriteLine (helper. m_index );
}
}
From this we can see how the compiler implements a closure:
• The Compiler automatically generates a private internal helper class and sets it as sealed. The instance of this class will become a closure object.
• If the anonymous method needs to access the parameters or local variables of the method, the parameter or local variable will be "upgraded" to a public Field in the auxiliary class.
• If the anonymous method requires other methods in the classes Class, the current instance of the class will be saved in the auxiliary class.
It is worth mentioning that the above three theories may not be satisfied in actual situations. In some very simple cases (for example, anonymous methods do not involve local variables or other methods at all), the compiler simply generates a static method to construct a delegate instance, in this way, we can achieve better performance.
For the previous cases, we have also rewritten it to "Avoid" using anonymous objects and clearly show the cause of the problem:
Private class AutoGeneratedClass
{
Public List <Item> m_batchItems;
Public void WaitCallback (object o)
{
DataContext db = new DataContext ();
Db. Items. InsertAllOnSubmit (this. m_batchItems );
Db. SubmitChanges ();
}
}
Static void Process ()
{
Var helper = new AutoGeneratedClass ();
Helper. m_batchItems = new List <Item> ();
Foreach (var item in ...)
{
Helper. m_batchItems.Add (item );
If (helper. m_batchItems.Count & gt; 1000)
{
ThreadPool. QueueUserWorkItem (helper. WaitCallback );
Helper. m_batchItems = new List <Item> ();
}
}
}
The compiler automatically generates an AutoGeneratedClass class, and uses the instance of this class in the Process method to replace the original batchItems local variable. Similarly, the delegate object sent to ThreadPool is changed from an anonymous method to a public method of the AutoGeneratedClass instance. Therefore, the thread pool calls the WaitCallback method of the Instance each time.
Now the question should be clear at a glance? Every time a delegate is handed over to the thread pool, the thread pool will not be executed immediately, but it will be retained to the appropriate time before proceeding. When the WaitCallback method is executed, it reads the object referenced by the Field "current" m_batchItems. At the same time, the Process method has "abandoned" the data we originally wanted to submit, which will lead to the loss of data submitted to the database. At the same time, during the preparation of each batch of data, it is very likely that two data submissions will be initiated. When the two threads submit the same batch of items, the so-called "inexplicable" exception will be thrown.
Solve the problem
Finding the problem is easy to solve:
Private class WrapperClass
{
Private List <Item> m_items;
Public WrapperClass (List <Item> items)
{
This. m_items = items;
}
Public void WaitCallback (object o)
{
DataContext db = new DataContext ();
Db. Items. InsertAllOnSubmit (this. m_items );
Db. SubmitChanges ();
}
}
Static void Process ()
{
List <Item> batchItems = new List <Item> ();
Foreach (var item in ...)
{
BatchItems. Add (item );
If (batchItems. Count & gt; 1000)
{
ThreadPool. QueueUserWorkItem (
New WrapperClass (batchItems). WaitCallback );
BatchItems = new List <Item> ();
}
}
}
Here we explicitly prepare an encapsulation class to retain the data we need to submit. The reserved data will be used for each submission, so there will naturally be no "data sharing" that is not expected, thus avoiding the occurrence of Error 1.
Summary
The anonymous method is powerful, but it also creates some imperceptible traps. If the delegate created using the anonymous method is not executed immediately and the local variable of the method is used, you need to keep an eye on it. At this time, the "local variable" has actually been changed from the compiler to the Field on an instance of an automatic class, and this Field will be shared by the current method and the delegate object. If you modify the shared "local variables" after creating the delegate object, make sure that this meets your intention without causing problems.
This kind of problem will not only occur in anonymous methods. If you create an expression tree using a Lambda expression and use a "local variable", the expression tree also obtains the "current" value during parsing or execution, instead of the value when creating the Expression Tree.
This is why inline writing in Java-Anonymous classes-if you want to share "local variables" in the method, you must use the final keyword to modify the variables: in this way, this variable can only be assigned during declaration, avoiding the "odd problem" that may be caused by subsequent "modifications ".
Lao Zhao-pursuing the beauty of Programming