📜 ⬆️ ⬇️

Delegates and events in .NET

From the translator. Judging from my own experience, as well as from the experience of familiar fellow programmers, I can say that for a novice developer, among all the basic functions of the C # language and the .NET platform, delegates and events are among the most complex. Perhaps this is due to the fact that the need for delegates and events at first glance seems not obvious, or because of some confusion in terms. Therefore, I decided to translate the article by John Skit, telling about delegates and events at the most basic level, “on fingers”. It is ideal for those familiar with C # / .NET, but has difficulty understanding delegates and events.

The translation presented here is free. However, if the term “free”, as a rule, is understood to be abbreviated translation, with omissions, simplifications and retellings, here it is the opposite. This translation is a slightly expanded, refined and updated version of the original. I express my deep gratitude to Sergey Teplyakov, aka SergeyT , who made an invaluable contribution to the translation and design of this article.

People often have difficulty understanding the differences between events and delegates. And C # confuses the situation even more, as it allows you to declare field-like events that are automatically converted into a delegate variable with the same name. This article is intended to clarify this issue. Another point is the confusion with the term "delegate", which has several meanings. Sometimes it is used to designate the type of delegate (delegate type), and sometimes to designate an instance of the delegate (delegate instance). To avoid confusion, I will explicitly use these terms - the type of delegate and the delegate instance, and when I use the word "delegate" - that means I’m talking about them in the broadest sense.

Types of delegates


In a sense, you can think of a delegate type as some kind of interface, in which only one method is defined with a clearly defined signature (in this article, under the signature of the method, I will understand all its input and output parameters (ref and out) parameters, and return value). Then the delegate instance is an object that implements this interface. In this understanding, having a delegate instance, you can call any existing method, the signature of which will match the signature of the method defined in the “interface”. Delegates have other functionality, but the ability to make method calls with predefined signatures is the very essence of delegates. The delegate instance stores a reference (pointer, label) to the target method and, if this method is an instance method, then a reference to the object instance (class or structure) in which the target method is located.
')
The delegate type is declared using the delegate keyword. The types of delegates can exist as independent entities, or be declared within classes or structures. For example:

 namespace DelegateArticle { public delegate string FirstDelegate (int x); public class Sample { public delegate void SecondDelegate (char a, char b); } } 

In this example, two types of delegate are declared. The first is DelegateArticle.FirstDelegate , which is declared at the namespace level. It is “compatible” with any method that has one parameter of type int and returns a value of type string . The second is DelegateArticle.Sample.SecondDelegate , which is already declared inside the class and is a member of it. It is “compatible” with any method that has two parameters of type char and returns nothing, since the return type is marked as void .

Note that both delegate types have a public access public . In general, with respect to access modifiers, delegate types behave in the same way as classes and structures. If the access modifier is not explicitly specified for the delegate type and this type is declared within the namespace, then it will be available for all objects that are also within this namespace. If the delegate type without a modifier is declared inside a class or structure, it will be closed, similar to the action of the private modifier.

When declaring a delegate type, you cannot use the static modifier.

But remember that the delegate keyword does not always mean a delegate type declaration. The same keyword is used when instantiating delegates when using anonymous methods.

Both delegate types declared in this example are inherited from System.MulticastDelegate , which, in turn, is inherited from System.Delegate . In practice, consider inheritance only from MulticastDelegate - the difference between Delegate and MulticastDelegate lies primarily in the historical aspect. These differences were significant in beta versions of .NET 1.0, but this was inconvenient, and Microsoft decided to merge the two types into one. Unfortunately, the decision was made too late, and when it was made, they did not dare to make such a serious change affecting the basis of .NET. Therefore, assume that Delegate and MulticastDelegate are one and the same.

Each delegate type you create inherits members from MulticastDelegate , namely: one constructor with the Object and IntPtr parameters, as well as three methods: Invoke , BeginInvoke and EndInvoke . We will return to the constructor a bit later. Actually, these three methods are not inherited in the literal sense, since their signature for each type of delegate is different - it “adjusts” to the signature of the method in the declared type of delegate. Looking at the code sample above, we derive the “inherited” methods for the first type of delegate FirstDelegate :

 public string Invoke (int x); public System.IAsyncResult BeginInvoke(int x, System.AsyncCallback callback, object state); public string EndInvoke(IAsyncResult result); 

As you can see, the return type of the Invoke and EndInvoke matches the one specified in the delegate signature, as well as the parameter of the Invoke method and the first parameter of BeginInvoke . We will look at the purpose of the Invoke method later in the article, while BeginInvoke and EndInvoke will look at the section on advanced use of delegates . It is still too early to talk about this, since we still don’t even know how to create instances of delegates. We will talk about this in the next section.

Delegate Copies: Basics


We now know how the delegate type is declared and what it contains, so let's take a look at how to create an instance of the delegate and what can be done with it.

Creating delegate instances

First of all, I’ll note that this article does not cover the new functionality of C # 2.0 and 3.0 related to instantiating delegates, nor does it cover the generic delegates that appeared in C # 4.0. My separate article about the closures of “ The Beauty of Closures ” tells about the new features of delegates that appeared in C # 2.0 and 3.0; In addition, a lot of information on this topic is contained in Chapters 5, 9, and 13 of my book, C # in Depth . I will adhere to the explicit style of instantiating delegates that appeared in C # 1.0 / 1.1, as I believe that such a style is easier for understanding what is going on under the hood. That's when you comprehend the basics, you can begin to learn new features from C # 2.0, 3.0 and 4.0; and vice versa, without a firm understanding of the fundamentals outlined in this article, the “new” delegates' functional can be overwhelming for you.

As mentioned earlier, each delegate instance necessarily contains a reference to the target method that can be invoked through this delegate instance, and a reference to the object instance (class or structure) in which the target method is declared. If the target method is static, then naturally there is no reference to the instance. The CLR also supports other, slightly different forms of delegates, where the first argument passed to the static method is stored in the delegate instance, or the reference to the target instance method is passed as an argument when the method is called. More information about this can be found in the documentation for System.Delegate on MSDN, but now, at this stage, this additional information is not significant.

So, we know that to create an instance we need two “units” of data (well, and the type of delegate itself, of course), but how to let the compiler know about it? We use what is referred to in the C # specification as “ delegate-creation-expression ”, which is one of the new delegate-type (expression) forms. An expression must either be a different delegate with the same type (or a compatible delegate type in C # 2.0), or the “method group”, which consists of the name of the method and an optional reference to the object instance. The group of methods is specified in the same way as the usual method call, but without any arguments and parentheses. The need to create delegate copies is quite rare, so we will focus on more general forms. Examples below.

 /*      d1  d2 .  InstanceMethod   ,    ,       ( ). ,     — this,      . */ FirstDelegate d1 = new FirstDelegate(InstanceMethod); FirstDelegate d2 = new FirstDelegate(this.InstanceMethod); /*  (d3)    ,     ,      ,        . */ FirstDelegate d3 = new FirstDelegate(anotherInstance.InstanceMethod); /*   (d4)      ,  ,     ;        . */ FirstDelegate d4 = new FirstDelegate(instanceOfOtherClass.OtherInstanceMethod); /*    (d5)     ,      ,     ( ). */ FirstDelegate d5 = new FirstDelegate(StaticMethod); /*  (d6)      ,       . */ FirstDelegate d6 = new FirstDelegate(OtherClass.OtherStaticMethod); 

The delegate constructor, which we talked about earlier, has two parameters - a reference to a method called of type System.IntPtr (in MSDN documentation this parameter is called method) and a reference to an instance of an object of type System.Object (in MSDN documentation this parameter is called target), which is null if the method specified in the method parameter is static.

An important note needs to be made: delegate instances can refer to methods and instances of objects that will be invisible (out of scope) in relation to the place in the code where the delegate instance will be called . For example, when creating a delegate instance, a private method can be used, and then this delegate instance can be returned from another public (public) method or property. On the other hand, an instance of an object specified when creating an instance of a delegate may be an object that, when called, will be unknown with respect to the object in which the call was made. The important thing is that both the method and the object instance must be accessible (be in scope) at the time the delegate is instantiated. In other words, if (and only if) in the code you can create an instance of a specific object and call a specific method from that instance, then you can use this method and an object instance to create an instance of the delegate. But when calling a previously created delegate instance, the access rights and scope are ignored. Speaking of challenges ...

Call delegate instances

Instances of delegates are called in the same way as regular methods are called. For example, invoking an instance of delegate d1, whose type is defined at the very top as delegate string FirstDelegate (int x) , would be as follows:
 string result = d1(10); 

The method referenced by the delegate instance is called “within” (or “in context”, if in other words) of the object instance, if any, after which the result is returned. Writing a full-fledged program that demonstrates the work of the delegates, and at the same time compact, not containing “extra” code, is not an easy task. However, below is a similar program containing one static and one instance method. Calling DelegateTest.StaticMethod equivalent to calling StaticMethod — I included the class name to make the example more understandable.

 using System; public delegate string FirstDelegate (int x); class DelegateTest { string name; static void Main() { FirstDelegate d1 = new FirstDelegate(DelegateTest.StaticMethod); DelegateTest instance = new DelegateTest(); instance.name = "My instance"; FirstDelegate d2 = new FirstDelegate(instance.InstanceMethod); Console.WriteLine (d1(10)); //    "Static method: 10" Console.WriteLine (d2(5)); //    "My instance: 5" } static string StaticMethod (int i) { return string.Format ("Static method: {0}", i); } string InstanceMethod (int i) { return string.Format ("{0}: {1}", name, i); } } 

The C # syntax on invoking delegate instances is syntactic sugar that masks the invocation of the Invoke method that each delegate type has. Delegates can run asynchronously if they provide BeginInvoke/EndInvoke , but more on that later .

Combining Delegates

Delegates can be combined (combined and subtracted) in such a way that when you call a single delegate instance, a whole set of methods are called, and these methods can be from different instances of different classes. When I said earlier that a delegate instance stores references to a method and an object instance, I simplified it a little. This is true for instances of delegates that represent one method. For clarity, hereinafter I will call such instances of delegates "simple delegates" (simple delegate). In contrast, there are instances of delegates that are actually lists of simple delegates, all of which are based on the same type of delegate (i.e., have the same signature of the methods referenced). I will call such instances of delegates “combined delegates”. Several combined delegates can be combined with each other, actually becoming one big list of simple delegates. The list of simple delegates in a combined delegate is called the “call list” or “action list” (invocation list). Thus, a call list is a list of pairs of references to methods and instances of objects that (pairs) are arranged in the order of the call.

It is important to know that delegate instances are always immutable. Each time when instances of delegates are merged (and also when subtracting — we will look at this below), a new combined delegate is created. String.PadLeft as with strings: if you apply String.PadLeft to an instance of a string, the method does not change this instance, but returns a new instance with the changes made.

The union (also known as the term “addition”) of two instances of delegates is usually done using the addition operator, as if the instances of delegates were numbers or strings. Similarly, subtraction (the term “deletion” also occurs) of one delegate instance from another is performed using the subtraction operator. Note that when subtracting one combined delegate from another, subtraction is done as part of the call list. If the original (reduced) call list does not contain one of those simple delegates that are in the subtracted call list, then the result of the operation (difference) will be the original list. Otherwise, if there are simple delegates in the original list, which are also present in the subtracted, then only the latest entries of simple delegates will be missing from the resulting list. However, it is easier to show with examples than to describe in words. But instead of the next source code, I will demonstrate the work of combining and subtracting using the example of the following table. In it, the literals d1, d2, d3 denote simple delegates. Further, the designation [d1, d2, d3] implies a combined delegate, which consists of three simple in exactly this order, i.e. when called, d1 will be called first, then d2, and then d3. An empty call list is represented by null.
ExpressionResult
null + d1d1
d1 + nulld1
d1 + d2[d1, d2]
d1 + [d2, d3][d1, d2, d3]
[d1, d2] + [d2, d3][d1, d2, d2, d3]
[d1, d2] - d1d2
[d1, d2] - d2d1
[d1, d2, d1] - d1[d1, d2]
[d1, d2, d3] - [d1, d2]d3
[d1, d2, d3] - [d2, d1][d1, d2, d3]
[d1, d2, d3, d1, d2] - [d1, d2][d1, d2, d3]
[d1, d2] - [d1, d2]null

In addition to the addition operator, delegate instances can be combined using the static Delegate.Combine method; similarly to it, the subtraction operation has an alternative in the form of the static method Delegate.Remove . Generally speaking, addition and subtraction operators are a kind of syntactic sugar, and the C # compiler, meeting them in code, replaces calls to the Combine and Remove methods. And precisely because these methods are static, they easily handle null instances of delegates.

The addition and subtraction operators always work as part of the assignment operation d1 += d2 , which is completely equivalent to the expression d1 = d1+d2 ; the same for subtraction. Again, recall that the instances of delegates involved in addition and subtraction do not change during the operation; in this example, the variable d1 simply replaces the reference to the newly created combined delegate consisting of the “old” d1 and d2.

Note that adding and deleting delegates occurs at the end of the list, so the sequence of calls is x + = y; x - = y; equivalent to an empty operation (the variable x will contain an unchanged list of subscribers, approx. transl. ).

If the delegate type signature is declared such that it returns a value (that is, the return value is not void) and a combined delegate instance is created “based on” this type, then when it is called, the return value “provided” by the last simple delegate will be written to the variable. in the call list of the combined delegate.

If there is a combined delegate (containing a list of calls consisting of a set of simple delegates), and when it is called in some simple delegate, an exception occurs, then the call of the combined delegate will stop, the exception will be forwarded, and all other simple delegates from the call list never be called.

Developments


First of all: events (event) are not instances of delegates. And now again:
Events are NOT delegate instances.

In a sense, it is a pity that the C # language allows you to use events and delegate instances in certain situations in the same way, but it is very important to understand the difference.

I came to the conclusion that the best way to understand events is to think of them as “as if” properties. Properties, although they look like “type as” fields (fields), in fact, they are definitely not - you can create properties that do not use fields at all. Events behave in a similar way - although they look like instances of delegates in terms of addition and subtraction operations, but in fact they are not.

Events are pairs of methods that are appropriately “styled” in IL (CIL, MSIL) and related to each other so that the language environment clearly knows that it “deals” not with “simple” methods, but with methods that represent events. The methods correspond to the add (add) and remove (remove) operations, each of which takes one parameter with an instance of a delegate that has a type that is the same as the event type. What you will do with these operations is largely up to you, but usually add and delete operations are used to add and remove delegate instances to / from the list of event handlers. When an event is triggered (and no matter what caused the trigger — click on a button, a timer, or an unhandled exception), the call to the handlers occurs (one by one). Be aware that in C #, calling event handlers is not part of the event itself.

The add and remove methods are called in C # eventName += delegateInstance;and so eventName -= delegateInstance;, where it eventNamecan be specified by reference to an object instance (for example myForm.Click) or by type name (for example MyClass.SomeEvent). However, static events are quite rare.

Events themselves can be announced in two ways. The first method is with an explicit implementation of the add and remove methods; This method is very similar to properties with explicitly declared getters (get) and setters (set), but with the keyword event. The following is an example of a property for a delegate type.System.EventHandler. Note that in the add and remove methods, there are no operations with delegate instances that are passed there - the methods simply output to the console that they were called. If you execute this code, you will see that the remove method will be invoked, despite the fact that we passed it a null value for deletion.

 using System; class Test { public event EventHandler MyEvent //  MyEvent   EventHandler { add { Console.WriteLine ("add operation"); } remove { Console.WriteLine ("remove operation"); } } static void Main() { Test t = new Test(); t.MyEvent += new EventHandler (t.DoNothing); t.MyEvent -= null; } //-,        EventHandler void DoNothing (object sender, EventArgs e) { } } 

The moments when it is necessary to ignore the obtained value valueappear quite rarely. And although there are very few cases where we can ignore the value passed in this way, there are times when we cannot use the simple delegate variable to keep subscribers. For example, if a class contains many events, but subscribers will use only some of them, we can create an associative array, the key of which will be the description of the event, and the value - the delegate with its subscribers. This technique is used in Windows Forms - i.e. A class can contain a huge number of events without unnecessary use of memory for variables, which in most cases will be equal to null.

Field-like events


C # provides an easy way to declare a delegate variable and an event at the same time. This method is called a "field-like event" (field-like event) and is declared very simply - just like the "long" form of event declaration (given above), but without the "body" with the add and remove methods.
 public event EventHandler MyEvent; 

This form creates a delegate variable and an event with the same type. Access to an event is defined in an event declaration using an access modifier (thus, in the example above, a public event is created), but the delegate variable is always private. The implicit body of the event is deployed by the compiler into quite obvious operations of adding and deleting delegate instances to / from the delegate variable, and these actions are performed under lock. For C # 1.1, the event MyEventfrom the example above is equivalent to the following code:

 private EventHandler _myEvent; public event EventHandler MyEvent { add { lock (this) { _myEvent += value; } } remove { lock (this) { _myEvent -= value; } } } 

This is for the instance members. As for static events, the variable is also static and the lock is captured on the type of the view.typeof(XXX)where XXX is the name of the class in which the static event is declared. C # 2.0 makes no guarantees about what is used to capture locks. It says only that the only object associated with the current instance is used to block instance events, and the only object associated with the current class to block static events. (Note that this is true only for events declared in classes, but not in structures. There are problems with blocking events in structures; and in practice I don’t remember a single example of a structure in which an event was announced.) But none of this not as useful as you might think, for details see the section on multithreading .

So, what happens when you refer toMyEvent? Inside the body of the type itself (including nested types), the compiler generates code that refers to the delegate variable ( _myEventin the example above). In all other contexts, the compiler generates code that refers to an event.

What's the point of this?


Now that we know both about delegates and about events, a completely natural question arises: why do we need both in the language? The answer is due to encapsulation. Suppose that in some fictional C # /. NET event does not exist. How then can a third-party class subscribe to an event? Three options:
  1. Public variable (field) with delegate type.
  2. Private variable (field) with a delegate type with a wrapper in the form of a public property.
  3. Private variable (field) with delegate type with public methods AddXXXHandler and RemoveXXXHandler.

Option number 1 is terrible - we usually hate public fields. Option # 2 is slightly better, but allows subscribers to effectively override (override) one single thing - it will be too easy to write an expression someInstance.MyEvent = eventHandler;, as a result of which all existing handlers will be replaced with eventHandler, instead of adding to existing ones eventHandler. Plus, you still need to explicitly set the property code.

Option number 3 is, in fact, what the events provide to you, but with a guaranteed agreement (generated by the compiler and reserved by special flags in IL) and with a “free” implementation, if only you are happy with the semantics of field-like events. Subscribing and unsubscribing to / from events is encapsulated without providing random access to the list of event handlers, which makes it possible to simplify the code for subscription and unsubscribe operations.

Thread safe events


(Note: with the release of C # 4, this section is somewhat outdated. From the translator: see details in the section “ From the translator ”)

Earlier, we touched upon the blocking (locking) that occurs in field-like events during add and remove operations that are automatically implemented by the compiler . This is done in order to provide some kind of thread safety guarantee. Unfortunately, it is not so useful. First of all, even in the C # 2.0 specifications, you can set a lock on a link to this object or to the type itself in static events. This is contrary to the principles of blocking on private links, which is necessary to prevent deadlocks.

Ironically, the second problem is the exact opposite of the first - due to the fact that in C # 2.0 you cannot guarantee which lock will be used, you also cannot use it when you trigger an event to make sure that you see the newest ( actual) value in this stream. You can use a lock on something else or use special methods that work with memory barriers, but all this leaves an unpleasant aftertaste 1 ↓ .

If you want your code to be truly thread-safe, such that when you raise an event, you always use the most current value of the delegate variable, and also so that you can make sure that add / remove operations do not interfere with one another, then to achieve You need to write the body of the add / remove operations yourself for such a “reinforced concrete” thread safety. Example below:

 /// <summary> ///    SomeEventHandler,  «» . /// </summary> SomeEventHandler someEvent; /// <summary> ///       SomeEvent. /// </summary> readonly object someEventLock = new object(); /// <summary> ///   /// </summary> public event SomeEventHandler SomeEvent { add { lock (someEventLock) { someEvent += value; } } remove { lock (someEventLock) { someEvent -= value; } } } /// <summary> ///   SomeEvent /// </summary> protected virtual void OnSomeEvent(EventArgs e) { SomeEventHandler handler; lock (someEventLock) { handler = someEvent; } if (handler != null) { handler (this, e); } } 

You can use a single lock for all of your events, and even use this lock for something else — this already depends on the specific situation. Note that you need to “write” the current value to a local variable inside the lock (in order to get the most current value), then check this value to null and execute outside the lock: holding the lock during the event call is a very bad idea. easily leading to a deadlock. To explain this, imagine that an event handler must wait for another thread to do some of its work, and if during that time another thread causes an add / remove operation for your event, then you will get a deadlock.

The above code works correctly because once the local variable handleris assigned the value someEvent, the value of the handler will not change even if it changes itself someEvent. If all event handlers unsubscribe from the event, then the call list will be empty, someEventwill become null, but handlerwill store its value, which will be what it was at the time of the assignment. In fact, delegate instances are immutable, so any subscribers who subscribe between assignment ( handler = someEvent) and event ( handler (this, e);) call will be ignored.

In addition, you need to determine whether you need thread safety at all. Are you going to add and remove event handlers from other threads? Are you going to trigger events from different threads? If you are in complete control of your application, then the very no and very easy-to-implement answer will be. If you are writing a class library, then most likely providing thread safety will come in handy. If you definitely do not need thread safety, then it is a good idea to self-implement the body of add / remove operations so that they do not explicitly use locks; after all, as we remember, C # when autogenerating these operations uses its “own” “wrong” locking mechanism. In this case, your task is very simple. Below is an example of the above code, but without thread safety.

 /// <summary> ///    SomeEventHandler,  «» . /// </summary> SomeEventHandler someEvent; /// <summary> ///   /// </summary> public event SomeEventHandler SomeEvent { add { someEvent += value; } remove { someEvent -= value; } } /// <summary> ///   SomeEvent /// </summary> protected virtual void OnSomeEvent(EventArgs e) { if (someEvent != null) { someEvent (this, e); } } 

If at the time of the method call the OnSomeEventdelegate variable someEventdoes not contain a list of delegate instances (due to the fact that they were not added via the add method or deleted via the remove method), then the value of this variable will be null and to avoid calling it with this value, and A check for null has been added. This situation can be solved in a different way. You can create an instance of the delegate stub (no-op) that will be bound to the “default” variable and will not be deleted. In this case, in the method OnSomeEventyou just need to get and call the value of the delegate variable. If the “real” instances of the delegates have not been added, then the stub will simply be called.

Delegate Instances: Other Methods


Earlier in the article, I showed that a call someDelegate(10)is just an abbreviation for a call someDelegate.Invoke(10). In addition Invoke, delegate types have asynchronous behavior through a pair of methods BeginInvoke/ EndInvoke. In the CLI, they are optional, but in C # they are always there. They follow the same asynchronous execution model as the rest of .NET, allowing you to specify a callback handler along with an object storing state information. As a result of an asynchronous call, the code is executed in threads created by the system and located in the thread pool (thread-pool) .NET.

In the first example, shown below 2 ↓ , there are no callbacks, they are simply used here BeginInvokeandEndInvokein one thread. Such a code pattern is sometimes useful when one thread is used for operations that are generally synchronous, but at the same time contains elements that can be executed in parallel. For the sake of simplicity of the code, all the methods in the example are static, but you can, of course, use "asynchronous" delegates along with instance methods, and in practice this will happen even more often. The method EndInvokereturns the value that is returned as a result of invoking the delegate instance. If an exception is raised during the invocation of the delegate instance, this exception will be thrown and EndInvoke.

 using System; using System.Threading; delegate int SampleDelegate(string data); class AsyncDelegateExample1 { static void Main() { SampleDelegate counter = new SampleDelegate(CountCharacters); SampleDelegate parser = new SampleDelegate(Parse); IAsyncResult counterResult = counter.BeginInvoke("hello", null, null); IAsyncResult parserResult = parser.BeginInvoke("10", null, null); Console.WriteLine("   ID = {0}  ", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("  '{0}'", counter.EndInvoke(counterResult)); Console.WriteLine("  '{0}'", parser.EndInvoke(parserResult)); Console.WriteLine("   ID = {0} ", Thread.CurrentThread.ManagedThreadId); } static int CountCharacters(string text) { Thread.Sleep(2000); Console.WriteLine("    '{0}'    ID = {1}", text, Thread.CurrentThread.ManagedThreadId); return text.Length; } static int Parse(string text) { Thread.Sleep(100); Console.WriteLine("  '{0}'    ID = {1}", text, Thread.CurrentThread.ManagedThreadId); return int.Parse(text); } } 

Method calls Thread.Sleepare inserted only in order to demonstrate that the methods CountCharactersand Parseactually carried out in parallel with the main flow. A sleep of CountCharacters2 seconds is large enough to force the thread pool to perform tasks in other threads — the thread pool serializes queries that do not take much time to execute, so as to avoid excessive creation of new threads (creating new threads is a relatively resource-intensive operation) . By putting the stream to sleep for a long time, we thus imitate a “heavy”, time-consuming task. And here is the output of our program:

	Main thread with ID = 9 continues
	Parsing the string '10' in the stream with ID = 10
	Counting characters in the string 'hello' in the stream with ID = 6
	Counter returned '5'
	Parser returned '10'
	Main thread with id = 9 terminated

If the process of executing a delegate in a third-party thread has not yet completed, then the method call EndInvokein the main thread will have a similar effect to the call Thread.Joinfor manually created threads - the main thread will wait until the task is completed in the third-party thread. The value IAsyncResultthat is returned by the method BeginInvokeand passed to the input EndInvokecan be used to transfer the state from BeginInvoke(through the last parameter - Object state), however, the need for such a transfer when using delegates does not often occur.

The code above is fairly simple, but not efficient enough compared to the callback model that is called after the delegate has completed its execution. As a rule, it is in the callback method EndInvokethat the method that returns the result of delegate execution is called. Although theoretically, this call still blocks the main thread (as in the above example), but in practice the thread will not be blocked, since the callback method is executed only when the delegate has completed its task. The callback method can use a state with some additional data that will be passed to it from the methodBeginInvoke. The example below uses the same delegates to parse and count the number of characters per line as in the example above, but this time with a callback method in which the result is output to the console. The statetype parameter is Objectused to send information about the format in which to output the result to the console, and because of this we can use the same callback method DisplayResultto handle both asynchronous delegate calls. Note the cast IAsyncResultto type AsyncResult: value received by the callback is always the instance AsyncResult, and through it we can get the original copy of the delegate, the result of which is obtained using EndInvoke. Here, a little weird is that the type is AsyncResultdeclared in the namespaceSystem.Runtime.Remoting.Messaging(which you need to connect), while all other types are declared in Systemor System.Threading.

 using System; using System.Threading; using System.Runtime.Remoting.Messaging; delegate int SampleDelegate(string data); class AsyncDelegateExample2 { static void Main() { SampleDelegate counter = new SampleDelegate(CountCharacters); SampleDelegate parser = new SampleDelegate(Parse); AsyncCallback callback = new AsyncCallback(DisplayResult); counter.BeginInvoke("hello", callback, "  '{0}'    ID = {1}"); parser.BeginInvoke("10", callback, "  '{0}'    ID = {1}"); Console.WriteLine("   ID = {0}  ", Thread.CurrentThread.ManagedThreadId); Thread.Sleep(3000); Console.WriteLine("   ID = {0} ", Thread.CurrentThread.ManagedThreadId); } static void DisplayResult(IAsyncResult result) { string format = (string)result.AsyncState; AsyncResult delegateResult = (AsyncResult)result; SampleDelegate delegateInstance = (SampleDelegate)delegateResult.AsyncDelegate; Int32 methodResult = delegateInstance.EndInvoke(result); Console.WriteLine(format, methodResult, Thread.CurrentThread.ManagedThreadId); } static int CountCharacters(string text) { Thread.Sleep(2000); Console.WriteLine("    '{0}'    ID = {1}", text, Thread.CurrentThread.ManagedThreadId); return text.Length; } static int Parse(string text) { Thread.Sleep(100); Console.WriteLine("  '{0}'    ID = {1}", text, Thread.CurrentThread.ManagedThreadId); return int.Parse(text); } } 

This time almost all work is done in threads from the thread pool. The main thread simply initiates asynchronous tasks and “falls asleep” until all these tasks are completed. All threads from the thread pool are background (background) threads that cannot “hold” the application (i.e., they cannot prevent it from closing), and so that the application does not terminate before the delegates work in the background threads, we and applied the call Thread.Sleep(3000)in the main thread - you can hope that 3 seconds will be enough to complete and complete the delegates. You can check this by commenting out the line Thread.Sleep(3000)- the program will end almost instantly after launch.

The result of our program is presented below. Pay attention to the order of results output to the console - the result of the parser appeared before the result of the counter, as the environment does not guarantee the preservation of order when called EndInvoke. In the previous example, the parsing was completed much faster (100 ms) than the counter (2 seconds), but the main thread waited for both of them to display, first of all, the result of the counter, and only then the parser.

	Main thread with ID = 9 continues
	Parsing the string '10' in the stream with ID = 11
	The parser returned '10' in the stream with ID = 11
	Counting characters in the 'hello' string in the stream with ID = 10
	The counter returned '5' in the stream with ID = 10
	Main thread with id = 9 terminated

Remember that when using this asynchronous model you must call EndInvoke; this is necessary in order to guarantee the absence of memory leaks and handlers. In some cases, in the absence of EndInvokeleaks may not be, but do not hope so. For more information, you can refer to my article “ Multi-threading in .NET: Introduction and Suggestions ” on multi-threading, in particular, to the “ The Thread Pool and Asynchronous Methods ” section.

Conclusion


Delegates provide an easy way to call methods based on instances of objects and with the ability to transfer some data. They are the basis for events, which themselves are an effective mechanism for adding and removing handlers to be called at an appropriate time.

Notes


1. Approx. trans.I admit, John Skyte's explanation is rather vague and crumpled. To understand in detail why locking on the current instance and type is bad, and why you should enter a separate read-only private field, I highly recommend using the book "CLR via C #" written by Jeffrey Richter, which has already gone through 4 editions. If we talk about the second edition of 2006, translated into 2007 into Russian, then the information on this problem is located in “Part 5. CLR Tools” - “Chapter 24. Flow Synchronization” - the section “Why the“ great ”idea was so unsuccessful ".

2. Approx. trans.This and the following code samples, as well as their output to the console, were slightly modified compared to the original examples from J. Skit. In addition to the translation, I added the output of thread identifiers so that you can clearly see which code is running in which thread.


From translator


Despite the rather large size of the article, one cannot but agree that the topic of delegates and events is much more extensive, complex and multifaceted. However, a hypothetical article that fully describes delegates and events would have a size similar to the size of an average book. Therefore, I provide links to the most useful articles on the topic, and those that complement this article as harmoniously as possible.

Alexey Dubovtsev. Delegates and Events (RSDN).
Although the article is not new (dated 2006) and considers only the basics of delegates and events, the level of “reviewing the basics” is much deeper: here you can look more closely at the type of MulticastDelegate, especially in terms of combined delegates, and describe the principle of working at the MSIL level, and the description of EventHandlerList, and more. In general, if you want to consider the basics of delegates and events at a deeper level, then this article is definitely for you.

coffeecupwinner . .NET events in detail .
I hope you paid attention to the article about outdated material at the beginning of the section " Thread-safe events"? In C # 4, the internal implementation of field-like events, which Skyt and Richter criticize, has been completely reworked: now thread safety is implemented through Interlocked.CompareExchange, without any blocking. About this, among other things, and tells this article. In general, the article meticulously examines only events, but at a much deeper level than that of John Skeet.

Daniel Grunwald. Andreycha . Weak events in C # .
When talking about the benefits between C # /. NET on the one hand and C ++ on the other, the advantage of the first among other things, is recording automatic garbage collection, which eliminates memory leaks as a class of errors. However, not everything is so rosy: events can lead to memory leaks, and this very detailed article is devoted to the solution of these problems.

rroyter . Why do we need delegates in C #?
As I mentioned in the introduction, novice developers have misunderstandings with delegates due to the lack of visible reasons requiring their use. This article very lucidly demonstrates some situations where delegates will be extremely relevant. In addition, the delegates introduced new features introduced in C # 2 and 3 versions.

Source: https://habr.com/ru/post/198694/


All Articles