C # multi-threaded programming (2): Thread Synchronization

Source: Internet
Author: User
The first article in the multi-thread programming series describes how to start a thread. This article describes how to ensure synchronization and avoid deadlocks when there is competition between threads.

The first article in the multi-thread programming series describes how to start a thread. This article describes how to ensure synchronization and avoid deadlocks when there is competition between threads.

Problems caused by thread non-synchronization

Let's make a hypothesis that there are 100 tickets and two threads are used to implement a ticket sales program. When each thread is running, first check whether there are tickets not sold, if the minimum number of tickets is sold in the ascending order of the number, the code of the program is as follows:


Using System;
Using System. Collections. Generic;
Using System. Text;
Using System. Threading;
Namespace StartThread
{
Public class ThreadLock
{
Private Thread threadOne;
Private Thread threadTwo;
Private List ticketList;
Private object objLock = new object ();
Public ThreadLock ()
{
ThreadOne = new Thread (new ThreadStart (Run ));
ThreadOne. Name = "Thread_1 ";
ThreadTwo = new Thread (new ThreadStart (Run ));
ThreadTwo. Name = "Thread_2 ";
}
Public void Start ()
{
TicketList = new List (100 );
For (int I = 1; I <= 100; I ++)
{
TicketList. Add (I. ToString (). PadLeft (3, '0'); // implement the three-digit ticket number. If there are less than three digits, the three digits are supplemented with 0.
}
ThreadOne. Start ();
ThreadTwo. Start ();
}
Private void Run ()
{
While (ticketList. Count> 0) // ①
{
String ticketNo = ticketList [0]; // ②
Console. WriteLine ("{0}: sold a ticket, ticket No.: {1}", Thread. CurrentThread. Name, ticketNo );
TicketList. RemoveAt (0); // ③
Thread. Sleep (1 );
}
}
}
}

The execution effect of this program is not the same every time. It is the result of a certain running:

  

It can be seen that the ticket number 001 has been sold twice (if you meet the role played by Nicholas Tse in promise, it may lead to another example of "a vote-caused bloody case", haha). Why is this happening?

See Code ③:


TicketList. RemoveAt (0); // ③

In a certain situation, it is possible that thread 1 is just running here. It extracts the element with an index of 0 from the ticketList and outputs the ticket number. Unfortunately, the time slice allocated to thread 1 has been used up, thread 1 enters the sleep state, and thread 2 starts from the beginning. It can calmly extract the element with an index of 0 from the ticketList and output it, when thread 1 is executed, although the element whose index is 0 in the ticketList is output, it is too late to delete it. Therefore, the value obtained by thread 2 is the same as that obtained by the previous thread 1 at this time, in this case, some tickets are sold twice, and some tickets may not be sold at all.

The root cause of this situation is that multiple threads operate on the same resource, so we should avoid this situation as much as possible in multi-threaded programming. Of course, in some cases, this situation cannot be avoided, this requires some measures to ensure that this situation does not occur. This is the so-called thread synchronization.

There are several methods to implement thread synchronization in C #: lock, Mutex, Monitor, Semaphore, Interlocked, and ReaderWriterLock. The synchronization policy can also be divided into synchronization context, synchronization code area, and manual synchronization.

Synchronization Context

The synchronization context policy is implemented mainly by the SynchronizationAttribute class. For example, the following code is a class that implements context synchronization:


Using System;
Using System. Collections. Generic;
Using System. Text;
// You need to add reference to the class library System. EnterpriseServices. dll to use this dll
Using System. EnterpriseServices;
Namespace StartThread
{
[Synchronization (SynchronizationOption. Required)] // make sure that the created object has been synchronized.
Public class SynchronizationAttributeClass
{
Public void Run ()
{
}
}
}

All objects in the same context domain share the same lock. In this way, the attributes, methods, and fields of the created object instance have thread security. Note that static fields, attributes, and methods of the class do not have thread security.

Synchronization Code Area

The Code Synchronization area is another method for synchronizing specific code.

Lock Synchronization

For the above code, you can use the lock keyword to implement it without any confusion (the same ticket is sold twice or some tickets are not sold at the root, the problematic part is to determine whether the remaining number of votes is greater than 0. If the number is greater than 0, the maximum number of votes is subtracted from the current total number of votes. The Code is as follows:


Private void Run ()
{
While (ticketList. Count> 0) // ①
{
Lock (objLock)
{
If (ticketList. Count> 0)
{
String ticketNo = ticketList [0]; // ②
Console. WriteLine ("{0}: sold a ticket, ticket No.: {1}", Thread. CurrentThread. Name, ticketNo );
TicketList. RemoveAt (0); // ③
Thread. Sleep (1 );
}
}
}
}

After such processing, the system running result will be normal. The effect is as follows:

  

In general, the lock statement is an effective small code block synchronization method that does not span multiple methods, that is, the use of the lock statement can only be between some code of a method, methods cannot be crossed.

Monitor class

For the above Code, if the Monitor class is used for synchronization, the code will be as follows:


Private void Run ()
{
While (ticketList. Count> 0) // ①
{
Monitor. Enter (objLock );
If (ticketList. Count> 0)
{
String ticketNo = ticketList [0]; // ②
Console. WriteLine ("{0}: sold a ticket, ticket No.: {1}", Thread. CurrentThread. Name, ticketNo );
TicketList. RemoveAt (0); // ③
Thread. Sleep (1 );
}
Monitor. Exit (objLock );
}
}

Of course, the final running effect of this Code is the same as that of synchronization using the lock keyword. In comparison, we will find that the difference between using the lock keyword to keep synchronization is not big: "lock (objLock) {" changed to "Monitor. enter (objLock); ","} "is replaced with" Monitor. exit (objLock );". In fact, if you view the final generated IL code in other ways, you will find that the code using the lock keyword is actually implemented using Monitor.

The following code:


Lock (objLock ){
// Synchronize code
}
It is actually equivalent:
Try {
Monitor. Enter (objLock );
// Synchronize code
}
Finally
{
Monitor. Exit (objLock );
}

We know that the finally code block will be executed in most cases, so that the synchronization lock can be released even if the synchronization code encounters an exception.

In addition to the Enter () and Exit () methods, the Monitor class also has the Wait () and Pulse () methods. The Wait () method is to temporarily release the current live lock and block the current object. The Pulse () method is to notify that the waiting object is ready, it will release the lock later. The following two methods are used to complete a collaborative thread. One thread is responsible for randomly generating data, and the other is responsible for displaying the generated data. The following code is used:


Using System;
Using System. Collections. Generic;
Using System. Text;
Using System. Threading;
Namespace StartThread
{
Public class ThreadWaitAndPluse
{
Private object lockObject;
Private int number;
Private Random random;
Public ThreadWaitAndPluse ()
{
LockObject = new object ();
Random = new Random ();
}
// Display the method to be executed by the thread that generates data
Public void ThreadMethodOne ()
{
Monitor. Enter (lockObject); // get the object lock
Console. WriteLine ("currently in the Thread:" + Thread. CurrentThread. GetHashCode ());
For (int I = 0; I <5; I ++)
{
Monitor. Wait (lockObject); // release the object lock and stop the current thread
Console. WriteLine ("WaitAndPluse1: Work ");
Console. WriteLine ("WaitAndPluse1: The data is obtained. number =" + number + ", Thread ID =" + Thread. CurrentThread. GetHashCode ());
// Notify other objects waiting for the lock that their statuses have changed. After the lock is released, the objects waiting for the lock will be locked.
Monitor. Pulse (lockObject );
}
Console. WriteLine ("Exit current Thread:" + Thread. CurrentThread. GetHashCode ());
Monitor. Exit (lockObject); // release the object lock
}
// Method for generating random data threads to execute
Public void ThreadMethodTwo ()
{
Monitor. Enter (lockObject); // get the object lock
Console. WriteLine ("currently in the Thread:" + Thread. CurrentThread. GetHashCode ());
For (int I = 0; I <5; I ++)
{
// Notify other objects waiting for the lock that their statuses have changed. After the lock is released, the objects waiting for the lock will be locked.
Monitor. Pulse (lockObject );
Console. WriteLine ("WaitAndPluse2: Work ");
Number = random. Next (DateTime. Now. Millisecond); // generate a random number
Console. WriteLine ("WaitAndPluse2: generated data, number =" + number + ", Thread ID =" + Thread. CurrentThread. GetHashCode ());
Monitor. Wait (lockObject); // release the object lock and stop the current thread
}
Console. WriteLine ("Exit current Thread:" + Thread. CurrentThread. GetHashCode ());
Monitor. Exit (lockObject); // release the object lock
}
Public static void Main ()
{
ThreadWaitAndPluse demo = new ThreadWaitAndPluse ();
Thread t1 = new Thread (new ThreadStart (demo. ThreadMethodOne ));
T1.Start ();
Thread t2 = new Thread (new ThreadStart (demo. ThreadMethodTwo ));
T2.Start ();
Console. ReadLine ();
}
}
}

In most cases, the following results are displayed:

  

Generally, we can see the above results because the Start () method of t1 is prior, so it generally takes priority to execute. After t1 is executed, the object lock is obtained first, then, in the loop, Monitor. the Wait (lockObject) method temporarily releases the object lock, and t1 is in the blocking state. In this way, t2 obtains the object lock and can be executed. t2 enters the loop and runs the lock through Monitor. the Pulse (lockObject) method notifies you to wait for t1 of the same object lock to be ready, and then temporarily release the object lock after a random number is generated. Then t1 obtains the object lock and executes the output of data generated by t2, then t1 goes through Monitor. wait (lockObject) notifies t2 that it is ready and uses Monitor in the next loop. the Wait (lockObject) method temporarily releases the object lock. In this way, t1 and t2 are executed alternately and the above results are obtained.

Of course, in some cases, the following results may be displayed:

  

The reason for this result is actually very simple, although t1.Start () appears before t2.Start, however, it cannot be considered that t1 will be executed first (although it may be in most cases) than t2, but thread scheduling should also be considered, after multithreading is used, the code execution sequence becomes complicated. In some cases, t1 and t2 conflict with the use of the lock, resulting in a deadlock, as shown in. To avoid this situation, you can delay T2.

 

Manual synchronization

Manual synchronization refers to the use of different synchronization classes to create their own synchronization mechanisms. Using this policy requires manual synchronization for different domains or methods.

ReaderWriterLock

ReaderWriterLock supports the locks of a single write thread and multiple read threads. At any specific time, multiple threads are allowed to perform read operations at the same time or write operations on one thread. Using ReaderWriterLock for read/write synchronization is more efficient than using monitoring methods (such as Monitor.

The following is an example where two read threads and one write thread are used. The Code is as follows:


Using System;
Using System. Collections. Generic;
Using System. Text;
Using System. Threading;
Namespace StartThread
{
Public class ReadWriteLockDemo
{
Private int number;
Private ReaderWriterLock rwl;
Private Random random;
Public ReadWriteLockDemo ()
{
Rwl = new ReaderWriterLock ();
Random = new Random ();
}
///
/// Method to be executed by the read thread
///
Public void Read ()
{
Thread. Sleep (10); // pause to ensure that the write Thread takes priority
For (int I = 0; I <5; I ++)
{
Rwl. AcquireReaderLock (Timeout. Infinite );
Console. WriteLine ("Thread" + Thread. CurrentThread. GetHashCode () + "read data, number =" + number );
Thread. Sleep (500 );
Rwl. ReleaseReaderLock ();
}
}
///
/// Method to be executed by the write thread
///
Public void Write ()
{
For (int I = 0; I <5; I ++)
{
Rwl. AcquireWriterLock (Timeout. Infinite );
Number = random. Next (DateTime. Now. Millisecond );
Thread. Sleep (100 );
Console. WriteLine ("Thread" + Thread. CurrentThread. GetHashCode () + "write data, number =" + number );
Rwl. ReleaseWriterLock ();
}
}
Public static void Main ()
{
ReadWriteLockDemo rwld = new ReadWriteLockDemo ();
Thread reader1 = new Thread (new ThreadStart (rwld. Read ));
Thread reader2 = new Thread (new ThreadStart (rwld. Read ));
Reader1.Start ();
Reader2.Start ();
Thread writer1 = new Thread (new ThreadStart (rwld. Write ));
Writer1.Start ();
Console. ReadLine ();
}
}
}

The program execution result is as follows:

  

WaitHandle

The WaitHandle class is a pumping class. Multiple classes directly or indirectly inherit from the WaitHandle class. The class diagram is as follows:

  

In the WaitHandle class, the SignalAndWait, WaitAll, WaitAny, and WaitOne methods all have the form of overloading. All methods except WaitOne are static. The WaitHandle method is often used as the base class of the synchronization object. The WaitHandle object notifies other threads that they need exclusive access to resources. Other threads must wait until WaitHandle no longer uses resources and the waiting handle is not used.

The WaitHandle method has multiple Wait methods. The differences between these methods are as follows:

WaitAll: waits for all elements in the specified array to receive signals.

WaitAny: waits for any element in the specified array to receive a signal.

WaitOne: when being rewritten in a derived class, the current thread is blocked until the current WaitHandle receives the signal.

These wait Methods block threads until one or more synchronization objects receive signals.

The following is an example in MSDN about a computing process. The final calculation result is the first item + the second item + the third item, base numbers must be used to calculate the first, second, and third items. The thread pool, or ThreadPool, is used in the Code. This involves the order of computing. This problem can be well solved through WaitHandle and its subclass.

The Code is as follows:


Using System;
Using System. Collections. Generic;
Using System. Text;
Using System. Threading;
Namespace StartThread
{
// The following code is taken from MSDN. The author makes a Chinese code comment.
// Zhou Gong
Public class EventWaitHandleDemo
{
Double baseNumber, firstTerm, secondTerm, thirdTerm;
AutoResetEvent [] autoEvents;
ManualResetEvent manualEvent;
// The class that generates the random number.
Random random;
Static void Main ()
{
EventWaitHandleDemo ewhd = new EventWaitHandleDemo ();
Console. WriteLine ("Result = {0 }.",
Ewhd. Result (234). ToString ());
Console. WriteLine ("Result = {0 }.",
Ewhd. Result (55). ToString ());
Console. ReadLine ();
}
// Constructor
Public EventWaitHandleDemo ()
{
AutoEvents = new AutoResetEvent []
{
New AutoResetEvent (false ),
New AutoResetEvent (false ),
New AutoResetEvent (false)
};
ManualEvent = new ManualResetEvent (false );
}
// Calculates the base number.
Void CalculateBase (object stateInfo)
{
BaseNumber = random. NextDouble ();
// Indicates that the base number has been calculated.
ManualEvent. Set ();
}
// Calculate the first item
Void CalculateFirstTerm (object stateInfo)
{
// Generate a random number
Double preCalc = random. NextDouble ();
// Wait for the Base to calculate.
ManualEvent. WaitOne ();
// Use preCalc and baseNumber to calculate the first item.
FirstTerm = preCalc * baseNumber * random. NextDouble ();
// Sends a signal indicating that the computation is complete.
AutoEvents [0]. Set ();
}
// Calculate the second item
Void CalculateSecondTerm (object stateInfo)
{
Double preCalc = random. NextDouble ();
ManualEvent. WaitOne ();
SecondTerm = preCalc * baseNumber * random. NextDouble ();
AutoEvents [1]. Set ();
}
// Calculate the third item
Void CalculateThirdTerm (object stateInfo)
{
Double preCalc = random. NextDouble ();
ManualEvent. WaitOne ();
ThirdTerm = preCalc * baseNumber * random. NextDouble ();
AutoEvents [2]. Set ();
}
// Calculation Result
Public double Result (int seed)
{
Random = new Random (seed );
// Calculate simultaneously
ThreadPool. QueueUserWorkItem (new WaitCallback (CalculateFirstTerm ));
ThreadPool. QueueUserWorkItem (new WaitCallback (CalculateSecondTerm ));
ThreadPool. QueueUserWorkItem (new WaitCallback (CalculateThirdTerm ));
ThreadPool. QueueUserWorkItem (new WaitCallback (CalculateBase ));
// Wait for all signals.
WaitHandle. WaitAll (autoEvents );
// Reset the signal to wait for the next calculation.
ManualEvent. Reset ();
// Return the calculation result
Return firstTerm + secondTerm + thirdTerm;
}
}
}

The program running result is as follows:


Result = 0.355650523270459.
Result = 0.125205692112756.

Of course, because random numbers are introduced, the results of each calculation are different. Here we will talk about the control between them. The Result (int seed) method is used to calculate the base number, the first item, the second item, and the third item in the thread pool. to calculate the first two or three items, you must first determine the base number, these methods use manualEvent. waitOne () temporarily stops execution. Therefore, the base calculation method is executed first. After the base is calculated, manualEvent is used. the Set () method indicates the start of the first two or three calculation methods. After the calculation is completed, the Set () method of the AutoResetEvent element in the autoEvents array sends a signal to mark the execution. In this way, the WaitHandle. WaitAll (autoEvents) step can be executed to get the execution result.

The other classes of WaitHandle in the code above are limited by space. Here we will give an example of how many similarities they use (after all, they are inherited from an abstract class ).

 

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.