Выбрать главу

You’ll notice that the threads tend to run in any order, which means that sleep( ) is also not a way for you to control the order of thread execution. It just stops the execution of the thread for awhile. The only guarantee that you have is that the thread will sleep at least 100 milliseconds (in this example), but it may take longer before the thread resumes execution, because the thread scheduler still has to get back to it after the sleep interval expires.

If you must control the order of execution of threads, your best bet is to use synchronization controls (described later) or, in some cases, not to use threads at all, but instead to write your own cooperative routines that hand control to each other in a specified order.

Priority

The priority of a thread tells the scheduler how important this thread is. Although the order that the CPU runs a set of threads is indeterminate, the scheduler will lean toward running the waiting thread with the highest priority first. However, this doesn’t mean that threads with lower priority aren’t run (that is, you can’t get deadlocked because of priorities). Lower priority threads just tend to run less often.

Here’s MoreBasicThreads.cpp modified so that the priority levels are demonstrated. The priorities are adjusting by using Thread’s setPriority( ) function.

//: C11:SimplePriorities.cpp

// Shows the use of thread priorities.

//{L} ZThread

#include "zthread/Thread.h"

#include <iostream>

#include <cmath>

using namespace ZThread;

using namespace std;

class SimplePriorities : public Runnable {

  int countDown;

  volatile double d; // No optimization

  int id;

public:

  SimplePriorities(int ident = 0): countDown(5),id(ident){}

  ~SimplePriorities() throw() {

    cout << id << " completed" << endl;

  }

  friend ostream&

  operator<<(ostream& os, const SimplePriorities& sp) {

    return os << "#" << sp.id << " priority: "

      << Thread().getPriority()

      << " count: "<< sp.countDown;

  }

  void run() {

    while(true) {

      // An expensive, interruptable operation:

      for(int i = 1; i < 100000; i++)

        d = d + (M_PI + M_E) / (double)i;

      cout << *this << endl;

      if(--countDown == 0) return;

    }

  }

};

int main() {

  try {

    Thread high(new SimplePriorities);

    high.setPriority(High);

    for(int i = 0; i < 5; i++) {

      Thread low(new SimplePriorities(i));

      low.setPriority(Low);

    }

  } catch(Synchronization_Exception& e) {

    cerr << e.what() << endl;

  }

} ///:~

Here, operator<<( ) is overridden to display the identifier, priority, and countDown value of the task.

You can see that the priority level of thread high is at the highest level, and all the rest of the threads are at the lowest level. We are not using an Executor in this example because we need direct access to the threads in order to set their priorities.

Inside SimplePriorities::run( ), 100,000 repetitions of a rather expensive floating-point calculation are performed, involving double addition and division. The variable d is volatile to ensure that no optimization is performed. Without this calculation, you don’t see the effect of setting the priority levels. (Try it: comment out the for loop containing the double calculations.) With the calculation, you see that thread high is given a higher preference by the thread scheduler. (At least, this was the behavior on a Windows machine.) The calculation takes long enough that the thread scheduling mechanism jumps in, changes threads, and pays attention to the priorities so that thread h gets preference.

You can also read the priority of an existing thread with getPriority( ) and change it at any time (not just before the thread is run, as in SimplePriorities.cpp) with setPriority( ).

Mapping priorities to operating systems is problematic. For example, Windows 2000 has seven priority levels that are not fixed, so the mapping is indeterminate; Sun’s Solaris has 231 levels. The only portable approach is to stick to very large priority granulations, such as the Low, Medium, and High used in the ZThread library.

Sharing limited resources

You can think of a single-threaded program as one lonely entity moving around through your problem space and doing one thing at a time. Because there’s only one entity, you never have to think about the problem of two entities trying to use the same resource at the same time: problems such as two people trying to park in the same space, walk through a door at the same time, or even talk at the same time.

With multithreading, things aren’t lonely anymore, but you now have the possibility of two or more threads trying to use the same resource at once. This can cause two different kinds of problems. The first is that the necessary resources may not exist. In C++, the programmer has complete control over the lifetime of objects, and it’s easy to create threads that try to use objects that get destroyed before those threads complete.

The second problem is that two or more threads may collide when they try to access the same resource at the same time. If you don’t prevent such a collision, you’ll have two threads trying to access the same bank account at the same time, print to the same printer, adjust the same valve, and so on.

This section introduces the problem of objects that vanish while tasks are still using them and the problem of tasks colliding over shared resources. You’ll learn about the tools that are used to solve these problems.

Ensuring the existence of objects

Memory and resource management are major concerns in C++. When you create any C++ program, you have the option of creating objects on the stack or on the heap (using new). In a single-threaded program, it’s usually easy to keep track of object lifetimes so that you don’t try to use objects that are already destroyed.

The examples shown in this chapter create Runnable objects on the heap using new, but you’ll notice that these objects are never explicitly deleted. However, you can see from the output when you run the programs that the thread library keeps track of each task and eventually deletes it, because the destructors for the tasks are called. This happens when the Runnable::run( ) member function completes—returning from run( ) tells the thread that the task is finished.