65.9K
CodeProject 正在变化。 阅读更多。
Home

使用全局/静态 void 返回函数创建线程,该函数接受任意类型和数量的参数

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (1投票)

2007年11月9日

CPOL

3分钟阅读

viewsIcon

32401

downloadIcon

121

这是一个宏,它接受一个返回 void 的全局/静态过程的名称,然后是传递给该过程的参数列表,并在新线程中调用带有提供参数的过程。

引言

这是一个非常普遍的问题。比如,我有一个函数像这样

void func(int a, int b)
{
     printf("\n a = %d, b = %d",a,b);
}

我需要在新线程中调用这个函数。

最简单的程序是

  1. 制作一个这样的结构
  2. struct funcargs
    {
        int m_a;
        int m_b;
    };
  3. 然后像这样动态创建一个该结构的实例
  4. funcargs *argobjptr = new funcargs;
  5. 像这样将参数放入对象中
  6. argobjptr->m_a = a; 
    argobjptr->m_b = b;
  7. 创建一个另一个包装函数,用于与 CreateProcess 或任何类似方法一起使用,如下所示
  8. DWORD WINAPI ThreadProc(LPVOID lpParameter)
    {
        funcargs *argobjptr = (funcargs *)lpParameter;
        func(argobjptr->m_a, argobjptr->m_b);
        delete argobjptr
        return 0;
    }
  9. 最后,像这样使用之前创建的“argobjptr”对象调用 ThreadProc 来创建线程
  10. CreateThread( NULL, 0, ThreadProc, argobjptr, 0, NULL):

所以,总的来说,我们必须经历这种打包参数到临时包装对象的麻烦,然后在某个包装函数中解包它们,因为所有这些创建线程的 API 都限制我们只能使用具有以下语法的函数来创建线程

DWORD WINAPI ThreadProc(LPVOID lpParameter)

如果有一个 API 可以从任何返回任意内容、接受任意数量和类型的参数的函数创建线程,那就太好了。

不幸的是,这非常困难,因为问题的范围非常广泛;我们需要简单的函数,返回一些对象/引用/指针/void,然后它们可以接受一些参数/引用/指针,并且参数的数量可能因每个函数而异。

然后函数可以是内联/静态/类成员/虚拟的......

我试图在一定程度上解决这个问题,我创建了一个宏,它只接受函数名及其参数(任意数量和类型)作为参数,并在新线程中调用带有给定参数的函数

就像对于上面的函数一样,代码将非常简单

CALL_IN_THREAD(func, 4, 5);

当然,这也有一些限制

  • 首先,宏只适用于返回 void 的全局/静态函数。
  • 其次,参数不应该是任何局部变量/对象的引用/指针(根据简单逻辑,一个线程如何访问在其他线程的上下文/堆栈中先前分配的对象?)。
  • 第三,如果参数的数量与实际函数预期的不匹配,宏就会出错。
  • 第四,该宏高度依赖于平台/硬件/编译器。我在 x86 机器上,使用 Windows NT 版本(NT、2000 和 XP)以及 Visual Studio 6.0 和 Visual Studio 2005 编译器进行了测试。

总之,请记住,这个宏在其内部使用了上述相同的旧过程来完成这项任务。

幕后

代码的工作原理很简单。

这段代码的原理是,每当调用一个函数时,为了将参数传递给该函数,它们首先被推入堆栈,然后才会调用该函数。

所以,如果我们想在另一个线程的上下文中调用同一个函数,我们需要在该线程的堆栈中推送相同的参数,然后再在那里调用这个函数。

我们可以做的是,我们可以从这个函数的堆栈中复制所有参数,将它们存储在临时的堆内存位置,然后将它们粘贴到新线程的堆栈中,然后就可以调用该函数了。函数将像在原始线程中调用一样工作。

这正是我们在传统方法中所做的,但这里的不同之处在于,(参数的打包/解包和调用该函数)这些事情只是在这里自动完成了。

但是,我们怎么知道保存参数值的堆栈内存区域的起始和结束地址呢?

我们可以,如果我们能以某种方式计算出第一个参数(在参数列表中最后一个,因为参数是根据 __cdecl 调用约定从最后一个推入的)之前的堆栈指针地址。结束位置将是传递给函数的指针地址(作为最后一个参数推入)之前 4 个字节。

看看这里的代码

#define CALL_IN_THREAD    if(CThreadWrapper tempobj=0) tempobj.FunctionCaller

//Main wrapper class for calling a function in a thread.
//manages variable and data types used in this process
class CThreadWrapper  
{
    //Structure to store function call information
    struct FUNC_CALL_INFO{
        FUNC_TYPE        m_pFuncAddress;
        DWORD            *m_pArgs;
        int                m_pArgLength;
    };

    //Stores value of stack pointer before the call of desired function
    DWORD m_InitialStackAddress;

    //Uses 'FUNC_CALL_INFO' object to call the 
    //desired function with given arguments
    static DWORD WINAPI ThreadFunc( LPVOID lpParam );
public:
    CThreadWrapper(const int);

    // Creates and object of 'FUNC_CALL_INFO' to be used in ThreadFunc
    void FunctionCaller(LPTHREAD_INFORMATION p_pthreadinfo, FUNC_TYPE func_address, ...);

    //helper function for MACRO with if condition
    __forceinline operator const bool(){return true;}
}; 

/*
Constructor function. in addition it also calculate the address of stack pointer 
before the call to this function 
*/

CThreadWrapper::CThreadWrapper(const int)
{
    DWORD ebpvalue;
    __asm mov ebpvalue, ebp;
    m_InitialStackAddress = ebpvalue + 12;
#ifdef _DEBUG
    printf("\n m_InitialStackAddress = 0x%08X",m_InitialStackAddress);
#endif
}

/*
This functions does following tasks:
1>Copies the arguments passed to a temperory buffer
    a> calculate address of last argument
    b> subtract address of last argument from first to calculate
        number of arguments
    c>allocate a new buffer of the size of all arguments
    d>copies all the arguments to buffer
2>Put all information in a wraaper object 'FUNC_CALL_INFO'
3>Call CreateThread on 'ThreadFunc'
*/

void CThreadWrapper::FunctionCaller(LPTHREAD_INFORMATION p_pthreadinfo, 
                                    FUNC_TYPE func_address, ...)
{
    DWORD CurrentStackAddress = (DWORD)&func_address;
//    _asm mov CurrentStackAddress, ebp;

#ifdef _DEBUG
    printf("\n FunctionAddress = 0x%08X",func_address);
#endif

#ifdef _DEBUG
    printf("\n CurrentStackAddress = 0x%08X",CurrentStackAddress);
#endif

    //Calculate no of arguments passed in 'DWORD'
    DWORD arglen = ( ( m_InitialStackAddress - CurrentStackAddress )
                            - OTHER_STACK_DATA_SIZE ) / STACK_DATA_SIZE;
#ifdef _DEBUG
    printf("\n Number of Arguments = %d",arglen);
#endif

    //Allocate memory on heap to copy arguments from stack
    DWORD *args = (DWORD *) malloc( arglen * STACK_DATA_SIZE );

    //Set pointer to argument on stack
    DWORD *argadd = (DWORD *)(CurrentStackAddress + OTHER_STACK_DATA_SIZE);

    //Copy arguments on heap
    for( unsigned int index=0; index < arglen; argadd++,index++)
        *(args + index) = *argadd;


    //Wrap up address of function to call and arguments in a object
    FUNC_CALL_INFO *finfo = (FUNC_CALL_INFO *)malloc(sizeof(FUNC_CALL_INFO));
    finfo->m_pFuncAddress = func_address;
    finfo->m_pArgLength = arglen;
    finfo->m_pArgs = args;

    //Create a thread with 'ThreadFunc' as wrapper function
    p_pthreadinfo->hThread = CreateThread( NULL, 0, CThreadWrapper::ThreadFunc, finfo, 0, 
                                              &(p_pthreadinfo->dwThreadId));
}

/*
This functions does following tasks:
1>fetch information from FUNC_CALL_INFO object
    a> fetch all arguments
    b> fectch function to be called
2>Push arguments into stack to be used by function to be called
3>Call the function
4>Adjust stack
5>free up allocated memory
*/

DWORD WINAPI CThreadWrapper::ThreadFunc( LPVOID lpParam ) 
{ 
    //Retrieve Function Call information
    FUNC_CALL_INFO *finfo    = (FUNC_CALL_INFO *)lpParam;
    DWORD *args        = finfo->m_pArgs;
    DWORD arglen    = finfo->m_pArgLength;
    FUNC_TYPE funcadd        = finfo->m_pFuncAddress;

    //Push all arguments to stack
    long index            = arglen-1;
    unsigned int value    = 0;
    unsigned stackdisp    = arglen*sizeof(DWORD);

    while( index>=0 )
    {
        value = *( args + index );
        index--;
        __asm push value;
    }

    //Call actual function
    __asm call funcadd; 

    //Rectify stack
    __asm add esp,stackdisp;
    
    //Free up memory
    free(finfo->m_pArgs);
    free(finfo);

    return 0; 
}

使用代码

使用代码很简单。看看下面的测试代码

bool complete = false;

void func(char u, char e, int a, int b)
{
    while(complete == false)
    printf("a = %d, b = %d\n",a,b);
    int t=7, y= 8;
    int c = t+y;
}

void func1(int a, int b)
{
    Sleep(2000);
    printf("\n\n\n\n\na = %d, b = %d\n\n\n\n\n\n",a,b);
    complete = true;
}

int main(int argc, char* argv[])
{
    printf("Hello World!\n");
    THREAD_INFORMATION t;

    //func & func1, would be called in a thread.
    for(int i=0;i<3;i++)
        CALL_IN_THREAD(&t, (FUNC_TYPE)func, 'U', 'A', 7+i, 8*i);
    CALL_IN_THREAD(&t, (FUNC_TYPE)func1, 4, 5);

    while(complete == false){};
    
    getchar();
    return 0;
}

在这里,函数 'func' 和 'func1' 已在另一个线程中调用。THREAD_INFORMATION 对象存储了创建线程的句柄和线程 ID,但这里没有使用它们。

接下来呢???

接下来,我将尝试让这个宏适用于类成员函数。非常感谢您的帮助和建议。:)

© . All rights reserved.