+++ /dev/null
-\documentclass[11pt]{article}
-\usepackage{a4wide}
-\begin{document}
-
-\begin{center}
- \begin{huge}
- Multithreading support in \texttt{deal.II}
- \end{huge}
-
- \vspace*{0.5cm}
-
- \begin{large}
- Wolfgang Bangerth\\
- University of Heidelberg\\[12pt]
- February 2000
- \end{large}
-\end{center}
-
-
-\begin{abstract}
- In this report, we describe the implementational techniques of
- multithreading support in \texttt{deal.II}, which we use for the
- parallelization of independent operations. Writing threaded programs in
- \texttt{C++} is obstructed by two problems: operating system dependent
- interfaces and that these interfaces are created for \texttt{C} programs
- rather than for \texttt{C++}. We present our solutions to these problems and
- describe first experiences using multithreading in \texttt{deal.II}.
-\end{abstract}
-
-
-\section{Background}
-
-Realistic finite element simulations tend to use enormous amounts of computing
-time and memory. Scientists and programmers have therefore long tried to use
-the combined power of several processors or computers to tackle these
-problems.
-
-The usual approach is to use physically separated computers (e.g. clusters) or
-computing units (e.g. processor nodes in a parallel computer), each of which
-is equipped with its own memory, and split the problem at hand into separate
-parts which are then solved on these computing units. Unfortunately, this
-approach tends to pose significant problems, both for the mathematical
-formulation as well as for the application programmer.
-
-\begin{itemize}
-\item \textit{Implementational problems.} On all available parallel computers,
-communicating data from one computing unit to other ones is extremely slow,
-compared to access to data which is local to a computing unit. It must
-therefore be restricted to the absolute minimum, if it is not to dominate
-the total computing time, in which case one would lose the advantages of
-parallel computing. However, avoiding communication is tedious and often
-makes parallelized programs rather complex. Furtermore. debugging programs
-on parallel computers is difficult.
-
-\item \textit{Mathematical problems.} Splitting the problem into subproblems
-is most often done by subdividing the domain into subdomains and let each
-computing unit solve the problem on its subdomain. However, the solution
-operators of partial differential equations are usually nonlocal; for
-example, a slight change in the right hand side function in a small region
-changes the solution function everywhere. It is therefore obvious that the
-subproblems can not be solved independently, but that some communication
-will be indispensable in any case. In order to reduce the amount of
-communication as much as possible, one usually uses the following iterative
-strategy: solve each subproblem independently; then exchange information
-with other units, such as boundary data of neighboring subdomains, and then
-solve again with the new boundary data. This procedure is repeated until a
-stopping criterion is reached.
-
-This iterative procedure poses mathematical questions: does the iteration
-converge? And if so, can one guarantee an upper bound on the number of
-iterations? While the first question can usually be answered with ``yes'', the
-second one is critical: since non-parallelized solvers do not need this outer
-subproblem iteration, parallelized programs become increasingly inefficient
-with the number of these outer iterations.
-\end{itemize}
-
-For the reasons stated above, parallelized implementations and their
-mathematical background are still subject to intense research. In recent
-years, however, multi-processor machines have been developed, which pose a
-reasonable alternative to small parallelized computers with the advantage of
-simple programming and the possibility to use the same mathematical
-formulation that can also be used for single-processor machines. These
-computers typically have between two and eight processors that can access the
-global memory at equal cost.
-
-Due to this uniform memory access (UMA) architecture, communication can be
-performed in the global memory and is no more costly than access to any other
-memory location. Thus, there is also no more need to change the mathematical
-formulation to reduce communication, and programs using this architecture look
-very much like programs written for single processor machines. The purpose of
-this report is to explain the techniques used in \texttt{deal.II} by which we
-try to program these computers.
-
-
-
-\section{Threads}
-
-The basic entity for programming multi-processor machines are threads. They
-represent parts of the program which are executed in parallel. On
-single-processor machines, they are simulated by letting each thread run for
-some time (usually a few milliseconds) before switching to the next thread. On
-multi-processor machines, threads can truly be executed in parallel. In order
-to let programs use more than one thread (which would be the regular
-sequential program), several aspects need to be covered:
-\begin{itemize}
-\item How do we assign operations to different threads? Of course, operations
- which depend on each other must not be executed in reverse order. This can
- be achieved by only letting independent operations run on different threads,
- or by using synchronisation methods. this is mostly a question of program
- design and thus problem dependent, which is why both aspects will only be
- briefly touched below.
-\item How does the operating system and the whole programming environment
- support this?
-\end{itemize}
-As mentioned, only the second aspect can be canonicalized, so we will treat it
-first.
-
-
-\section{Creating and managing threads}
-
-\subsection{Operating system dependence and ACE}
-
-While all relevant operating systems now support multi-threaded programs, they
-all have different notions on what threads actually are on an operating system
-level, how they shall be managed and created. Even on Unix systems, which are
-usually well-standardized, there are at least three different and mutually
-incompatible interfaces to threads: POSIX threads, Solaris threads, and Linux
-threads. Some operating systems support more than one interface, but there is
-no interface that is supported by all operating systems. Furthermore, other
-systems like Microsoft Windows have interfaces that are incompatible to all
-Unix systems.
-
-Writing multi-threaded programs based on the operating system interfaces is
-therefore something inherently incompatible unless much effort is spent to
-port it to a new system. To avoid this, we chose to use the ACE (Adaptive
-Communication Environment) library which encapsulates the operating system
-dependence and offers a uniform interface to the user. ACE runs on many
-platforms, including most Unix systems and Windows.
-
-We chose ACE over other libraries, since it runs on almost all relevant
-platforms, and since it is the only library which is actively developed by a
-large group around Doug Schmidt at the University of Washington. Furthermore,
-it also is significantly larger than only thread management, offering
-interprocess communication and communication between different computers, as
-well as many other services. Contrary to most other libraries, it therefore
-offers both the ability to support a growing \texttt{deal.II} as well as the
-prospect to support independence also with respect to future platforms.
-
-
-\subsection{\texttt{C} interface to threads versus \texttt{C++}}
-
-While ACE encapsulates almost all of the synchronisation and interprocess
-interface into \texttt{C++} classes, it for some reason does not do so for
-tread creation. Rather it only offers the \texttt{C} interface which is that
-when creating a new thread, a function is called which has the following
-signature:
-\begin{verbatim}
- void * f (void * arg);
-\end{verbatim}
-Thus, only functions which take a single parameter of type \texttt{void*} and
-return a \texttt{void*} may be called. Further, these functions must be global
-or static member functions, as opposed to true member functions of
-classes. This is not in line with the \texttt{C++} philosophy and in fact does
-not fit well into \texttt{deal.II} as well: there is not a single function in
-the library that has this signature.
-
-The task of multi-threading support in \texttt{deal.II} is therefore to
-encapsulate member functions, arbitrary types and numbers of parameters, and
-return types of functions into mechanisms built atop of ACE. This has been
-done twice for \texttt{deal.II}, and we will explain both approaches. At
-present, only the first approach is distributed with \texttt{deal.II}, since
-the second is still experimental and also requires a newer compiler. The
-latter approach, however, has clear advantages over the first one, which is
-why we plan to switch to it in the next major version of \texttt{deal.II}.
-
-
-\subsubsection{First approach}
-
-The first idea is the following: assume that we have a class
-\texttt{TestClass}
-\begin{verbatim}
- class TestClass {
- public:
- void test_function (int i, double d);
- };
-\end{verbatim}
-and we would like to call
-\texttt{test\_object}.\texttt{test\_function(1,3.1415926)} on a newly created
-thread, where \texttt{test\_object} is
-an object of type \texttt{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:
-\begin{verbatim}
- struct MemFunData {
- typedef void (TestClass::*MemFunPtr) (int, double);
- MemFunPtr mem_fun_ptr;
- TestClass *test_object;
- int arg1;
- double arg2;
- };
-\end{verbatim}
-
-We further need a function that satisfies the signature required by the
-operating systems (or ACE, respectively) and that can call the member function
-if we pass it an object of type \texttt{MemFunData}:
-\begin{verbatim}
- void * start_thread (void *arg_ptr) {
- // first reinterpret the void* as a
- // pointer to the object which
- // encapsulates the arguments
- // and addresses:
- MemFunData *MemFunData
- = reinterpret_cast<MemFunData *>(arg_ptr);
- // then call the member function:
- (MemFunData->test_object)
- ->*(MemFunData->mem_fun_ptr) (MemFunData->arg1,
- MemFunData->arg2);
- // since the function does not return
- // a value, we do so ourselves:
- return 0;
- };
-\end{verbatim}
-Such functions are called \textit{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:
-\begin{verbatim}
- MemFunData mem_fun_data;
- mem_fun_data.mem_fun_ptr = &TestClass::test_function;
- mem_fun_data.test_object = &test_object;
- mem_fun_data.arg1 = 1;
- mem_fun_data.arg2 = 3.1415926;
-
- ACE_Thread_Manager::spawn (&start_thread,
- (void*)&mem_fun_data);
-\end{verbatim}
-\texttt{ACE\_Thread\_Manager::spawn} is the function from ACE that actually calls the
-operating system and tells it to call on a new thread the function which it is
-given as first parameter (here: \texttt{start\_thread}) with the parameter
-which is given as second parameter. \texttt{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
-\texttt{MemFunData} and a function like \texttt{start\_thread} for each class
-\texttt{TestClass} and all functions \texttt{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, \texttt{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:
-\begin{verbatim}
- template <typename Class, typename Arg1, typename Arg2>
- struct MemFunData {
- typedef void (Class::*MemFunPtr) (Arg1, Arg2);
- MemFunPtr mem_fun_ptr;
- Class *test_object;
- Arg1 arg1;
- Arg2 arg2;
- };
-\end{verbatim}
-Next, we need a function that can process these arguments:
-\begin{verbatim}
- template <typename Class, typename Arg1, typename Arg2>
- void * start_thread (void *arg_ptr) {
- MemFunData<Class,Arg1,Arg2> *MemFunData
- = reinterpret_cast<MemFunData *>(arg_ptr);
- (MemFunData->test_object)
- ->*(MemFunData->mem_fun_ptr) (MemFunData->arg1,
- MemFunData->arg2);
- return 0;
- };
-\end{verbatim}
-Then we can start the thread as follows:
-\begin{verbatim}
- MemFunData<TestClass,int,double> mem_fun_data;
- mem_fun_data.mem_fun_ptr = &TestClass::test_function;
- mem_fun_data.test_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);
-\end{verbatim}
-Here we first create an object which is suitable to encapsulate the parameters
-of a binary function that takes an integer and a double and is a member
-function of the \texttt{TestClass} class. 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 \texttt{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:
-\begin{verbatim}
- 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);
- };
- };
-\end{verbatim}
-This way, we can call
-\begin{verbatim}
- ThreadManager::spawn (mem_fun_data);
-\end{verbatim}
-and the compiler will figure out which the right trampoline function is.
-
-The way described above is basically the way which is presently used in
-\texttt{deal.II}. Some care has to be paid to details, however. In particular,
-\texttt{C++} functions often pass references as arguments, which however are
-not assignable after initialization. Therefore, the \texttt{MemFunData} class
-needs to have a constructor, and arguments must be set through it. Assume, for
-example, \texttt{TestClass} had a second member function
-\begin{verbatim}
- void f (int &i, double &d);
-\end{verbatim}
-Then, we would have to use \texttt{MemFunData<TestClass,int\&,double\&>},
-which in a form without templates would look like this:
-\begin{verbatim}
- struct MemFunData {
- typedef void (TestClass::*MemFunPtr) (int &, double &);
- MemFunPtr mem_fun_ptr;
- TestClass *test_object;
- int &arg1;
- double &arg2;
- };
-\end{verbatim}
-The compiler would require us to initialize the references to the two
-parameters at construction time of the \texttt{MemFunData} object, since
-it is not possible in \texttt{C++} to change the object which a reference
-points to after initialization. Adding a constructor to the
-\texttt{MemFunData} class would then enable us to write
-\begin{verbatim}
- MemFunData<TestClass,int&,double&>
- mem_fun_data (&test_object,
- 1,
- 3.1415926,
- &TestClass::f);
-\end{verbatim}
-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 \texttt{deal.II}, we therefore have
-classes \texttt{MemFunData0} through \texttt{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 must be made on virtual member functions and constant
-functions. While the first is handled by the compiler (member function
-pointers can also be to virtual functions, without explicitly stating so), the
-latter can be achieved by writing
-\texttt{MemFunData<const TestClass,int,double>}, which would be the correct
-object if \texttt{test\_function} were declared 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:
-\begin{verbatim}
- MemFunData<TestClass,int&,double&>
- mem_fun_data (this, 1, 3.1415926, &TestClass::f);
-\end{verbatim}
-Here, instead of an arbitrary \texttt{test\_object}, the present object is
-used, which is represented by the \texttt{this} pointer.
-
-
-
-\subsubsection{Second approach}
-
-While the approach outlined above works satisfactorily, it has one serious
-flaw: the programmer has to provide the data types of the arguments of the
-member function himself. While this seems to be a simple task, in practice it
-is often not, as will be explained in the sequel.
-
-To expose the problem, we take an example from one of the application programs
-where we would like to call the function
-\begin{verbatim}
- template <int dim>
- void DoFHandler<dim>::distribute_dofs (const FiniteElement<dim> &,
- const unsigned int);
-\end{verbatim}
-on a new thread. Correspondingly, we would need to use
-\begin{verbatim}
- MemFunData2<DoFHandler<dim>, const FiniteElement<dim> &, unsigned int>
- mem_fun_data (dof_handler, fe,
- 0, &DoFHandler<dim>::distribute_dofs);)
-\end{verbatim}
-to encapsulate the parameters. However, if one forgets the \texttt{const}
-specifier on the second template parameter, one receives the following error
-message (using gcc 2.95.2):
-\begin{verbatim}
- test.cc: In method `void InterstepData<2>::wake_up(unsigned int, Interst
- epData<2>::PresentAction)':
- test.cc:683: instantiated from here
- test.cc:186: no matching function for call to `ThreadManager::Mem_Fun_Da
- ta2<DoFHandler<2>,FiniteElement<2> &,unsigned int>::MemFunData2 (DoFHa
- ndler<2> *, const FiniteElement<2> &, int, void (DoFHandler<2>::*)(const
- FiniteElement<2> &, unsigned int))'
- /home/atlas1/wolf/program/newdeal/deal.II/base/include/base/thread_manag
- er.h:470: candidates are: ThreadManager::MemFunData2<DoFHandler<2>,Fin
- iteElement<2> &,unsigned int>::MemFunData2(DoFHandler<2> *, FiniteElem
- ent<2> &, unsigned int, void * (DoFHandler<2>::*)(FiniteElement<2> &, un
- signed int))
- /home/atlas1/wolf/program/newdeal/deal.II/base/include/base/thread_manag
- er.h:480: ThreadManager::MemFunData2<DoFHandler<2>,Fin
- iteElement<2> &,unsigned int>::MemFunData2(DoFHandler<2> *, FiniteElem
- ent<2> &, unsigned int, void (DoFHandler<2>::*)(FiniteElement<2> &, unsi
- gned int))
- /home/atlas1/wolf/program/newdeal/deal.II/base/include/base/thread_manag
- er.h:486: ThreadManager::MemFunData2<DoFHandler<2>,Fin
- iteElement<2> &,unsigned int>::MemFunData2(const ThreadManager::Mem_Fu
- n_Data2<DoFHandler<2>,FiniteElement<2> &,unsigned int> &)
-\end{verbatim}
-
-While the compiler is certainly right to complain, the message is not very
-helpful. Furthermore, since interfaces to functions sometimes change, for
-example by adding additional default parameters that do not show up in usual
-code, programs that used to compile do no more so with messages as shown
-above.
-
-Due to the lengthy and complex error messages, even very experienced
-programmers usually need between five and ten minutes until they get an
-expression like this correct. In most cases, they don't get it right in the
-first attempt, so the time used for the right declaration dominates the whole
-setup of starting a new thread. To circumvent this bottleneck at least in most
-cases, we chose to implement a second strategy at encapsulating the parameters
-of member functions. This is done in several steps: first let the compiler
-find out about the right template parameters, then encapsulate the parameters,
-use the objects, and finally solve some technical problems with virtual
-constructors. We will treat these steps sequentially in the following.
-
-
-\paragraph{Finding the correct template parameters.}
-\texttt{C++} offers the possibility of templated functions that deduce their
-template arguments themselves. This can be used as follows: assume we have a
-function class
-\begin{verbatim}
- template <typename Class, typename Arg1, typename Arg2>
- class MemFunData { ... };
-\end{verbatim}
-as above, and a function
-\begin{verbatim}
- template <typename Class, typename Arg1, typename Arg2>
- MemFunData<Class,Arg1,Arg2>
- deduce_types (void (Class::*mem_fun_ptr)(Arg1, Arg2)) {
- return MemFunData<Class,Arg1,Arg2> (mem_fun_ptr);
- };
-\end{verbatim}
-If we call this function like this:
-\begin{verbatim}
- deduce_types (&TestClass::test_function);
-\end{verbatim}
-then it can unambiguously determine the template parameters to be
-\texttt{Class=TestClass}, \texttt{Arg1=int}, \texttt{Arg2=double}.
-
-\paragraph{Encapsulating the parameters.}
-We should
-not try to include the arguments right away, for example by declaring
-\texttt{deduce\_types}
-\begin{verbatim}
- template <typename Class, typename Arg1, typename Arg2>
- MemFunData<Class,Arg1,Arg2>
- deduce_types (void (Class::*mem_fun_ptr)(Arg1, Arg2),
- Arg1 arg1,
- Arg2 arg2,
- Class object) {
- return MemFunData<Class,Arg1,Arg2> (mem_fun_ptr, object, arg1, arg2);
- };
-\end{verbatim}
-The reason is that for template functions, no parameter promotion is
-performed. Thus, if we called this function as in
-\begin{verbatim}
- deduce_types (&TestClass::test_function,
- 1, 3,
- test_object);
-\end{verbatim}
-then the compiler would refuse this since from the function pointer it must
-deduce that \texttt{Arg2=double}, but from the parameter ``3'' it must assume
-that \texttt{Arg2=int}. The resulting error message would be similarly lengthy
-as the one shown above.
-
-One could instead write \texttt{MemFunData} like this:
-\begin{verbatim}
- template <typename Class, typename Arg1, typename Arg2>
- class MemFunData {
- public:
- typedef void (Class::*MemFunPtr)(Arg1, Arg2);
-
- MemFunData (MemFunPtr mem_fun_ptr_) {
- mem_fun_ptr = mem_fun_ptr_;
- };
-
- void collect_args (Class *object_,
- Arg1 arg1_,
- Arg2 arg2_) {
- object = object_;
- arg1 = arg1_;
- arg2 = arg2_;
- };
-
- MemFunPtr mem_fun_ptr;
- Class *object;
- Arg1 arg1;
- Arg2 arg2;
- };
-\end{verbatim}
-One would then create an object of this type including the parameters to be
-passed as follows:
-\begin{verbatim}
- deduce_types (&TestClass::test_function).collect_args(1, 3,
- test_object);
-\end{verbatim}
-Here, the first function call creates an object with the right template
-parameters, and the second one, calling a member function, fills in the
-function arguments.
-
-Unfortunately, this way does not work: if one or more of the parameter types
-is a reference, then the respective reference variable needs to be initialized
-by the constructor, not by \texttt{collect\_args}. It needs to be known which
-object the reference references at construction time, since later on only the
-referenced object can be assigned, not the reference itself anymore.
-
-Since we feel that we are close to a solution, we introduce one more
-indirection, which indeed will be the last one:
-\begin{verbatim}
- template <typename Class, typename Arg1, typename Arg2>
- class MemFunData {
- public:
- typedef void (Class::*MemFunPtr)(Arg1, Arg2);
-
- MemFunData (MemFunPtr mem_fun_ptr_,
- Class *object_,
- Arg1 arg1_,
- Arg2 arg2_) :
- mem_fun_ptr (mem_fun_ptr_),
- object (object_),
- arg1 (arg1_),
- arg2 (arg2_) {};
-
- MemFunPtr mem_fun_ptr;
- Class *object;
- Arg1 arg1;
- Arg2 arg2;
- };
-
-
- template <typename Class, typename Arg1, typename Arg2>
- struct Intermediate {
- typedef void (Class::*MemFunPtr)(Arg1, Arg2);
-
- Intermediate (MemFunPtr mem_fun_ptr_) {
- mem_fun_ptr = mem_fun_ptr_;
- };
-
-
- MemFunData<Class,Arg1,Arg2>
- collect_args (Class *object_,
- Arg1 arg1_,
- Arg2 arg2_) {
- return MemFunData<Class,Arg1,Arg2> (mem_fun_ptr, object,
- arg1, arg2);
- };
-
- MemFunPtr mem_fun_ptr;
- };
-
-
- template <typename Class, typename Arg1, typename Arg2>
- Intermediate<Class,Arg1,Arg2>
- deduce_types (void (Class::*mem_fun_ptr)(Arg1, Arg2)) {
- return Intermediate<Class,Arg1,Arg2> (mem_fun_ptr);
- };
-\end{verbatim}
-
-Now we can indeed write
-\begin{verbatim}
- deduce_types (&TestClass::test_function).collect_args(1, 3,
- test_object);
-\end{verbatim}
-The first call creates an object of type \texttt{Intermediate<...>} with the
-right parameters, while the second call, a call to a member function of that
-intermediate class, generates the final object we are interested in, including
-the member function pointer and all necessary parameters. Since
-\texttt{collect\_args} already has its template parameters fixed from
-\texttt{deduce\_types}, it can convert between data types.
-
-
-\paragraph{Using these objects.} Now we have an object of the correct type
-automatically generated, without the need to type in any template parameters
-by hand. What can we do with that? First, we can't assign it to a variable of
-that type. Why? Since we would then have to write the data type of that
-variable by hand, which is exactly what we wanted to avoid. However, we can do
-some such thing if the variable to which we assign the result is of a type
-which is a base class of \texttt{MemFunData<...>}. Unfortunately, the
-parameters that \texttt{MemFunData<...>} encapsulates depend on the
-template parameters, so the respective variables in which we store can only be
-in the derived class and could not be copied when we assign the variable to a
-base class object, since that does not have these variables.
-
-What can we do here? Assume we have the following structure in the library:
-\begin{verbatim}
- class MemFunBase {};
-
- template <...> class MemFunData : public MemFunBase
- { /* as above */ };
-
- class MemFunEncapsulation {
- public:
- MemFunEncapsulation (MemFunBase *mem_fun_base)
- : mem_fun_base (mem_fun_base) {};
- MemFunBase *mem_fun_base;
- };
-
-
- template <typename Class, typename Arg1, typename Arg2>
- MemFunEncapsulation
- Intermediate<Class,Arg1,Arg2>::collect_args (Class *object_,
- Arg1 arg1_,
- Arg2 arg2_) {
- return new MemFunData<Class,Arg1,Arg2> (mem_fun_ptr, object,
- arg1, arg2);
- };
-\end{verbatim}
-
-Now, the call to \texttt{deduce\_types(...).collect\_args(...)} generates an
-object of type \texttt{MemFunEncapsulation}, which in turn stores a pointer to
-an object of type \texttt{MemFunBase}, here to \texttt{MemFunData<...>} with
-the correct template parameters. We can assigne the result to a variable the
-type of which does not contain any template parameters any more, as desired:
-\begin{verbatim}
- MemFunEncapsulation
- mem_fun_encapsulation = deduce_types (&TestClass::test_function)
- .collect_args(1, 3, test_object);
-\end{verbatim}
-
-But how can we start a thread with this object if we have lost the full
-information about the data types? This can be done as follows: add am abstract
-virtual function \texttt{get\_trampoline()} to \texttt{MemFunBase} which is
-implemented in the derived classes
-\begin{verbatim}
- class MemFunBase {
- public:
- typedef void * (*ThreadEntryPoint) (void *);
- virtual ThreadEntryPoint get_trampoline () = 0;
- };
-
- template <...>
- class MemFunData : public MemFunBase {
- public:
- virtual ThreadEntryPoint get_trampoline () {
- return &start_thread;
- };
-
- static void * start_thread (void *args) {
- // do the same as in start_thread above
- }
- };
-
-
- void spawn (MemFunEncapsulation &mem_fun_encapsulation) {
- ACE_Thread_Manager::spawn (mem_fun_encapsulation.mem_fun_base
- ->get_trampoline()),
- (void*)&mem_fun_base);
- };
-\end{verbatim}
-The call to \texttt{get\_trampoline} gets us the right thread starter function
-which knows that the parameter it gets has the right data type to which it can
-be casted. Thus, we can now write the whole sequence of function calls:
-\begin{verbatim}
- MemFunEncapsulation
- mem_fun_encapsulation = deduce_types (&TestClass::test_function)
- .collect_args(1, 3, test_object);
- spawn (mem_fun_encapsulation);
-\end{verbatim}
-This solves our problem in that no template parameters need to be specified by
-hand any more. The only source for lengthy compiler error messages is if the
-parameters to \texttt{collect\_args} arg in the wrong order or can not be
-casted to the parameters of the member function which we want to call. These
-problems, however, are much more unlikely in our experience, and are also much
-quicker sorted out.
-
-
-\paragraph{Virtual constructors.} While the basic techniques have been fuly
-developed now, there are some aspects which we still have to take care of. The
-basic problem here is that the \texttt{MemFunEncapsulation} objects store a
-pointer to an object that was created using the \texttt{new} operator. To
-prevent a memory leak, we need to destroy this object at some time, preferably
-in the destructor of \texttt{MemFunEncapsulation}:
-\begin{verbatim}
- MemFunEncapsulation::~MemFunEncapsulation () {
- delete mem_fun_base;
- };
-\end{verbatim}
-However, what happens if we have copied the object before? In particular,
-since this is always the case using the functions above: \texttt{collect\_args}
-generates a temporary object of type \texttt{MemFunEncapsulation}, but there
-could be other sources of copies as well. If we do not take special measures,
-only the pointer to the object is copied around, and we end up with stale
-pointers pointing to invalid locations in memory once the first object has
-been destroyed. What we obviously need to do when copying objects of type
-\texttt{MemFunEncapsulation} is to not copy the pointer but to copy the object
-which it points to. Unfortunately, the following copy constructor is not
-possible:
-\begin{verbatim}
- MemFunEncapsulation::MemFunEncapsulation (const MemFunEncapsulation &m) {
- mem_fun_base = new MemFunBase (*m.mem_fun_base);
- };
-\end{verbatim}
-The reason, of course, is that we do not want to copy that part of the object
-belonging to the abstract base class (besides the fact that the compiler won't
-let us do so, since \texttt{MemFunEncapsulation} has abstract virtual
-functions). But we can emulate something like this in the following way (the
-programming idiom is called ``virtual constructors''):
-\begin{verbatim}
- class MemFunBase {
- public:
- // as above
-
- virtual MemFunBase * clone () const = 0;
- };
-
- template <...>
- class MemFunData : public MemFunBase {
- public:
- // as above
-
- // copy constructor:
- MemFunData (const MemFunData<...> &mem_fun_data) {...};
-
- // clone the present object, i.e.
- // create an exact copy:
- virtual MemFunBase * clone () const {
- return new MemFunData<...>(*this);
- };
- };
-
-
- MemFunEncapsulation::MemFunEncapsulation (const MemFunEncapsulation &m) {
- mem_fun_base = m.mem_fun_base->clone ();
- };
-\end{verbatim}
-Thus, whenever the \texttt{MemFunEncapsulation} object is copied, it creates a
-copy of the object it harbours (the \texttt{MemFunData<...>} object) which it
-owns. When the destructor is called, it is free to delete its copy without
-affecting other objects (from which it may have been copied, or to which it
-was copied).
-
-\end{document}
-
-%%% Local Variables:
-%%% mode: latex
-%%% TeX-master: t
-%%% End: