Introduction
One of the most common errors in multi-thread programming is accessing invalid data in different threads. Although this is obviously the programmer’s fault, this kind of error is too common in reality to leave it as it is.
In this article, a simple remedy for this issue is presented. The presented solution is a set of supplementary classes. These classes serve a way to tell whether or not a variable is valid which does not exist in the C++ language originally.
Before you read this article, there are some terminologies you should check:
LitmusV
means this set of classes or this system.
LitmusV
variable means a variable which is managed by LitmusV
. Therefore, the variable can be verified by using LitmusV
methods.
LitmusV
class means a class which is managed by LitmusV
.
LitmusV
object means an object which is managed by LitmusV
.
Motivation: Why Do I Need It?
The following code is a common erroneous thread use:
ULONG __stdcall CThreadTestDlg::ThreadFunc(LPVOID lParam)
{
CThreadTestDlg* pThis = (CThreadTestDlg*)lParam;
return 0;
}
void CThreadTestDlg::OnBnClickedOk()
{
DWORD tid;
::CreateThread( NULL, NULL, ThreadFunc, (LPVOID)this, 0, &tid );
}
Although it looks logically right, this code can cause runtime errors when an instance of CThreadTestDlg
is released before the thread ends.
There are two possible approaches to avoid that error. One method is delaying the variables’ destruction process until threads end or terminating threads before variables are released. Another method is tracing validity of variables and checking its validity before it is accessed. In other words, the latter solution does not touch threads and selectively allows to access variables which are not yet released.
The first approach is commonly used in multi-threaded programming and well known solution. However, the application can be seen as an unresponsive program when you use the delaying destruction process method. Additionally, if the application terminates when it is releasing, the thread would encounter unstable state which can increase a number of unreleased objects. Also, forced terminating thread can cause additional variable dependency problem if the thread has variables that are shared with another thread.
Therefore, the need of the latter method emerged. However, tracing validity of variables including objects is needed to implements the latter method. Additionally, additional functions that check the validity of variables must be offered. Moreover, the offered features must have lock and unlock features to avoid runtime exceptions that come from multi-thread environment.
The LitmusV
gives simple solutions for all the challenges mentioned above.
Background
To use it, basic knowledge of C++ language is enough.
To understand it, stack management of C/C++ language and basic knowledge of template class are required.
Caution: Things You Must Know Before You Use It
The LitmusV
is not a panacea for accessing invalid variables in threads.
Additionally, it might have logical flaws in some situations. Although that situation is very unrealistic, I cannot say it never happens.
Therefore, if you are in trouble with this class, I strongly recommend you read this article fully and think about your problem is the case which this article describes.
Additionally, it does not contain a solution for pointer variables. This is because dealing pointer variables are usually easier than local variables and member variables which are allocated on the stack. Therefore, the solution for pointer variables will be covered later if it is possible and I have time to deal with it.
Usages: How To Use It?
By following the below instructions, you can use LitmusV
properly. There are two methods to make a LitmusV
variable: Using macro functions for local variables and using inheritance class.
Method I: Using Macro Functions for Local Variables
|
1. |
The first thing you should do is include “LitmusV.h” file. |
|
2. |
CLitmusVManager::Init() and CLitmusVManager::Deinit() should be called at the beginning of and at the end of the program. If you are writing a MFC application, you can call these functions at the very beginning and end of CxxApp::InitInstance() . |
|
3. |
To declare variables, _LV_DECL_LOCAL macro is used. The macro has two parameters: variable type and variable name. For example, if you want to declare like “int temp ;”, you can declare like “_LV_DECL_LOCAL( int, temp ); ”. You can declare not only local variables but also class member variables in this way. |
|
4. |
To use variable, _LV macro must be used since the type of variables declared with _LV_DECL_LOCAL differs from the original type that is the first parameter of _LV_DECL_LOCAL . Also, _LV macro must be used in CreateThread or AfxBeginThread function to pass variable to the new thread. |
|
5. |
To access variable in the threads, _LV_EXTRACT is used. If you receive parameter in thread like “int* pVarFromCaller = (int*)lParam; ”, then you can use _LV_EXTRACT like “_LV_EXTRACT(int*, pVarFromCaller, lParam, uVarID, bVarError); ”. The first three parameters are similar to the original code you might use. uVarID has variant ID which is used to check the validity of variable. bVarError stores the result of _LV_EXTRACT macro. If the variable was already released before _LV_EXTRACT is executed, bVarError is FALSE . |
|
6. |
In the thread, you can specify a block to ensure the validity of variable by using _LV_USEHERE . It checks the validity of variable and if the variable is valid, it locks the variable until the block ends. Therefore, the validity of variable is ensured in the specified block. If the variable was already released, the second parameter, which is bError , is set to FALSE to choose alternative path (usually exit the thread safely). |
The following is an example code for a simple thread task:
UINT CThreadTestDlg::ThreadFunc(LPVOID lParam)
{
_LV_EXTRACT(int*, pVarFromCaller, lParam, uVarID, bVarError);
if( bVarError ) {
OutputDebugString(_T("Error occurs."));
return 0;
}
CString s;
while(1) {
{ _LV_USEHERE( uVarID, bError );
if( bError ) {
OutputDebugString(_T("variable IS NOT VALID"));
} else {
s.Format(_T("variable = %d, (ID = %u)"),
(*pVarFromCaller)++, uVarID);
OutputDebugString(s);
}
}
Sleep(1000);
}
return 0;
}
class CTestObj {
public:
_LV_DECL_LOCAL( int, temp ); };
void CThreadTestDlg::OnBnClickedButton1()
{
_LV_DECL_LOCAL( int, temp ); AfxBeginThread(ThreadFunc,
(LPVOID)&_LV(temp));
CTestObj obj;
_LV(obj.temp) = 1024; AfxBeginThread(ThreadFunc,
(LPVOID)&_LV(obj.temp));
AfxMessageBox(_T("Temp will not be released until this message closed."));
}
This method is simple and easy to use. Moreover, you can use it to make LitmusV
variable even in the class. Therefore, you can use this method everywhere you want to declare variable.
However, using macros every time you access the variable is somewhat annoying. Therefore, method II is suggested to minimize use of macros.
Method II: Using Inheritance Class
The above example is somewhat complex since macros are needed every time you access variables.
LitmusV
offers a class to simplify this annoying task. The below instructions are what you should do in order to make a LitmusV
variable.
|
1. |
“_LV_INHERIT_HDR ” has to be added to the class declaration in order to inherit it. |
|
2. |
“_LV_INHERIT_SRC ” has to be added on the constructor in order to initialize the class. |
|
3. |
You can use “this pointer of the object” without any modification of source code. |
|
4. |
To use this pointer in threads, _LV_EXTRACT must be used like “_LV_EXTRACT(CPopupDlg*, pThis, lParam, uThisID, bError); ”. Since method II is only for a class, another expression of _LV_EXTRACT may not be used frequently. |
|
5. |
This step is exactly the same as step six of method I. - In the thread, you can specify a block to ensure the validity of variable by using _LV_USEHERE . It checks the validity of variable and if the variable is valid, it locks the variable until the block ends. Therefore, the validity of variable is ensured in the specified block. If the variable was already released, the second parameter, which is bError , is set to FALSE to choose alternative path (usually exit the thread safely). |
The following is an example code for a simple thread task. The code given below is from the header file:
#pragma once
#include "LitmusV.h"
class CPopupDlg : public CDialogEx, _LV_INHERIT_HDR
{
DECLARE_DYNAMIC(CPopupDlg)
public:
CPopupDlg(CWnd* pParent = NULL);
.
.
.
private:
static UINT ThreadFunc(LPVOID lParam);
public:
CListBox m_lstInfo;
virtual BOOL OnInitDialog();
};
The below code is from the source file:
#include "stdafx.h"
.
.
.
#include "LitmusV.h"
IMPLEMENT_DYNAMIC(CPopupDlg, CDialogEx)
CPopupDlg::CPopupDlg(CWnd* pParent )
: CDialogEx(CPopupDlg::IDD, pParent), _LV_INHERIT_SRC
{
}
.
.
.
void CPopupDlg::OnBnClickedOk()
{
AfxBeginThread(ThreadFunc, (LPVOID)this);
}
UINT CPopupDlg::ThreadFunc(LPVOID lParam)
{
_LV_EXTRACT(CPopupDlg*, pThis, lParam, uThisID, bError);
if( bError ) {
OutputDebugString(_T("Error occurs during the extraction process."));
return 0;
}
int nCount = 0;
CString szText;
while(1) {
{ _LV_USEHERE( uThisID, bError );
if( bError ) {
OutputDebugString(_T("this pointer IS NOT VALID"));
} else {
szText.Format(_T("Count : %d [this = %X]"),
++nCount, pThis);
pThis->m_lstInfo.AddString(szText);
}
}
Sleep(1000);
}
return 0;
}
Method II is easy and simple. However, this can be applied to only classes, not the primitive type variables such as int
, float
, char
, etc.
Therefore, I recommend you to use method I for primitive type variables and to use method II for classes.
Implementation
LitmusV
The LitmusV
consists of three parts: LitmusV
Support Classes, LitmusV
Manager and its macro functions.
LitmusV Support Classes
To trace variables’ life cycle, detecting releasing of variables must be implemented. LitmusV
Support Classes do this by using template classes. Basically, it manipulates the fact that C++ compiler calls destructor during the object is releasing. Additionally, the template class stores the Variable ID.
Although using Variable ID to identify variable is annoying, identifying variable by its address is practically impossible. This is because sometimes different variables can be assigned at the same address.
For example, the address of tempA
and the address of tempB
are the same because the stack pointer of caller is the same during the loop. However, tempA
and tempB
are definitely different variables and must be treated as different variables.
void funcA()
{
int tempA = 1024;
printf("The address of tempA(int) : %X\n", &tempA);
}
void funcB()
{
float tempB = 4096.1024;
printf("The address of tempB(float) : %X\n", &tempB);
}
int main(int argc, char *argv[])
{
for(int i = 0; i < 2; i++ ) {
if(i) funcA();
else funcB();
} }
The following output is the result of the above code:
- The address of
tempB(float)
: 28FF14
- The address of
tempA(int)
: 28FF14
By adding Variable ID to the original variable, now you do not have to pass Variable ID since the ID can be extracted by reading the previous 4 bytes of an original variable.
Although variable IDs are attached to the original value, the problem is that variable ID can also be overwritten if a new variable is allocated at the same address. To avoid this problem, it extracts variable ID at the very beginning of a thread. However, extracting variable ID on the thread can theoretically introduce a misidentifying problem. For example, it would be possible that a variable was released and new variable allocated during the creation of the thread. In other words, since there is small time gap between the execution of CreateThread
or AfxBeginThread
function and the actual beginning of thread function, it would be possible that a variable passed to CreateThread
or AfxBeginThread
can be replaced with a new one. Therefore, variable ID extracted in the thread can differ from passed variable’s ID, resulting in logical errors in runtime. This situation is shown in the following code and picture in detail:
void CThreadTestDlg::OnBnClickedButton2()
{
{
_VL_DECL_LOCAL( int, tempA );
_VL(tempA) = 100;
AfxBeginThread(ThreadFunc, (LPVOID)&(_VL(tempA)));
}
{
_VL_DECL_LOCAL( int, tempB );
_VL(tempB) = 1000;
AfxMessageBox(_T("tempB will not be released until
this message is closed."));
}
}
The above topics are related to apply LitmusV
on local variables. In case of the objects, not primitive type variables such as int
, float
, char
, etc., LitmusV
serves a different way to apply it. By adding "_LV_INHERIT_HDR
” on the class definition and “_LV_INHERIT_SRC
” on the class constructor, the class can be traceable and verifiable. Although declaring a LitmusV
class is more complex (it requires 2 additional expressions), the object can be accessed without “_LV
” macro which means that it does not require any additional changes on the legacy code. The following code is an example for declaring a LitmusV
class.
class CPopupDlg : public CDialogEx, _LV_INHERIT_HDR
{
.
.
.
};
CPopupDlg::CPopupDlg(CWnd* pParent )
: CDialogEx(CPopupDlg::IDD, pParent), _LV_INHERIT_SRC
{
.
.
.
}
Similar to the case of local variables, _LV_INHERIT_xxx
macros create a class inherited from CLitmusVObject
class. However, you do not have to use _LV
macro to access a LitmusV
object.
CLitmutVLock
serves lock
and unlock
functions. It locks the given variable when it is declared. Then, it unlocks the locked variable when the block ends.
Macro Functions
To make this class accessible, LitmusV
offers several macros:
_LV_DECL_LOCAL
: It declares a local variable as a LitmusV
variable.
_LV_INHERIT_HDR
and _LV_INHERIT_SRC
: It makes a LitmusV
class.
_LV
: It allows a programmer to access LitmusV
variable.
_LV_ISVALID
: It just checks the validity of variable.
_LV_EXTRACT
: It is used to extract variable ID and information from the variable in threads.
_LV_USEHERE
: It checks the validity of variable and if the variable is valid, it locks the variable.
All macros are defined at LitmusV.h. Therefore, if you are curious about the above macros, please refer to the “LitmusV.h” file.
LitmusV Manager
To trace the validity of variables, storing variables information such as an address of a variable is needed. The LitmusV
Manager(LVM) is the object for storing and managing variables’ information.
class CLitmusVManager
{
public:
static unsigned int GetVarID(void* ptr);
static void Init();
static void Deinit();
static BOOL IsAlive(unsigned int uVarID);
static unsigned int Register(void* ptr, void* pLitmusObj, LV_TYPE vlType);
static void Unregister(unsigned int uVarID);
static BOOL Lock(unsigned int uVarID);
static void Unlock(unsigned int uVarID);
private:
static CPtrArray m_aryVariable;
static CPtrArray m_aryVariableLitmusObj;
static CDWordArray m_aryVariableID;
static CWordArray m_aryVariableType;
static CPtrArray m_aryVariableCriticalSection;
static BOOL m_bInited;
static CRITICAL_SECTION m_csAryVariable;
static unsigned int m_uVarID;
};
It has five lists to store variables’ information.
|
1. |
m_aryVariable saves addresses of variables. |
|
2. |
m_aryVariableID saves identification numbers of variables which are related to each address of variable. |
|
3. |
m_aryVariableCriticalSection holds CRITICAL_SECTION object for each variable in order to serve lock and unlock features. |
|
4. |
m_aryVariableType is a storage for variable type. A variable type can be “LV_TYPE_LOCAL ” or “LV_TYPE_OBJECT ”. LV_TYPE_LOCAL indicates that the variable is declared with “_LV_DECL_LOCAL ” and LV_TYPE_OBJECT means that the variable is declared with _LV_INHERIT_xxx . |
|
5. |
m_aryVariableLitmusObj stores addresses of “CLitmusVObject ” objects. As some people might know, an inherited object has different “this ” pointers for each inherited class. |
Member functions are really simple. All functions except GetVarID
and Register
, have the Variable ID parameter.
Once the variable is registered by using Register
function, the registered variable must be accessed by using Variable ID(uVarID
). The Variable ID(m_uVarID
) is an unsigned integer number that starts from 1
(the initial value is 1
).
“IsAlive
” just checks validity of the given variable. Although this is the core function of LitmusV
, this is not usually used since “Lock
” function does the same work with a lock feature.
“Lock
” and “Unlock
” functions allow programs to access variables exclusively.
“Register
” and “Unregister
” functions manage what variables are under the control of LitmusV
Manager.
“Init
” and “Deinit
” functions must be called at the very beginning of the program and at the very end of the program to initialize and de-initialize the LitmusV
class. “Init
” function does not have to be called since all LitmusV
methods try to initialize LitmusV
class if it is not yet initialized.
History
- 2011-07-07: Initial release