Introduction to Transaction Locking
Introduction to Transaction Locking
Fundamentals of Transaction Locking
Introduction to Atomicity
Consider the following analogy. In a department store (a store that sells clothes), a customer selects and brings ten (or more) items to the cashier. The cashier is using a cashing machine (also called point of sale or POS) to process the customer's orders but there is also another cashing machine available but nobody is using it at this time. The cashier starts processing the customer's purchase.
Another customer comes from somewhere and says that he just has a simple question for the cashier. The cashier stops the processing she was doing in order to answer the other customer's question. After answering the question, the clerk continues serving the first customer. A few seconds later, a manager interrupts the cashier asking why there are so many clothes on the floor throughout the store. The cashier stops what she was doing and thinks about how to answer the manager, but the manager starts talking about something else.
To be polite and respectful, the cashier continues listening and seems interested in what the manager is saying. The manager leaves and the clerk resumes what she was doing. Another customer comes from nowhere, asks the waiting customer if he doesn't mind. The new customer asks the previous customer if he (the new customer) can move ahead because he has only two items and he left his car parked outside in a towing area. The previous customer says he doesn't mind. The new customer walks to the cashier who now has to think about helping the new customer while the previous customer is waiting.
The clerk things: (*) should she ask the new customer to wait until she has completed the first customer's order? (*) Should she cancel the previous customer order to process the new one? (*) Should she use another machine to process the order of the new customer in order to reduce confusion? etc. Let's consider that the cashier serves the new customer on another machine and now she must come back to complete the order of the previous customer. What if the previous customer is now upset and starts talking aloud (yelling, cursing, etc). What if the cashier doesn't remember what clothes she had rang already and what others are left? What if the cashier was going to give a discount to the previous customer but now she doesn't remember what type of discount she was going to apply? What if, for some unpredictable reason, the machine has stopped working, in which case the cashier is wondering whether the previous transactions, or some transactions, had been saved, etc.
In the same way, there are many transactions going on inside the computer, and there are many threads running at the same time. The threads must share resources (such as the memory (RAM), the ports (USB, Wi-Fi, etc). In fact, threads also compete for (the available) resources. As a result, threads interrupt each other. Just like in real life where some people drive faster because they think they are more in a hurry than others, some threads want to complete their jobs faster or regardless of what other threads need. Just like in real life, these situations create problems, conflicts, misunderstandings, etc. Both the computer processor and the operating system try to deal with various types of these problems. You too as a programmer can intervene to solve them. One way to address these issues is referred to as atomicity.
Atomicity is the ability to complete a transaction in an all-or-nothing scenario. It means that when a transaction starts, it must be completed. If anything interrupts the transaction before it ends, if anything prevents the transaction from completing, the whole transaction is cancelled as if it never took place. Both the C# language and the .NET Framework provide the means to deal with atomicity.
Locking a Transaction
Locking a transaction consists of isolating its area of work so that nothing else in the flow of the program would be dealt with until that area of work is complete. The use of a lock in C# is done using the lock keyword in a formula as follows:
lock(holder) { // What to do }
You start with the lock keyword that appears as when calling a method. The job to do, which we referred to as a transaction, is between curly brackets { and }. That code area is called a critical section. Just like in the real world where you must first purchase a lock, in C# you must have a key. You have various options. You can use a string as a key and pass that string as the argument of the lock keyword. Here is an example:
using System;
public class Exercise
{
[STAThread]
public static int Main()
{
lock("Operation")
{
double number1 = 94705.75;
double number2 = 50824.85;
double addition = number1 + number2;
Console.WriteLine("{0} + {1} = {2}", number1, number2, addition);
}
return 0;
}
}
This would produce:
94705.75 + 50824.85 = 145530.6 Press any key to continue . . .
Of course, you can process variables from outside the lock. Here is an example:
using System; public class Exercise { [STAThread] public static int Main() { double number1 = 0; double number2 = 0; lock("Operation") { number1 = 94705.75; number2 = 50824.85; double addition = number1 + number2; Console.WriteLine("{0} + {1} = {2}", number1, number2, addition); } return 0; } }
The key of a lock must be a reference type. This means that you must use an object of type object or a class (not a structure type, and remember that any class you create in C# is directly or indirectly derived from the Object class). To provide this object, you can first declare a variable of the desired type. If you declare the variable, you must initialize it using any constructor of that class. The name of the variable follows the rules of names of variables in C#. You will pass that variable to lock(). Here is an example:
using System; public class Exercise { [STAThread] public static int Main() { object separation = new object(); lock (separation) { double number1 = 94705.75; double number2 = 50824.85; double addition = number1 + number2; Console.WriteLine("{0} + {1} = {2}", number1, number2, addition); } return 0; } }
In most cases, you will not use the key other than passing it to lock(). Based on this, you don't have to first declare a variable. You can pass the new keyword and the constructor in the parentheses of lock(). Here is an example:
using System;
public class Exercise
{
[STAThread]
public static int Main()
{
lock (new object())
{
double number1 = 94705.75;
double number2 = 50824.85;
double addition = number1 + number2;
Console.WriteLine("{0} + {1} = {2}", number1, number2, addition);
}
return 0;
}
}
When you use a lock in C#, no thread should (or can) interrupt the transaction(s) of that lock; that is, everything in its curly brackets must complete. Any thread that reaches that section must wait.
In the real world, you can use only one lock per item such as per house or suitcase, etc. In your application, you can use the same lock in different critical sections (such as in different methods or inside the same method). This is said that the sections (or methods) share the same lock. Here is an example:
using System; public class Exercise { [STAThread] public static int Main() { Random series = new Random(); Console.Title = "Addition"; lock (series) { double number1 = 94705.75; double number2 = 50824.85; double addition = number1 + number2; Console.WriteLine("Addition: {0} + {1} = {2}", number1, number2, addition); } Console.WriteLine("--------------------------------------------"); lock ("Critique") { double number = 94705.75; double times = number * 2; Console.WriteLine("Doubler: {0} * 2 = {1}", number, times); } Console.WriteLine("--------------------------------------------"); lock (series) { Console.WriteLine("Full Name = Jeannette Clarence"); } Console.WriteLine("============================================="); return 0; } }
This would produce:
Addition: 94705.75 + 50824.85 = 145530.6 -------------------------------------------- Doubler: 94705.75 * 2 = 189411.5 -------------------------------------------- Full Name = Jeannette Clarence ============================================= Press any key to continue . . .
Normally, you should avoid sharing a lock. If you need to use many critical sections in your code, declare a variable for each. The lock variables can be of the same type or different types, as long as their names are different.
In the same way, avoid using the same string as the object (name) of many locks.
Nesting a Lock
You can create a critical section inside another critical section. Each section must use its own lock. The nested critical section can appear anywhere inside the nesting section, such as before some of its code lines, after some code lines but before other code lines, or just before the closing curly bracket. Here is an example:
using System; public class Exercise { [STAThread] public static int Main() { Random series = new Random(); object analysis = new object(); Console.Title = "Threading"; lock (series) { double number1 = 94705.75; double number2 = 50824.85; double addition = number1 + number2; lock (analysis) { double number = 94705.75; double times = number * 2; Console.WriteLine("{0} * 2 = {1}", number, times); } Console.WriteLine("--------------------------------------------"); Console.WriteLine("Addition: {0} + {1} = {2}", number1, number2, addition); } Console.WriteLine("--------------------------------------------"); lock (series) { Console.WriteLine("Full Name = Jeannette Clarence"); } Console.WriteLine("============================================="); return 0; } }
This would produce:
94705.75 * 2 = 189411.5 -------------------------------------------- Addition: 94705.75 + 50824.85 = 145530.6 -------------------------------------------- Full Name = Jeannette Clarence ============================================= Press any key to continue . . .
In the same way, you can nest many critical sections in one, or nest a critical section in one that itself is nested.
Monitoring a Thread
Introduction
While the C# language provides the lock keyword to isolate a transaction that must complete without interruption, the .NET Framework supports the locking concept through a static class named Monitor.
The Monitor class goes beyond the limitations of the lock keyword. For example, the C#'s lock is mostly used to create a section of code for atomicity. On the other hand, the Monitor class goes behind-the-scenes to manage the lock used by the current object such as to restrict its access and/or to release it. To be able to manage the various locks in an application, Monitor is a static class with various methods that, of course, are all static. This means that you will not declare a variable for this class. Instead, you can just call the method you need (provided you know what the method does).
Entering a Critical Section
To let you start delimiting a critical section, the Monitor class is equipped with an overloaded method named Enter that has two versions. One of them uses the following syntax:
public static void Enter(object obj)
As seen for the lock keyword, the Monitor.Enter() method takes an argument that must be a reference type. Here is an example:
using System.Threading; public class Exercise { public static int Main() { Monitor.Enter("Truth"); return 0; } }
The other version of the Monitor.Enter() method uses the following syntax:
public static void Enter(object obj, ref bool lockTaken)
Besides the key, this version takes a Boolean argument by reference. This argument is the value returned by the method to indicate whether the lock was actually acquired (if it returns true) or not (if it returns false).
Practical Learning: Suspending a Thread
namespace Chemistry8 { public enum Phase { Gas, Liquid, Solid, Unknown } public class Element { public string Symbol { get; set; } = "H"; public string ElementName { get; set; } = "Hydrogen"; public int AtomicNumber { get; set; } = 1; public float AtomicWeight { get; set; } = 1.008F; public Phase Phase { get; set; } = Phase.Gas; public Element() { } public Element(int number) { AtomicNumber = number; } public Element(string symbol) { Symbol = symbol; } public Element(int number, string symbol, string name, float mass) { ElementName = name; AtomicWeight = mass; AtomicNumber = number; Symbol = symbol; Phase = Phase.Unknown; } public Element(int number, string symbol, string name, float mass, Phase phase) { ElementName = name; AtomicWeight = mass; Phase = phase; AtomicNumber = number; Symbol = symbol; } } }
using System.Threading; using static System.Console; using System.Collections.Generic; namespace Chemistry7 { public class Chemistry { private static void Describe(Element e) { Clear(); WriteLine("Chemistry - " + e.ElementName); WriteLine("-------------------------------"); WriteLine("Symbol: " + e.Symbol); WriteLine("Element Name: " + e.ElementName); WriteLine("Atomic Number: " + e.AtomicNumber); WriteLine("Atomic Weight: " + e.AtomicWeight); WriteLine("Phase: " + e.Phase); WriteLine("================================"); } public static int Main(string[] args) { bool taken = false; List<Element> elements = new List<Element>(); elements.Add(new Element(1, "H", "Hydrogen", 1.008f, Phase.Gas)); elements.Add(new Element(2, "He", "Helium", 4.002602f, Phase.Gas)); elements.Add(new Element(3, "Li", "Lithium", 6.94f, Phase.Solid)); elements.Add(new Element(4, "Be", "Beryllium", 9.0121831f, Phase.Solid)); elements.Add(new Element(5, "B", "Boron", 10.81f, Phase.Solid)); elements.Add(new Element(number: 6, symbol: "C", name: "Carbon", mass: 12.011f, phase: Phase.Solid)); elements.Add(new Element(7, "N", "Nitrogen", 14.007f, Phase.Gas)); elements.Add(new Element(8, "O", "Oxygen", 15.999f, Phase.Gas)); elements.Add(new Element(9, "F", "Fluorine", 15.999f, Phase.Gas)); elements.Add(new Element("Ne") { AtomicNumber = 10, ElementName = "Neon", AtomicWeight = 20.1797f, Phase = Phase.Gas }); elements.Add(new Element(11, "Na", "Sodium", 22.98976928f, Phase.Solid)); elements.Add(new Element(12, "Mg", "Magnesium", 24.305f, Phase.Solid)); elements.Add(new Element(13, "Al", "Aluminium", 26.9815385f, Phase.Solid)); elements.Add(new Element() { ElementName = "Silicon", AtomicWeight = 28.085f, Symbol = "Si", AtomicNumber = 14, Phase = Phase.Solid }); elements.Add(new Element() { ElementName = "Phosphorus", AtomicWeight = 30.973761998f, Symbol = "P", AtomicNumber = 15, Phase = Phase.Solid }); elements.Add(new Element(16, "S", "Sulfur", 32.06f, Phase.Solid)); elements.Add(new Element(17, "Cl", "Chlorine", 35.45f, Phase.Gas)); elements.Add(new Element(18, "Ar", "Argon", 39.792f, Phase.Gas)); elements.Add(new Element(19, "K", "Potassium", 39.0983f, Phase.Solid)); elements.Add(new Element(20, "Ca", "Calcium", 40.078f, Phase.Solid)); elements.Add(new Element(21, "Sc", "Scandium", 44.955908f, Phase.Solid)); elements.Add(new Element(22, "Ti", "Titanium", 47.867f, Phase.Solid)); Monitor.Enter("Creation", ref taken); elements.Add(new Element(23, "V", "Vanadium", 50.9415f, Phase.Solid)); WriteLine("Chemistry - Periodic Table"); WriteLine("------------------------------------------------"); foreach (Element elm in elements) { Describe(elm); Thread.Sleep(3500); } WriteLine("The monitor was taken: {0}", taken); return 0; } } }
Chemistry - Hydrogen ------------------------------- Symbol: H Element Name: Hydrogen Atomic Number: 1 Atomic Weight: 1.008 Phase: Gas ================================ Press any key to continue . . .
Chemistry - Vanadium ------------------------------- Symbol: V Element Name: Vanadium Atomic Number: 23 Atomic Weight: 50.9415 Phase: Solid ================================ The monitor was taken: True Press any key to continue . . .
Exiting a Critical Section
To let you indicate the end of a critical section, the Monitor class is equipped with a method named Exit. Its syntax is:
public static void Exit(object obj)
This method takes an argument as the (same) object that was passed to the Monitor.Enter() method. For this reason, you should (must) explicitly declare a variable and pass it as argument to both methods. Here is an example:
using System;
using System.Threading;
public class Exercise
{
public static int Main()
{
Monitor.Enter("Truth");
Console.WriteLine("The current time is " + DateTime.Now.ToLongTimeString());
Monitor.Exit("Truth");
Console.WriteLine("=================================");
return 0;
}
}
Here is an example of a result:
The current time is 9:36:26 AM ================================= Press any key to continue . . .
After using a lock, to indicate to the other objects that the lock has been released, you should create a try...finally block in your code. In this case, call the Monitor.Exit() method in the finally section. In the same way, you can create as many monitors as you want as long as each call to Monitor.Enter() and Monitor.Exit() uses the same object.
As we saw for locks in C#, you can nest a monitored section inside another.
Managing a Lock
Synchronizing a Lock
When creating a lock using the Monitor class, both the Monitor.Enter() and the Monitor.Exit() methods must use the same object. If they don't, the application would throw and exception named SynchronizationLockException:
public class SynchronizationLockException : SystemException
Most of the times, this error should be very to deal with. Most of the times (but not always), the error indicates that you are using different keys in the beginning (Monitor.Enter()) and the end (Monitor.Exit()) and your critical section, which should be easy to fix by checking their names (or objects).
Requesting a Lock
We saw that you can (and should) first create locks before assigning them to threads. You should use a different lock for each critical section. As a matter of fact, a thread can use different locks, one at a time. This means that a thread can use a lock A to perform an operation, release that lock A and grab another lock B to perform another operation, release that lock B, and start using another lock C or re-use a previously released lock A or B for another operation. The thread you are using at one particular time is called the current thread,
When a critical section grabs a lock, no other critical section should use the same lock, until the previous critical section ends. This means that you should not assign a lock that one thread is currently using to another section. To help you find out whether a lock is available before using it, the Monitor class provides an overloaded method named TryEnter. One of its versions uses the following syntax:
public static bool TryEnter(object obj)
This method takes a lock as argument and tries to start a critical section with it. If the method is able to get the lock, the method returns true.
If the Monitor.TryEnter(object obj) method cannot get the lock for any reason, it returns false. To let you find out whether the lock was acquired or not, the Monitor class provides the following version of the TryEnter method:
public static void TryEnter(object obj, ref bool lockTaken)
In this case, the lockTaken argument passed by reference returns a value that indicates the outcome of grabbing the lock.
In any case, if the method fails in accessing the lock, you can let the thread wait a certain time before trying agin. To suport this, the Monitor class provides two versions of the TryEnter method whose syntaxes are:
public static bool TryEnter(object obj, int millisecondsTimeout) public static bool TryEnter(object obj, TimeSpan timeout)
Checking the Availability of a Lock
At any time, to let you find out whether the current thread is currently holding a certain lock, the Monitor class provides a static Boolean method named IsEntered. Its syntax is:
public static bool IsEntered(object obj)
A lock object is passed to the method to check.
Interlocking Some Threads
Introduction
While different threads are running in an application, they must coordinate their activities to eliminate or reduce conflicts with regards to atomicity. The interactions among threads sometimes occur when two or more of them are trying to access the same variable for any reason such as to change the value of the variable. To deal with atomic problems that could occur when different threads are trying to perform an operation on the same variable, the .NET Framework provides a static class named Interlocked.
Incrementing an Integer
One of the most routine operations performed in an application is to increment a number by 1, which is usually done in a for loop. If the value being incremented is globally defined, to make sure that such an operation respects atomicity, the Interlocked class provides an overloaded method named Increment. One of the versions is made for 32-bit integers. Its syntax is:
public static int Increment(ref int location)
The other version is for 64-bit integers. Its syntax is:
public static long Increment(ref long location);
The Interlocked.Increment() method takes a number as argument, adds 1 to it, and stores the result in the argument. When the method ends, the incrementing result can be retrieved in the argument that is passed by reference. Here is an example of calling the 64-bit version of the method:
using System;
using System.Threading;
public class Exercise
{
public static int Main()
{
int orderNumber = 100000;
Console.WriteLine("Number: {0}", orderNumber);
Interlocked.Increment(ref orderNumber);
Console.WriteLine("Number: {0}", orderNumber);
Console.WriteLine("=================================");
return 0;
}
}
This would produce:
Number: 100000 Number: 100001 ================================= Press any key to continue . . .
Decrementing an Integer
The reverse of incrementing an integer is to subtract 1 from it. To support this operation in atomic scenarions, the Interlocked class is equipped with an overloaded method named Decrement. It provides a version form 32-bit integers and a version for 64-bit integers. Their syntaxes are:
public static int Decrement(ref int location) public static long Decrement(ref long location)
This method works like its Interlocked.Increment() counterpart, except that it subtract its argument by 1.
Adding Two Integers
One of the primary operations of the Interlocked class is to add two numbers. To assist you with this operation, the Interlocked class is equipped with an overloaded method named Add. It provides a version for 32-bit integers and a version for 64-bit integers. The syntaxes of this method are:
public static int Add(ref int location1, int value) public static long Add(ref long location1, long value)
This method adds its two arguments and stores the result in the first argument. This means that when the method ends, it returns the result in both the first argument and its own return value. Here is an example of calling the 32-bit version:
using static System.Console;
using System.Threading;
public class Exercise
{
public static int Main()
{
int number1 = 2237;
int number2 = 5848;
WriteLine($"Number 1: {number1}");
WriteLine($"Number 2: {number2}");
WriteLine("--------------------");
Interlocked.Add(ref number1, number2);
WriteLine($"Number 1: {number1}");
WriteLine($"Number 2: {number2}");
WriteLine("=================================");
return 0;
}
}
This would produce:
Number 1: 2237 Number 2: 5848 -------------------- Number 1: 8085 Number 2: 5848 ================================= Press any key to continue . . .
The Interlocked.Add() method is very valuable if a thread needs to add numbers that produce a large integer. The class provides a second version of the method for long integers. Its syntax is:
public static long Add(ref long location1, long value)
This works like the previous one except for long (very large) integers.
Exchanging Values
As you may be aware from your knowledge of C#, initializing a variable consists of assigning a value to the variable as soon as it is declared. In fact, assigning a value to a variable consists of giving a (new) value to it. In the same way, creating an object consists of assigning a reference from a class variable to it. You may also have to assign a list from one source to another list variable. The value, object, or list can be a constant or it can come from the return value of a called method, or from a list processed from some type of source. Sometimes this operation can be the subject of multiple operations from different threads, which means threads may interfere with each other when trying to initialize a variable or when trying to change/update a list.
To deal with atomic issues related to a variable, object, or list receiving (a) new value(s) or getting (an) object(s) for its object, the Interlocked class is equipped with an overloaded method named Exchange. It provides versions for integers (32-bit and 64-bit), floating-point numbers (single and double-precision), and objects of any kinds. The syntaxes of this method are:
public static int Exchange(ref int location1, int value, int comparand ) public static long Exchange(ref long location1, long value, long comparand ) public static float Exchange(ref float location1, float value, float comparand ) public static double Exchange(ref double location1, double value, double comparand ) public static object Exchange(ref object location1, object value, object comparand ) [ComVisibleAttribute(false)] public static T Exchange<T>(ref T location1, T value, T comparand ) where T : classpublic static IntPtr Exchange(ref IntPtr location1, IntPtr value, IntPtr comparand )
Here is an example of calling the double-precision number version of the method:
using static System.Console;
using System.Threading;
public class Exercise
{
public static int Main()
{
double number1 = 2237.84;
double number2 = 5848.39;
WriteLine($"Number 1: {number1}");
WriteLine($"Number 2: {number2}");
WriteLine("--------------------");
Interlocked.Exchange(ref number1, number2);
WriteLine($"Number 1: {number1}");
WriteLine($"Number 2: {number2}");
WriteLine("=================================");
return 0;
}
}
This would produce:
Number 1: 2237.84 Number 2: 5848.39 -------------------- Number 1: 5848.39 Number 2: 5848.39 ================================= Press any key to continue . . .
Characteristics of a Thread
The Name of a Thread
When you create a thread, to further identify it in your code, you can/should give it a name. If you don't specify a name, the compiler automatically gives it a default name. To let you specify the name of your thread, the Thread class is equipper with a property named Name:
public string Name { get; set; }
This read-write property allows you to not only name a thread but also to get the name of a thread. Here is an example of using this property:
using System.Threading; using static System.Console; public class Exercise { private static void MurphysLaw() { WriteLine("Whatever can go wrong will go wrong."); } public static int Main() { Thread thWalk = new Thread(MurphysLaw); thWalk.Name = "Common"; thWalk.Start(); WriteLine("Thread Name: " + thWalk.Name); WriteLine("===================================="); return 0; } }
This would produce:
Thread Name: Common ==================================== Whatever can go wrong will go wrong. Press any key to continue . . .
The Identifier of a Managed Thread
As threads are running in the computer, the operating system assigns a unique number to each. To let you find out what that number is, the Thread class is equipped with the read-only ManagedThreadId property:
public int ManagedThreadId { get; }
Here is an example of accessing this property:
using System.Threading; using static System.Console; public class Exercise { private static void MurphysLaw() { WriteLine("Whatever can go wrong will go wrong."); } public static int Main() { Thread thWalk = new Thread(MurphysLaw); thWalk.Name = "Common"; WriteLine("Thread Name: " + thWalk.Name); WriteLine("Thread Id: " + thWalk.ManagedThreadId); thWalk.Start(); WriteLine("===================================="); return 0; } }
This would produce:
Thread Name: Common Thread Id: 3 ==================================== Whatever can go wrong will go wrong. Press any key to continue . . .
Scheduling Threads
A process (or application) uses many threads and some threads are running at the same time. Also, many processes are used in the computer at the same time. On the other hand, a computer has limited resources. For example in the past, most computers had only one processor. Even though nowadays processors are becoming more and more sophisticated, computers also now have to honor many requests at the same time, for limited resources. For example, most computers have only one motherboard, one sound system, one DVD or Blu-ray player if any, one audio port, one network port (RJ-45 or Wi-Fi), etc. To address these infinite requests, the operating system uses an internal program (or algorithm) named a task scheduler, or just a scheduler. The scheduler keeps a first-in-first-out list, like a queue where people stand at the bank, a supermarket, or the airport in front of a cashier, ready to be served. By default, the first item in the queue (the first person arrived) is also the first to be served. Of course, as items (people) are added (or coming) to the queue, those that (who) have been served leave.
As life experience has proved so many times, some people are in a hurry more than others or for some reasons, some people should (must) be served faster than others. For example, important people such as politicians and movie stars must be served first. Such situations also apply to threads. For example, while a media player of a machine or device (desktop computer, laptop, tablet PC, cell phone, etc) is playing music through the machine's sound system, when some applications such as email messengers (Hotmail, Yahoo, Gmail, etc) receive an update or notification, they may have to (sometimes must) immediately let the user know, which is done by playing a sound bite through the same speakers the media player is using. As a result, some threads must go ahead and/or bypass what other threads are doing. Normally, this is usually a temporary or very fast situation and the user doesn't experience any real interruption.
The Importance/Priority of a Thread
Not all threads are created equal, and threads must share resources of the computer. As a result, some threads must be executed before other threads. In most cases, the OS is in charge (and knows how (is equipped) to take care) of deciding what thread must execute before what thread. Still, in a custom application, you can decide what thread has a more important job. To let you set this aspect, the Thread class is equipped with a property named Priority:
public ThreadPriority Priority { get; set; }
The value of the Thread.Priority property comes from the ThreadPriority enumeration. Its members are (the names should be self-explanatory): Highest, AboveNormal, Normal, BelowNormal, and Lowest.
The Current Thread
To let you know the thread that is currently being used, the Thread class provides a read-only property named CurrentThread:
public static Thread CurrentThread { get; }
Here is an example of getting the Id of the current thread:
using System.Threading;
using static System.Console;
public class Exercise
{
private static void MurphysLaw()
{
WriteLine("Whatever can go wrong will go wrong.");
}
public static int Main()
{
Thread thWalk = new Thread(MurphysLaw);
thWalk.Name = "Common";
WriteLine("Thread Name: " + thWalk.Name);
WriteLine("Thread Id: " + thWalk.ManagedThreadId);
thWalk.Start();
WriteLine("Current Thread Id: " + Thread.CurrentThread.ManagedThreadId);
WriteLine("====================================");
return 0;
}
}
This would produce:
Thread Name: Common Thread Id: 3 Current Thread Id: 1 ==================================== Whatever can go wrong will go wrong. Press any key to continue . . .
As an alternative to let you get this information, the static System.Environment class is equipped with a read-only property named CurrentManagedThreadId:
public static int CurrentManagedThreadId { get; }
Here is an example of accessing it:
using System.Threading; using static System.Console; public class Exercise { private static void MurphysLaw() { WriteLine("Whatever can go wrong will go wrong."); } public static int Main() { Thread thWalk = new Thread(MurphysLaw); thWalk.Name = "Common"; WriteLine("Thread Name: " + thWalk.Name); WriteLine("Thread Id: " + thWalk.ManagedThreadId); thWalk.Start(); WriteLine("Current Thread Id: " + Thread.CurrentThread.ManagedThreadId); BackgroundColor = System.ConsoleColor.Black;
ForegroundColor = System.ConsoleColor.Red; WriteLine("Current Thread Id: {0}", System.Environment.CurrentManagedThreadId); ResetColor();
WriteLine("===================================="); return 0; } }
This would produce:
Thread Name: Common Thread Id: 3 Current Thread Id: 1 Whatever can go wrong will go wrong. Current Thread Id: 1 ==================================== Press any key to continue . . .
A Background Thread
All of the threads we have created so far are used to run an application (a process), to start and/or to end it. In fact, depending on how it is created and is configured, a thread can prevent a user from closing an application. Such threads are referred to as foreground threads.
A background thread is a thread that works behind-the-scenes in an application. One of the main characteristics of a background thread is that it cannot interfere with the user trying to close the application.
A background thread is primarily created like a foreground thread. To let you control the grounding status of a thread, the Thread class is equipped with a Boolean property named IsBackground:
public bool IsBackground { get; set; }
As you can see, this is a read-write property that allows you either to specify that a thread is, or must behave as, a background thread, or to find out whether a thread is background. Here are examples of accessing this property:
using System.Threading; using static System.Console; public class Exercise { private static void MurphysLaw() { WriteLine("Whatever can go wrong will go wrong."); } public static int Main() { Thread thWalk = new Thread(MurphysLaw); WriteLine("The Walking thread is a background thread: " + thWalk.IsBackground.ToString()); WriteLine("------------------------------------------------"); WriteLine("Thread Id: " + thWalk.ManagedThreadId); WriteLine("------------------------------------------------"); thWalk.Start(); thWalk.IsBackground = true; WriteLine("The Walking thread is a background thread: " + thWalk.IsBackground.ToString()); WriteLine("================================================="); return 0; } }
This would produce:
The Walking thread is a background thread: False ------------------------------------------------ Thread Id: 3 ------------------------------------------------ The Walking thread is a background thread: True ================================================= Whatever can go wrong will go wrong. Press any key to continue . . .
The Status of a Thread
At any time, you may want to know what is going on with a certain thread. This information is considered the status of the thread. To keep track of the ongoing or current status of a thread, the Thread class is equipped with the read-only ThreadState property, which is based on the ThreadState enumeration:
public ThreadState ThreadState { get; }
Use this property to know the current status of the thread. The members of the ThreadState enumeration are:
Here are two examples of checking the status of the current thread:
using System.Threading; using static System.Console; public class Exercise { private static void MurphysLaw() { WriteLine("Whatever can go wrong will go wrong."); } public static int Main() { Thread thWalk = new Thread(MurphysLaw); WriteLine("Current Thread Status: " + thWalk.ThreadState.ToString()); WriteLine("------------------------------------"); WriteLine("Thread Id: " + thWalk.ManagedThreadId); WriteLine("------------------------------------"); thWalk.Start(); WriteLine("Current Thread Status: " + thWalk.ThreadState.ToString()); WriteLine("===================================="); return 0; } }
This would produce:
Current Thread Status: Unstarted ------------------------------------ Thread Id: 3 ------------------------------------ Current Thread Status: Running ==================================== Whatever can go wrong will go wrong. Press any key to continue . . .
When checking the status of a thread, you can use the bitwise operator "|" which allows you to combine members. For example ThreadState.Background | ThreadState.Running indicates that you want to know whether the thread is a background thread and is currently active. Of course, you should not try to combine opposite values such as Aborted and Running.
A Pool of Threads
As seen above, you can use the Thread class to create one thread at a time and have as many threads as possible. A collection of threads created using the .NET Framework is referred to as a managed pool thread.
Besides the ability to declare as many Thread variables as necessary, the .NET Framework provides the ThreadPool class used to create a collection of threads. Whether you use many Thread variables or the ThreadPool class, to let you find out whether a thread belongs to the managed thread pool, the Thread class is equipped with a read-only Boolean property named IsThreadPoolThread:
public bool IsThreadPoolThread { get; }
|
||
Previous | Copyright © 2016-2019, FunctionX | Next |
|