能向入口函数传入多个参数的 QueueUserWorkItem

2022-11-08 16:24:04 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

不啰嗦了,花一堆时间也没赶上 std::async 和 std::thread 的设计,标准库的设计真的,很优秀。 我记下这段时间里做了什么; 这里包含了把函数拆成两步调用的方法,第一步传参,第二步执行;SplitInvoke;如果我能把第一步放到A线程,第二步放到B线程,就能解决std::thread 潜在的两次拷贝和对象(Windows的窗口对象等)绑定到线程问题,就能制造一个优于 std::async和std::thread的东西。 一个向仅有一个VOID*型回调函数传入任意多个任意类型参数的方法;InvocationShim; 一个推导函数调用约定以及函数摘要的方法;FnSynopsis、CallableSynopsis; 一个仿制的 TLS;PushEx0ArgThunk; 以上这些足以为所有函数编写一个通用的 detour函数,或用来帮助处理inline hook。以下是代码:

代码语言:javascript复制
/*
threadsapiex.h
一些常用的线程函数只接受向入口函数传入一个类型为 VOID* 的参数,这个文件
里的函数用来扩展该不足。支持向入口函数传入无限个类型不同的参数。
注意:
1、向入口函数传递引用,移动操作发生在创建线程成功后,调用入口函数前。
2、一如既往,线程入口的可调用对象不能在线程开始后、结束前被销毁,如果传入
一个 std::function 等对象 —— 小心。
< .fuhao >
*/
#pragma once
#include <tuple>
#include "stdex.h"
#include <cassert>
#include <memory>
#include <process.h>
#include <type_traits>
#if !defined( _X86_ )
#error __FILE__"不能编译为非x86平台"
#endif
namespace _ThreadsexImpl 
{
using std::bind;
using std::tuple;
using std::mem_fn;
using std::result_of;
using std::enable_if;
using std::true_type;
using std::false_type;
using std::addressof;
using std::conditional;
using std::shared_ptr;
using std::exception_ptr;
using std::current_exception;
using std::rethrow_exception;
using std::is_convertible;
using std::integral_constant;
using std::remove_reference;
using std::is_nothrow_default_constructible;
using stdex::FnSynopsis;
using stdex::CallConvention;
using stdex::CallableSynopsis;
// {D0CDAE19-FCF2-4F61-9E4E-46147473566A}
STATIC CONST GUID Ex0ArgNestedFlags =
{ 0xd0cdae19, 0xfcf2, 0x4f61,{ 0x9e, 0x4e, 0x46, 0x14, 0x74, 0x73, 0x56, 0x6a } };
typedef struct _Ex0ArgThunk 
{
CONST GUID NestedFlags{ Ex0ArgNestedFlags };
LPVOID ShimObj;
VOID( STDCALL * Release )(LPVOID);
LPVOID OldStuff;
LPVOID Address;
exception_ptr **_ExceptionPtr;
} Ex0ArgThunk, *PEx0ArgThunk;
STATIC BOOL STDCALL ExArg0CheckNestedCall()
{
LPVOID *StackBase = (LPVOID*)(__readfsdword( 4 ) - sizeof( LPVOID ));
PEx0ArgThunk pThunk = (PEx0ArgThunk)*StackBase;
if( pThunk )
{
__try {
return pThunk->NestedFlags == Ex0ArgNestedFlags;
}
__except( EXCEPTION_EXECUTE_HANDLER ) {
}
}
return FALSE;
}
STATIC VOID STDCALL PushEx0ArgThunk(
PEx0ArgThunk pThunk, LPVOID Address, exception_ptr ** _ExceptionPtr ) _NOEXCEPT
{
//		tib   4 是 StackBase,把 StackBase - sizeof( LPVOID ) 的内容备份到
// pThunk->OldStuff,然后把 pThunk 写入到StackBase - sizeof( LPVOID )处,
// 栈底的四字节本应用来存储线程参数的,但在 win7 wow64环境下调试发现
// 线程参数被存储在 StackBase - 281处,而StackBase到StackBase-281的区域
// 被填充为0,不知原因为何。但这些区域可以被用来填充别的值,所以,现在的
// 这种做法是不确定是否可行的,如果在其它平台发生错误,请尝试改用 Tls 存储
// pThunk,或弃用这些代码;
//		不直接在 SplitInvoker 函数中使用 ebp 寻址 pThunk是因为在PushEx0ArgThunk
// 和SplitInvoker两次调用之间还包含了一大堆为推导参数等而写的函数调用,这
// 些调用并不总是如期被编译器全部优化,若因某个函数未被优化而产生实际调用,
// 则会在建立栈帧过程修改 ebp,在 SplitInvoker 中将无法找到 pThunk。
//		还可以考虑用 Tls,大概像这样:
//		BackupTlsValue( Index = 0 );
//		TlsSetValue( Index = 0, pThunk );
//		...
//	不使用动态 Tls 的原因是 使用动态 Tls 必须在 TEB::TlsSlots中占据一个特定位置,
// 虽然可以考虑备份并还原,但用户可能会假设某个 TEB::TlsSlots位置中存储了特定
// 内容(参数拷贝过程中用户代码得以执行,可能会操作 Tls)。
//		或者使用静态 Tls,但使用静态 Tls的DLL可能会在 LoadLibrary时出问题,如下
//	文所述: https://msdn.microsoft.com/en-us/library/6yh4a9k1.aspx
//		还剩动态重建 SplitInvoker 函数和用全局变量的办法。先这样吧,试试好用否,
// 后期再完善。
LPVOID *StackBase = (LPVOID*)(__readfsdword( 4 ) - sizeof( LPVOID ));
pThunk->OldStuff = *StackBase;
*StackBase = pThunk;
pThunk->Address = Address;
pThunk->_ExceptionPtr = _ExceptionPtr;
}
STATIC VOID STDCALL PopEx0ArgThunk() _NOEXCEPT
{
// 从 tib   4指向的 StackBase - sizeof( LPVOID ) 处得到存储的 PEx0ArgThunk,
// 把 PEx0ArgThunk::OldStuff 的值还原到 StackBase - sizoef( LPVOID ) 处。
LPVOID *StackBase = (LPVOID*)(__readfsdword( 4 ) - sizeof( LPVOID ));
PEx0ArgThunk pThunk = (PEx0ArgThunk)*StackBase;
*StackBase = pThunk->OldStuff;
*pThunk->_ExceptionPtr = NULL;
pThunk->Release( pThunk->ShimObj );
}
STATIC LPVOID STDCALL Ex0ArgGetAddress() _NOEXCEPT
{
LPVOID *StackBase = (LPVOID*)(__readfsdword( 4 ) - sizeof( LPVOID ));
return ((PEx0ArgThunk)*StackBase)->Address;
}
template< CallConvention CallType > __declspec(naked) STATIC VOID* SplitInvoke()
{
static_assert( CallType == CallConvention::_CDECL_CALL ||
CallType == CallConvention::_STD_CALL, "尚未支持的调用约定" );
__asm
{
call Ex0ArgGetAddress
push eax
call PopEx0ArgThunk
pop eax
jmp eax
}
}
template<> __declspec(naked)
STATIC VOID *SplitInvoke< CallConvention::_THIS_CALL >()
{
__asm
{
push ecx
call Ex0ArgGetAddress
push eax
call PopEx0ArgThunk
pop eax
pop ecx
jmp eax
}
}
template<> __declspec(naked) 
STATIC VOID* SplitInvoke< CallConvention::_FAST_CALL>()
{
__asm
{
push ecx
push edx
call Ex0ArgGetAddress
push eax
call PopEx0ArgThunk
pop eax
pop edx
pop ecx
jmp eax
}
}
// c   禁止成员函数转 void* 的语义,用 ptr_cast 逃避检查
template< typename _Ty, typename _Ty1 > inline _Ty ptr_cast( _Ty1 Src ) 
{
union { _Ty1 Src; _Ty Dst; } u{ Src };
return u.Dst;
}
template< typename _RetType,
typename _Fn1,
typename ..._Types >
struct InvocationShim final 
{
static_assert(is_nothrow_default_constructible< _RetType >::value, "");
typedef CallableSynopsis< _Fn1 > _FnTraits;
enum  FnType{ NonMemberFunction, MemberFunction, CallableObject };
STATIC _RetType WINAPI ShimProc( LPVOID lpv )
{
assert( !IsBadReadPtr( lpv, sizeof( InvocationShim ) ) &&
!IsBadWritePtr( lpv, sizeof( InvocationShim ) ) );
return ptr_cast<InvocationShim*>(lpv)->
ShimInvoke<  FnType( !!_FnTraits::IsCallableObject   !!_FnTraits::IsMemberFunction ) >();
}
template< FnType > _RetType ShimInvoke()
{
return InvokeWrapper(
ptr_cast<_FnTraits::_PtrType>(SplitInvoke< _FnTraits::CallType >), ptr_cast<LPVOID>(addressof( _Func )) );
}
template<> inline _RetType ShimInvoke< CallableObject >()
{
return InvokeWrapper( [this]( _Types &&... Args )->typename _FnTraits::_ResultType{
return (_Func.*ptr_cast<_FnTraits::_PtrType>(SplitInvoke< _FnTraits::CallType >))(std::forward< _Types >( Args )...);
}, ptr_cast<LPVOID>(&typename remove_reference<_Fn1>::type::operator()) );
}
template<> inline _RetType ShimInvoke<MemberFunction>()
{
static_assert(tuple_size< tuple< _Types...> >::value, "需要为成员函数绑定调用对象");
static_assert(is_same< tuple_element< 0, tuple< _Types...> >::type, _FnTraits::_ClassType* >::value, "为成员函数绑定的调用对象不正确");
return InvokeWrapper( MemberShimInvoke< _Types... >, ptr_cast<LPVOID>(_Func) );
}
template< typename _ClassPtrType, typename ...Types >
INLINESTATIC auto MemberShimInvoke( _ClassPtrType pObj, Types &&... Args )-> typename _FnTraits::_ResultType{
return (pObj->*ptr_cast<_FnTraits::_PtrType>(SplitInvoke< _FnTraits::CallType>))(std::forward< Types >( Args )...);
}
STATIC InvocationShim *Create( ATOMDWORD *_Locked,
exception_ptr *_ExceptionPtr, _Fn1 &&_Func, _Types &&... Args ) _NOEXCEPT
{
return new InvocationShim( _Locked, _ExceptionPtr,
std::forward< _Fn1 >( _Func ), std::forward< _Types >( Args )... );
}
STATIC VOID Release( _Pre_invalid_ InvocationShim *p ) _NOEXCEPT { delete p; }
STATIC VOID STDCALL Release( _Pre_invalid_ LPVOID p ) _NOEXCEPT{ delete reinterpret_cast<InvocationShim *>(p); }
private:
InvocationShim( ATOMDWORD *Locked, exception_ptr *ExceptionPtr, _Fn1 &&_Fn, _Types &&...Args )
: _Locked( Locked )
, _Func( std::forward<_Fn1>( _Fn ) )
, _Args( std::forward< _Types>( Args )... )
, _ExceptionPtr( ExceptionPtr )
{
//  如果没有参数需要拷贝,_Locked 处于可获得状态
InterlockedExchange( _Locked, !!sizeof...(_Types) );
ZeroMemory( &_Arg0Thunk, sizeof( Ex0ArgThunk ) );
_Arg0Thunk.ShimObj = this;
_Arg0Thunk.Release = Release;
}
~InvocationShim()
{
/** 如果参数数量不为零,用户必须等待参数拷贝完成才能销毁 _Locked,
否则此处可能产生一个 EXCEPTION_ACCESS_VIOLATION 异常。*/
if( sizeof...(_Types) )
InterlockedExchange( _Locked, FALSE );
}
template< typename _Fty >
inline _RetType InvokeWrapper( _Fty &&_Proc, LPVOID Address )
{
exception_ptr *_Ptr( _ExceptionPtr );
assert( _Ptr != NULL );
PushEx0ArgThunk( &_Arg0Thunk, Address, &_Ptr );
try {	// 捕获参数拷贝过程发生的异常
return _ApplyImpl( std::forward< _Fty >( _Proc ), std::move( _Args ) );
}
catch( ... )
{
/** 安装的异常处理例程并未在完成参数拷贝后被卸载,因此,当被调函数引发异常时
会在此被重新抛出,那么,当用户附加调试器检查调用栈时可能会发现异常在此被抛出
而不是真正引发异常的帧。*/
if( !_Ptr ) throw;
/** _Ptr 指向外部调用 Create 时传入的 exception_ptr对象。当此处捕获到异常时,
_Ptr 指向的 exception_ptr 对象可能已销毁 —— 因为用户误用,在参数拷贝完成
前销毁了其持有的exception_ptr对象。这将导致以下对 *_Ptr 的访问产生异常。
还有一种情况会导致以下代码访问错误的 exception_ptr 对象 —— 当 _Args的
长度为零(参数数量为零)或 _Args 内只包含一个 _FnType* 时 _ApplyImpl 函数
抛出了异常。这种情况不会发生,_ApplyImpl 自身不会抛出异常,除非修改了
_ApplyImpl函数。*/
*_Ptr = current_exception();
// PopEx0ArgThunk 内部会把 _Ptr 置为 NULL
PopEx0ArgThunk();
}
return _RetType();	// 无奈,不容易避免写下 _RetType()
}
template< typename _Fty, typename ...Types >
INLINESTATIC _RetType _ApplyImpl( _Fty &&_Proc, tuple< Types... > &&_Tuple, 
typename enable_if<
is_convertible< 
typename result_of< decltype(_Func)&(_Types...)>::type, _RetType 
>::value, true_type 
>::type tt = true_type() )/* noexcept( _Proc( Types... ) ), 兼容 2013 */
{
return ApplyTupleExpand(
std::forward< _Fty >( _Proc ), std::move( _Tuple ) );
}
template< typename _Fty, typename ...Types > 
INLINESTATIC _RetType _ApplyImpl( _Fty &&_Proc, tuple< Types ... > &&_Tuple,
typename enable_if<
!is_convertible< 
typename result_of<decltype(_Func)&(_Types...)>::type, _RetType 
>::value, false_type 
>::type ft = false_type() )/* noexcept( _Proc( Types... ) ), 兼容 2013 */
{
ApplyTupleExpand(
std::forward< _Fty >( _Proc ), std::move( _Tuple ) );
return _RetType();	// 如果_Func返回类型不能被转换成 _RetType类型,
// 那 _RetType 类型必须能够接受以此(_RetType(0))方式构造一个对象。
}
private:
_Fn1 &&_Func;
tuple< _Types &&... > _Args;
ATOMDWORD *_Locked;
Ex0ArgThunk _Arg0Thunk;
exception_ptr *_ExceptionPtr;
};
}
/*
QueueUserWorkItemEx
QueueUserWorkItem 的扩展函数,可以任何可调用对象为入口,亦可向任务的入口函数传递任意多个任意类型的参数。
参数:
Flags,参见 QueueUserWorkItem 的 Flags 参数。
_Func,可调用对象。
Args,传递给可调用对象的参数包。
返回值:
参见 QueueUserWorkItem。
备注:
1、_Func 不可为绑定表达式,既当 is_bind_expression< decltype(_Func )> 成立时将无法编译,原因是
ms c   标准库中 bind 函数返回的对象其 operator() 为模板函数,而QueueUserWorkItemEx内部无法处理
未实例化的模板函数。原因是QueueUserWorkItemEx内部需要推导函数调用约定并拆分调用。
2、当 Args 参数包中包含“按值传递”的对象时将发生一次(不同于 std::thread 或 std::async 等需要拷贝移
动和一次)拷贝构造行为,且拷贝构造发生在目标线程中而非调用者线程,若拷贝构造过程发生异常则异常被传
导到调用(QueueUserWorkItemEx的)线程抛出(该行为和 std::async 相同)。
3、QueueUserWorkItemEx 函数可接受成员函数为入口,当向QueueUserWorkItemEx传递一个成员函数作
为 _Func 的实参时,QueueUserWorkItemEx的第二个参数必须为调用 _Func 时绑定到之上的对象的指针,参见
示例1.2;
4、QueueUserWorkItemEx 在完成参数拷贝后返回,而非向线程池的任务队列投递任务后立即返回;原因是
若在目标线程拷贝参数前返回可能导致目标线程使用已被销毁的对象。这可能会导致线程池依托任务队列建立的可
伸缩性失效,具体解决方法请参考注意事项第1条。
注意:
1、当Args参数包中参数数量不为零时会引起等待;等待线程池中线程调用_Func 前的参数拷贝完成。当线程
池中所有线程均处于繁忙状态时可能导致调用线程长时间挂起,若调用线程是QueueUserWorkItem中的线程还会
导致线程池的伸缩性丧失。建议的解决方法是使用参数数量为零的lambda,并捕获所需参数,让拷贝提前发生。
2、若用户试图嵌套调用 QueueUserWorkItemEx ,将得到一个“IO未决”错误。嵌套调用指 —— 在参数拷贝过程
中再次调用 QueueUserWorkItemEx。
3、一如往常,_Func 指向的可调用对象在其自身调用结束前不能被销毁,若 _Func 指向成员函数,那么绑定在其
上的对象指针也必须拥有相同(或超越 _Func)的生命周期。
4、参数传递过程可能包含隐式的向引用或右值引用的转换。不同于 std::thread 和 std::aysnc 等需要显示的 std::ref 
调用;见示例1.1。
5、参数包中包含的某些对象的初始化过程可能会创建某些依赖于线程的内部对象(如 Windows 的窗口对象),
对于此情况,我的建议是不要作为参数传递,或改用 std::async 。
QueueUserWorkItemEx 是个失败的设计。改用 std::async 和 std::thread 吧 :-( 
虽然 thread 会包含一次拷贝一次移动,但逻辑都是正确的,也不容易误用。显然标准早已考虑过这些。不再班门弄斧了。
示例1.1	——	以函数为入口:
VOID __stdcall Proc1( string s1, string &s2, string &&s3 ){ }
int main( int argc, char **argv )
{
string s1, s2;
auto GetS3 = []()->string { return ""; };
QueueUserWorkItemEx( Proc1, 
s1, // 按值传递
s2, // 按引用传递,无须 std::ref(s2)
GetS3() // 函数返回值为右值,无需 std::move,传入非右值对象需要 std::move
);
// 注意:s2以引用方式传入 Proc1,Proc1调用完成前不能销毁s2
WaitProc1InvokeComplete();
return EXIT_SUCCESS;
}
示例1.2	——	以成员函数为入口并参数拷贝过程中的异常:
struct B {
B(){ }
B( const B & ) { _Xinvalid_argument( "hi." ); }
};
struct A {
int __cdecl Proc( B obj ) { return 0; }
void operator() ( int ) { }
};
int main( int argc,char ** argv )
{
A a;
QueueUserWorkItemEx( &A::operator(), &a, 0 );
B b;
try {
QueueUserWorkItemEx( &A::Proc,
&a, // 必须绑定对象的指针,不能是引用或值。
b );
}
catch( invalid_argument &re ) {
printf( "%s", re.what() );		// 输出:hi.
}
WaitProcOfACallComplete();
return EXIT_SUCCESS;
}
示例 1.3	——	以可调用对象为入口:
struct A {
void operator() ( int ) { }
};
int main( int argc, char ** argv )
{
A a;
QueueUserWorkItemEx( a, 0 );
function< void( int )> pfn1 = bind( &A::operator(), a, placeholders::_1 );
QueueUserWorkItemEx( pfn1, 0 );
return EXIT_SUCCESS;
}
*/
template< typename _Fn1, typename ..._Types >
BOOL QueueUserWorkItemEx( ULONG Flags, _Fn1 &&_Func, _Types&&... Args )
{
using namespace _ThreadsexImpl;
// 如果用户试图嵌套调用此函数-返回IO未决
if( ExArg0CheckNestedCall() )
{
SetLastError( ERROR_IO_PENDING );
return FALSE;
}
typedef InvocationShim< DWORD, _Fn1, _Types... > InvocationShimImpl;
exception_ptr _ExceptionPtr;
ATOMDWORD _Locked( TRUE );
InvocationShimImpl *pShim = InvocationShimImpl::Create( &_Locked,
&_ExceptionPtr, std::forward< _Fn1>( _Func ), std::forward< _Types >( Args )... );
if( QueueUserWorkItem( InvocationShimImpl::ShimProc,
reinterpret_cast<LPVOID>(pShim), Flags) )
{
while( InterlockedExchange( &_Locked, TRUE ) )
SwitchToThread();
/** 拷贝参数过程中产生的异常会在此处被抛出,设计比较奇怪
但逻辑是正确的。若不在此处抛出,用户将无法处理拷贝参数
过程产生的异常。后续使用 InvocationShim之处也有相同逻辑。*/
if( _ExceptionPtr )
rethrow_exception( _ExceptionPtr );
return TRUE;
}
InvocationShimImpl::Release( pShim );
return FALSE;
}
template< typename _Fn1, typename ..._Types >
BOOL QueueUserWorkItemEx( _Fn1 &&_Func, _Types&&... Args )
{
return QueueUserWorkItemEx( (ULONG)WT_EXECUTEDEFAULT,
std::forward< _Fn1 >( _Func ), std::forward< _Types >( Args )... );
}

对于 QueueUserAPC、CreateThread、_beginthreadex、RegisterWaitForSingleObject、SetWaitableTimer、SetTimer等等等等都可像QueueUserWorkItemEx那样实现传递任意多个任意类型的参数,用就自己写吧。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

0 人点赞