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

Synchronizing entire classes

The ZThread library also provides a GuardedClass template to automatically create a synchronized wrapper for an entire class. This means that every member function in the class will automatically be guarded:

//: C11:SynchronizedClass.cpp

//{L} ZThread

#include "zthread/GuardedClass.h"

using namespace ZThread;

class MyClass {

public:

  void func1() {}

  void func2() {}

};

int main() {

  MyClass a;

  a.func1(); // not synchronized

  a.func2(); // not synchronized

  GuardedClass<MyClass> b(new MyClass);

  // Synchronized calls, only one thread at a time allowed:

  b->func1();

  b->func2();

} ///:~

Object a is a not synchronized, so func1( ) and func2( ) can be called at any time by any number of threads. Object b is protected by the GuardedClass wrapper, so each member function is automatically synchronized and only one function per object can be called any time.

The wrapper locks at a class level of granularity, which may affect performance in some cases. If a class contains some unrelated functions, it may be better to synchronize those functions internally with two different locks. However, if you find yourself doing this, it means that one class contains groups of data that may not be strongly associated. Consider breaking the class into two classes.

Guarding all member functions of a class with a mutex does not automatically make that class thread-safe. You must carefully consider all threading issues in order to guarantee thread safety.

Thread local storage

A second way to eliminate the problem of tasks colliding over shared resources is to eliminate the sharing of variables, which can be done by creating different storage for the same variable, for each different thread that uses an object. Thus, if you have five threads using an object with a variable x, thread local storage automatically generates five different pieces of storage for x. Fortunately, the creation and management of thread local storage is taken care of automatically by ZThread’s ThreadLocal template, as seen here:

//: C11:ThreadLocalVariables.cpp

// Automatically giving each thread its own storage.

//{L} ZThread

#include "zthread/Thread.h"

#include "zthread/Mutex.h"

#include "zthread/Guard.h"

#include "zthread/ThreadedExecutor.h"

#include "zthread/Cancelable.h"

#include "zthread/ThreadLocal.h"

#include "zthread/CountedPtr.h"

#include <iostream>

using namespace ZThread;

using namespace std;

class ThreadLocalVariables : public Cancelable {

  ThreadLocal<int> value;

  bool canceled;

  Mutex lock;

public:

  ThreadLocalVariables() : canceled(false) {

    value.set(0);

  }

  void increment() { value.set(value.get() + 1); }

  int get() { return value.get(); }

  void cancel() {

    Guard<Mutex> g(lock);

    canceled = true;

  }

  bool isCanceled() {

    Guard<Mutex> g(lock);

    return canceled;

  }

};

class Accessor : public Runnable {

  int id;

  CountedPtr<ThreadLocalVariables> tlv;

public:

  Accessor(CountedPtr<ThreadLocalVariables>& tl, int idn)

  : id(idn), tlv(tl) {}

  void run() {

    while(!tlv->isCanceled()) {

      tlv->increment();

      cout << *this << endl;

    }

  }

  friend ostream&

    operator<<(ostream& os, Accessor& a) {

    return os << "#" << a.id << ": " << a.tlv->get();

  }

};

int main() {

  cout << "Press <Enter> to quit" << endl;

  try {

    CountedPtr<ThreadLocalVariables>

      tlv(new ThreadLocalVariables);

    const int sz = 5;

    ThreadedExecutor executor;

    for(int i = 0; i < sz; i++)

      executor.execute(new Accessor(tlv, i));

    cin.get();

    tlv->cancel(); // All Accessors will quit

  } catch(Synchronization_Exception& e) {

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

  }

} ///:~

When you create a ThreadLocal object by instantiating the template, you are only able to access the contents of the object using the get( ) and set( ) functions. The get( ) function returns a copy of the object that is associated with that thread, and set( ) inserts its argument into the object stored for that thread, returning the old object that was in storage. You can see this is use in increment( ) and get( ) in ThreadLocalVariables.

Since tlv is shared by multiple Accessor objects, it is written as Cancelable so that the Accessors can be signaled when we want to shut the system down.

When you run this program, you’ll see evidence that the individual threads are each allocated their own storage.

Terminating tasks

In previous examples, we have seen the use of a "quit flag" or the Cancelable interface in order to terminate a task. This is a reasonable approach to the problem. However, in some situations the task must be terminated more abruptly. In this section, you’ll learn about the issues and problems of such termination.

First, let’s look at an example that not only demonstrates the termination problem but is also an additional example of resource sharing. To present this example, we’ll first need to solve the problem of iostream collision

Preventing iostream collision

You may have noticed in previous examples that the output is sometimes garbled. The problem is that C++ iostreams were not created with threading in mind, and so there’s nothing to keep one thread’s output from interfering with another thread’s output.

To solve the problem, we need to create the entire output packet first and then explicitly decide when to try to send it to the console. One simple solution is to write the information to an ostringstream and then use a single object with a mutex as the point of output among all threads, to prevent more than one thread from writing at the same time:

//: C11:Display.h

// Prevents ostream collisions

#ifndef DISPLAY_H

#define DISPLAY_H

#include "zthread/Mutex.h"

#include "zthread/Guard.h"