Home Random Page


CATEGORIES:

BiologyChemistryConstructionCultureEcologyEconomyElectronicsFinanceGeographyHistoryInformaticsLawMathematicsMechanicsMedicineOtherPedagogyPhilosophyPhysicsPolicyPsychologySociologySportTourism






Primitive Thread Synchronization Constructs

In this chapter:

Class Libraries and Thread Safety............................................................. 759

Primitive User-Mode and Kernel-Mode Constructs.................................. 760

User-Mode Constructs............................................................................ 762

Kernel-Mode Constructs......................................................................... 778

 

When a thread pool thread blocks, the thread pool creates additional threads, and the time and memory resources required to create, destroy, and schedule threads is very expensive. When many developers see that they have threads in their program that are not doing anything useful, they tend to create more threads in hopes that the new threads will do something useful. The key to building scalable and responsive applications is to not block the threads you have so that they can be used and reused to execute other tasks. Chapter 27, “Compute-Bound Asynchronous Operations,” focused on how to use existing threads to perform compute-bound operations, and Chapter 28, “I/O-Bound Asynchronous Operations,” focused on how to use threads when performing I/O-bound operations.

In this chapter, I focus on thread synchronization. Thread synchronization is used to prevent cor- ruption when multiple threads access shared data at the same time. I emphasize at the same time, because thread synchronization is all about timing. If you have some data that is accessed by two threads and those threads cannot possibly touch the data simultaneously, then thread synchroniza- tion is not required at all. In Chapter 28, I discussed how different sections of async functions can be executed by different threads. Here we could potentially have two different threads accessing

the same variables and data. But async functions are implemented in such a way that it is impossible for two threads to access this same data at the same time. Therefore, no thread synchronization is required when code accesses data contained within the async function.

This is ideal because thread synchronization has many problems associated with it. First, it is tedious and extremely error-prone. In your code, you must identify all data that could potentially be touched by multiple threads at the same time. Then you must surround this code with additional code that acquires and releases a thread synchronization lock. The lock ensures that only one thread at a time can access the resource. If you forget to surround just one block of code with a lock, then the data will become corrupted. Also, there is no way to prove that you have added all your locking code correctly. You just have to run your application, stress-test it a lot, and hope that nothing goes wrong.


In fact, you should test your application on a machine that has as many CPUs as possible because the more CPUs you have, the better chance that two or more threads will attempt to access the resource at the same time, making it more likely you’ll detect a problem.



The second problem with locks is that they hurt performance. It takes time to acquire and release a lock because there are additional method calls, and because the CPUs must coordinate with each other to determine which thread will acquire the lock first. Having the CPUs in the machine communi- cate with each other this way hurts performance. For example, let’s say that you have code that adds a node to the head of a linked list.

 

// This class is used by the LinkedList class public class Node {

internal Node m_next;

// Other members not shown

}

 

public sealed class LinkedList { private Node m_head;

 

public void Add(Node newNode) {

// The following two lines perform very fast reference assignments newNode.m_next = m_head;

m_head = newNode;

}

}

 

This Add method simply performs two reference assignments that can execute extremely fast.

Now, if we want to make Add thread safe so that multiple threads can call it simultaneously without corrupting the linked list, then we need to have the Add method acquire and release a lock.

 

public sealed class LinkedList {

private SomeKindOfLock m_lock = new SomeKindOfLock(); private Node m_head;

 

public void Add(Node newNode) { m_lock.Enter();

// The following two lines perform very fast reference assignments newNode.m_next = m_head;

m_head = newNode; m_lock.Leave();

}

}

 

Although Add is now thread safe, it has also become substantially slower. How much slower de- pends on the kind of lock chosen; I will compare the performance of various locks in this chapter and in Chapter 30, “Hybrid Thread Synchronization Constructs.” But even the fastest lock could make the Add method several times slower than the version of it that didn’t have any lock code in it at all. Of course, the performance becomes significantly worse if the code calls Add in a loop to insert several nodes into the linked list.


The third problem with thread synchronization locks is that they allow only one thread to access the resource at a time. This is the lock’s whole reason for existing, but it is also a problem, because blocking a thread causes more threads to be created. So, for example, if a thread pool thread at- tempts to acquire a lock that it cannot have, it is likely that the thread pool will create a new thread to keep the CPUs saturated with work. As discussed in Chapter 26, “Thread Basics,” creating a thread is very expensive in terms of both memory and performance. And to make matters even worse, when the blocked thread gets to run again, it will run with this new thread pool thread; Windows is now scheduling more threads than there are CPUs, and this increases context switching, which also hurts performance.

The summary of all of this is that thread synchronization is bad, so you should try to design your applications to avoid as much of it as possible. To that end, you should avoid shared data such as static fields. When a thread uses the new operator to construct an object, the new operator returns a reference to the new object. At this point in time, only the thread that constructs the object has a reference to it; no other thread can access that object. If you avoid passing this reference to another thread that might use the object at the same time as the creating thread, then there is no need to synchronize access to the object.

Try to use value types because they are always copied, so each thread operates on its own copy. Finally, it is OK to have multiple threads accessing shared data simultaneously if that access is read- only. For example, many applications create some data structures during their initialization. Once initialized, the application can create as many threads as it wants; if all these threads just query the data, then all the threads can do this simultaneously without acquiring or releasing any locks. The String type is an example of this: after a String object is created, it is immutable; so many threads can access a single String object at the same time without any chance of the String object becom- ing corrupted.

 

 


Date: 2016-03-03; view: 848


<== previous page | next page ==>
Nbsp;   I/O Request Priorities | Nbsp;   Primitive User-Mode and Kernel-Mode Constructs
doclecture.net - lectures - 2014-2024 year. Copyright infringement or personal data (0.007 sec.)