next up previous
Next: Second approach Up: Creating and managing threads Previous: C interface to threads

First approach

The first idea is the following: assume that we have a class TestClass

Code sample 2  
    class TestClass {
      public:
        void test_function (int i, double d);
    };

and we would like to call test_object.test_function(1,3.1415926) on a newly created thread, where test_object is an object of type TestClass. We then need an object that encapsulates the address of the member function, a pointer to the object for which we want to call the function, and both parameters. This class would be suitable:

Code sample 3  
    struct MemFunData {
        typedef void (TestClass::*MemFunPtr) (int, double);
        MemFunPtr  mem_fun_ptr;
        TestClass *object;
        int        arg1;
        double     arg2;
    };

We further need a function that satisfies the signature required by the operating systems (or ACE, respectively), see Code Sample 1, and that can call the member function if we pass it an object of type MemFunData:

Code sample 4  
    void * start_thread (void *arg_ptr) {
                        // first reinterpret the void* as a
                        // pointer to the object which
                        // encapsulates the arguments
                        // and addresses:
      MemFunData *mem_fun_data
            = reinterpret_cast<MemFunData *>(arg_ptr);
                        // then call the member function:
      (mem_fun_data->object)
            ->*(mem_fun_data->mem_fun_ptr) (mem_fun_data->arg1,
                                            mem_fun_data->arg2);
                        // since the function does not return
                        // a value, we do so ourselves:
      return 0;
    };

Such functions are called trampoline functions since they only serve as jump-off point for other functions.

We can then perform the desired call using the following sequence of commands:

    MemFunData mem_fun_data;
    mem_fun_data.mem_fun_ptr = &TestClass::test_function;
    mem_fun_data.object      = &test_object;
    mem_fun_data.arg1        = 1;
    mem_fun_data.arg2        = 3.1415926;
    
    ACE_Thread_Manager::spawn (&start_thread,
                               (void*)&mem_fun_data);
ACE_Thread_Manager::spawn is the function from ACE that actually calls the operating system and tells it to create a new thread and call the function which it is given as first parameter (here: start_thread) with the parameter which is given as second parameter. start_thread, when called, will then get the address of the function which we wanted to call from its parameter, and call it with the values we wanted as arguments.

In practice, this would mean that we needed a structure like MemFunData and a function like start_thread for each class TestClass and all functions test_function with different signatures. This is clearly not feasible in practice and places an inappropriate burden on the programmer who wants to use multiple threads in his program. Fortunately, C++ offers an elegant way for this problem, in the form of templates: we first define a data type which encapsulates address and arguments for all binary functions:

Code sample 5  
    template <typename Class, typename Arg1, typename Arg2>
    struct MemFunData {
        typedef void (Class::*MemFunPtr) (Arg1, Arg2);
        MemFunPtr  mem_fun_ptr;
        Class     *object;
        Arg1       arg1;
        Arg2       arg2;
    };

Next, we need a function that can process these arguments:

Code sample 6  
    template <typename Class, typename Arg1, typename Arg2>
    void * start_thread (void *arg_ptr) {
      MemFunData<Class,Arg1,Arg2> *mem_fun_data
            = reinterpret_cast<MemFunData<Class,Arg1,Arg2>*>(arg_ptr);
      (mem_fun_data->object)
            ->*(mem_fun_data->mem_fun_ptr) (mem_fun_data->arg1,
                                            mem_fun_data->arg2);
      return 0;
    };

Then we can start the thread as follows:
    MemFunData<TestClass,int,double>  mem_fun_data;
    mem_fun_data.mem_fun_ptr = &TestClass::test_function;
    mem_fun_data.object      = &test_object;
    mem_fun_data.arg1        = 1;
    mem_fun_data.arg2        = 3.1415926;
    
    ACE_Thread_Manager::spawn (&start_thread<TestClass,int,double>,
                               (void*)&mem_fun_data);
Here we first create an object which is suitable to encapsulate the parameters of a binary function that is a member function of the TestClass class and takes an integer and a double. Then we start the thread using the correct trampoline function. It is the user's responsibility to choose the correct trampoline function (i.e. to specify the correct template parameters) since the compiler only sees a void* and cannot do any type checking.

We can further simplify the process and remove the user responsibility by defining the following class and function:

Code sample 7  
    class ThreadManager : public ACE_Thread_Manager {
      public:
        template <typename Class, typename Arg1, typename Arg2>
        static void 
        spawn (MemFunData<Class,Arg1,Arg2> &MemFunData) {
          ACE_Thread_Manager::spawn (&start_thread<Class,Arg1,Arg2>,
                                     (void*)&MemFunData);
        };
    };

This way, we can call
    ThreadManager::spawn (mem_fun_data);
and the compiler will figure out which the right trampoline function is, since it knows the data type of mem_fun_data and therefore the values of the template parameters in the ThreadManager:: spawn function.

The way described above is basically the way which is used in deal.II version 3.0. Some care has to be paid to details, however. In particular, C++ functions often pass references as arguments, which however are not assignable after initialization. Therefore, the MemFunData class needs to have a constructor, and arguments must be set through it. Assume, for example, TestClass had a second member function

        void f (int &i, double &d);
Then, we would have to use MemFunData<TestClass,int&,doubleSPMamp;>, which in a form without templates would look like this:
    struct MemFunData {
        typedef void (TestClass::*MemFunPtr) (int &, double &);
        MemFunPtr  mem_fun_ptr;
        TestClass *object;
        int       &arg1;
        double    &arg2;
    };
The compiler would require us to initialize the references to the two parameters at construction time of the MemFunData object, since it is not possible in C++ to change to which object a reference points to after initialization. Adding a constructor to the MemFunData class would then enable us to write
    int    i = 1;
    double d = 3.1415926;
    MemFunData<TestClass,int&,double&>  
           mem_fun_data (&test_object, i, d, &TestClass::f);
Non-reference arguments could then still be changed after construction. For historical reasons, the pointer to the member function is passed as last parameter here.

The last point is that this interface is only usable for functions with two parameters. Basically, the whole process has to be reiterated for any number of parameters which we want to support. In deal.II, we therefore have classes MemFunData0 through MemFunData10, corresponding to member function that do not take parameters through functions that take ten parameters. Equivalently, we need the respective number of trampoline functions.

Additional thoughts need to be taken on virtual member functions and constant functions. While the first are handled by the compiler (member function pointers can also be to virtual functions, without explicitly stating so), the latter can be achieved by writing MemFunData<const TestClass,int,double>, which would be the correct object if we had declared test_function constant.

Finally we note that it is often the case that one member function starts a new thread by calling another member function of the same object. Thus, the declaration most often used is the following:

    MemFunData<TestClass,int&,double&>  
           mem_fun_data (this, 1, 3.1415926, &TestClass::f);
Here, instead of an arbitrary test_object, the present object is used, which is represented by the this pointer.


next up previous
Next: Second approach Up: Creating and managing threads Previous: C interface to threads
Wolfgang Bangerth
2000-04-20