yunh 发表于 2014-3-17 15:14:06

ACE TSS 自动清理机制分析与应用

本帖最后由 yunh 于 2014-3-28 15:54 编辑

      TSS (Thread Specific Storage),在 Windows 平台上称为 TLS (Thread Local Storage),简单的概括就是基于线程的专有数据,进程中的所有线程都访问一个全局变量,但不同线程访问的却是基于自己线程的数据,不会相互干扰,也不用加锁保护,当然实际上它们也就不是同一份数据了,所以一个线程的修改对于另一个线程是不可见的。对于想要使用 per thread 的场景,是最好的选择,例如 C 运行库的 errno。
      Unix like 平台上的 TSS 具有线程退出时自动清理的能力,在 pthread_/thr_keycreate 调用中可以指定一个清理函数,当线程退出时,会对每个线程专有的数据执行该清理函数。
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));       但 Windows 上就没有这么便利了,对等的 TlsAlloc 就没有相应参数:
DWORD TlsAlloc(void);      用户必需在线程退出前显示的删除 TSS 中的专用数据,否则会产生内存泄漏 (如果是动态分配内存的话)。
      ACE 的封装消除了平台差异,它也可以指定一个 destructor 供线程退出前销毁专用数据:
int thr_keycreate (ACE_thread_key_t *key,
                     ACE_THR_DEST,
                     void *inst = 0);      为了证实这一点,写一个小的测试程序在 Windows 平台上运行一下,如果指定的 destructor 被调用了,就证明线程专用数据被自动清理了,这里分四种场景分别模拟不同情况下线程的退出。下面首先给出测试代码:
#include "stdafx.h"
#include "ace/Log_Msg.h"
#include "ace/OS_NS_unistd.h"

//#define USE_THR_MGR
//#define SCENE_MAIN_TERM
//#define SCENE_MAIN_EXIT
//#define SCENE_THR_EXIT

#if defined (USE_THR_MGR)
#include "ace/Thread_Manager.h"
#else
#include "ace/OS_NS_thread.h"
#endif

ACE_thread_key_t key = 0;

void tss_cleanup_func (void *data)
{
    ACE_DEBUG ((LM_DEBUG, "(%t) tss data (%u) cleaned up.\n", *(int *)data));
    delete (int *)data;
}

ACE_THR_FUNC_RETURN thr_func (void *arg)
{
    ACE_DEBUG ((LM_DEBUG, "(%t) start\n"));
    ACE_OS::thr_setspecific (key, new int (ACE_OS::thr_self ()));
#if defined (SCENE_MAIN_TERM)
    ACE_OS::sleep (10);
#elif defined (SCENE_THR_EXIT)
    ACE_DEBUG ((LM_DEBUG, "(%t) exit\n"));
    ACE_OS::thr_exit (2);
#endif
    ACE_DEBUG ((LM_DEBUG, "(%t) end\n"));
    return 0;
}

int ACE_TMAIN(int argc, ACE_TCHAR* argv[])
{
    ACE_DEBUG ((LM_DEBUG, "(%t) main start\n"));
    ACE_OS::thr_keycreate (&key, tss_cleanup_func);

#if defined (USE_THR_MGR)
    ACE_Thread_Manager::instance ()->spawn (thr_func);
#else
    ACE_hthread_t hid = 0;
    ACE_OS::thr_create (thr_func, 0, THR_NEW_LWP | THR_JOINABLE, 0, &hid);
#endif

#if defined (SCENE_MAIN_TERM)
    ACE_OS::sleep (3);
#elif defined (SCENE_MAIN_EXIT)
    ACE_OS::exit (1);
#else
#if defined (USE_THR_MGR)
    ACE_Thread_Manager::instance ()->wait ();
#else
    ACE_OS::thr_join (hid, 0);
#endif
#endif

    ACE_DEBUG ((LM_DEBUG, "(%t) main end\n"));
    return 0;
}
      这里使用编译开关 USE_THR_MGR 来切换线程的启动方式,但无论使用哪种启动方式,程序的输出应当是一致的。
      场景一,线程函数运行完毕,线程自动结束。保持测试程序所有编译开关关闭,编译运行,输出如下:
(6304) main start
(6128) start
(6128) end
(6128) tss data (6128) cleaned up.
(6304) main end
请按任意键继续. . .      子线程 6128 的 TSS 清理函数确实被调用了!
      场景二,线程函数调用 ACE_OS::thr_exit 主动结束自己 (注意不是 ACE_OS::exit,后者是专门给主线程使用的)。打开编译开关 SCENE_THR_EXIT,编译运行,输出如下:
(3576) main start
(7232) start
(7232) exit
(7232) tss data (7232) cleaned up.
(3576) main end
请按任意键继续. . .      这种情况下 TSS 清理函数也被调用了!
      场景三,线程函数还在执行,主线程线程函数运行完毕自动退出,导致进程中所有线程结束。打开编译开关 SCENE_MAIN_TERM,关闭其它开关,编译运行,输出如下:
(1480) main start
(4808) start
(1480) main end
请按任意键继续. . .      这种情况下清理函数没有被调用 :(
      场景四,线程函数还在执行,主线程调用 ACE_OS::exit 退出 (注意不是 ACE_OS::thr_exit,后者是专门给 ACE 管理的线程使用的),导致进程中所有线程结束。打开编译开关 SCENE_MAIN_EXIT,关闭其它,输出如下:
(3324) main start
请按任意键继续. . .      这种情况下清理函数也没有被调用。
      发生上面现象的根本原因是主线程一般不是由 ACE 管控的,所以无法在线程函数开始与结束安插代码来处理 TSS 的清理,因此最好不要在主线程中使用 ACE_OS::exit 直接退出,或不等待其它线程就结束。对于 ACE 管理的线程,它的清理工作做的还是很好的,下面着重从两种场景分析 TSS 自动清理机制是如何实现的,即线程函数执行完毕自动结束与直接调用 ACE_OS::thr_exit (或 ACE_Thread::exit,但不是 ACE_OS::exit 或 ::exit) 退出。
      从上面的输出可以看到,清理函数就是在本线程中被执行的,所以最简单的办法莫过于在清理函数中打一个断点,观察它是怎么被调用的,下面是在不开启所有编译开关时的堆栈:
>    tss_scene1.exe!tss_cleanup_func(void * data=0x00207fa8)行24    C++
   ACE5.4.1d.dll!ACE_TSS_Cleanup::exit(void * __formal=0x00000000)行772 + 0x1b 字节    C++
   ACE5.4.1d.dll!ACE_OS::cleanup_tss(const unsigned int main_thread=0)行1093    C++
   ACE5.4.1d.dll!ACE_OS_Thread_Adapter::invoke()行133 + 0x7 字节    C++
   ACE5.4.1d.dll!ACE_OS_Thread_Adapter::invoke()行110 + 0xc 字节    C++
   ACE5.4.1d.dll!ace_thread_adapter(void * args=0x00207fa8)行131 + 0xe 字节    C++
   msvcr80d.dll!_callthreadstartex()行348 + 0xf 字节    C
   msvcr80d.dll!_threadstartex(void * ptd=0x00208268)行331    C      ACE 启动的所有线程的线程函数默认就是 ace_thread_adapter,它又回调了可执行对象的 invoke 方法:
// Invoke the user-supplied function with the args.
ACE_THR_FUNC_RETURN status = thread_args->invoke ();      对于使用 Thread_Manager 的情形,这个对象是 ACE_Thread_Adapter,否则是 ACE_OS_Thread_Adapter,为了验证这一点,单独打开 USE_THR_MGR 开关,可以看到堆栈如下:
>    tss_scene1.exe!tss_cleanup_func(void * data=0x002182f0)行24    C++
   ACE5.4.1d.dll!ACE_TSS_Cleanup::exit(void * __formal=0x00000000)行772 + 0x1b 字节    C++
   ACE5.4.1d.dll!ACE_OS::cleanup_tss(const unsigned int main_thread=0)行1093    C++
   ACE5.4.1d.dll!ACE_Thread_Adapter::invoke_i()行192 + 0x7 字节    C++
   ACE5.4.1d.dll!ACE_Thread_Adapter::invoke_i()行164 + 0xc 字节    C++
   ACE5.4.1d.dll!ACE_Thread_Adapter::invoke()行93 + 0xf 字节    C++
   ACE5.4.1d.dll!ace_thread_adapter(void * args=0x002182f0)行131 + 0xe 字节    C++
   msvcr80d.dll!_callthreadstartex()行348 + 0xf 字节    C
   msvcr80d.dll!_threadstartex(void * ptd=0x002185b0)行331    C
      好,那现在“话分两头,各表一枝”,先说使用 ACE_OS::thr_create 的情况,再讲使用 ACE_Thread_Manager::spawn 的情况,ACE_Task 的 activate 实际属于后一种情况。前者回调 ACE_OS_Thread_Adapter 的 invoke 方法:
            // Call thread entry point.
#if defined (ACE_PSOS)
            (*func) (arg);
            status = 0;
#else /* ! ACE_PSOS */
            status = (*func) (arg);
#endif /* ACE_PSOS */      函数的主体就是对用户提供的线程函数进行回调 (*func)(arg),但是接着往下看,线程函数返回后,又做了哪些调用:
#if defined (ACE_WIN32) || defined (ACE_HAS_TSS_EMULATION)
      // Call TSS destructors.
      ACE_OS::cleanup_tss (0 /* not main thread */);
……      不出所料,对于没有 TSS 自动清理的平台 (WIN32),通过 cleanup_tss 来完成这一切,正如之前在堆栈中看到的那样。该函数一进来,开门见山就清理 TSS:#if defined (ACE_HAS_TSS_EMULATION) || defined (ACE_WIN32) || (defined (ACE_PSOS) && defined (ACE_PSOS_HAS_TSS))
// Call TSS destructors for current thread.
ACE_TSS_Cleanup::instance ()->exit (0);
#endif /* ACE_HAS_TSS_EMULATION || ACE_WIN32 || ACE_PSOS_HAS_TSS */
      下面的代码过于琐碎,就不一一列举了,概括讲来,就是 ACE_TSS_Cleanup 类负责进程中所有 TSS 数据的记录与清理,在 thr_keycreate 当中它记录这些信息,当线程退出时,它清理它们。
      返回头来,看使用 Thread_Manager 创建的线程,在 ACE_Thread_Adapter::invoke_i 中 (被 invoke 所调用),线程函数返回后所做的工作与之前完全一致:
          // Call TSS destructors.
      ACE_OS::cleanup_tss (0 /* not main thread */);         至此二者合流了,也就是说,不论使用何种方式创建线程,最终都会调用 cleanup_tss 来清理线程专用数据。
      这是线程正常退出的情况,那调用 ACE_OS::thr_exit 的情况呢,下面看下打开 SCENE_THR_EXIT 时 (关闭其它) 的堆栈:
>    tss_scene1.exe!tss_cleanup_func(void * data=0x013c7fa8)行24    C++
   ACE5.4.1d.dll!ACE_TSS_Cleanup::exit(void * __formal=0x00000000)行772 + 0x1b 字节    C++
   ACE5.4.1d.dll!ACE_OS::cleanup_tss(const unsigned int main_thread=0)行1093    C++
   ACE5.4.1d.dll!ACE_OS::thr_exit(unsigned long status=2)行3079 + 0x7 字节    C++
   tss_scene1.exe!thr_func(void * arg=0x00000000)行35 + 0xa 字节    C++
   ACE5.4.1d.dll!ACE_OS_Thread_Adapter::invoke()行96 + 0x9 字节    C++
   ACE5.4.1d.dll!ace_thread_adapter(void * args=0x013c7fa8)行131 + 0xe 字节    C++
   msvcr80d.dll!_callthreadstartex()行348 + 0xf 字节    C
   msvcr80d.dll!_threadstartex(void * ptd=0x013c8268)行331    C
      同理,在 ACE_OS::thr_exit 中存在清理 TSS 的过程,它们位于 _endthread 调用之前:
    // Call TSS destructors.
    ACE_OS::cleanup_tss (0 /* not main thread */);       也就是说不论是线程函数执行完毕正常退出,还是调用 ACE_OS::thr_exit 退出,都会调用 ACE_OS::cleanup_tss 进行 TSS 的自动清理工作。使用 Thread_Manager 创建的线程在调用 ACE_OS::thr_exit 时基本相同,在此基础上打开 USE_THR_MGR 开关 (即同时打开 SCENE_THR_EXIT 开关),观察堆栈:
>    tss_scene1.exe!tss_cleanup_func(void * data=0x016582f0)行24    C++
   ACE5.4.1d.dll!ACE_TSS_Cleanup::exit(void * __formal=0x00000000)行772 + 0x1b 字节    C++
   ACE5.4.1d.dll!ACE_OS::cleanup_tss(const unsigned int main_thread=0)行1093    C++
   ACE5.4.1d.dll!ACE_OS::thr_exit(unsigned long status=2)行3079 + 0x7 字节    C++
   tss_scene1.exe!thr_func(void * arg=0x00000000)行35 + 0xa 字节    C++
   ACE5.4.1d.dll!ACE_Thread_Adapter::invoke_i()行150 + 0x9 字节    C++
   ACE5.4.1d.dll!ACE_Thread_Adapter::invoke()行93 + 0xf 字节    C++
   ACE5.4.1d.dll!ace_thread_adapter(void * args=0x016582f0)行131 + 0xe 字节    C++
   msvcr80d.dll!_callthreadstartex()行348 + 0xf 字节    C
   msvcr80d.dll!_threadstartex(void * ptd=0x016585b0)行331    C      至 thr_func 之后的调用完全相同。注意下面几个函数的区别: ACE_OS::thr_exit、ACE_OS::exit、ACE_Thread::exit、::exit。其中 ACE_OS::thr_exit 与 ACE_Thread::exit 一致,是 ACE 创建的线程用来退出线程的,它会自动调用 TSS 清理函数,其中后者是前者的简单封装;ACE_OS::exit 是为非 ACE 管理的线程准备的,而且是主线程,所以如果不是主线程,最好不要调用它,否则会因为关闭一些 ACE 的管理对象而导致莫名其妙的问题;::exit 一般是 C library 提供的,如果直接调用它,也得不到想要的自动清理效果。
      ACE_Object_Manager::at_exit 可以注册一个对象,当进程退出时,该对象自动被销毁:
static int at_exit (ACE_Cleanup *object, void *param = 0);
static int at_exit (void *object,
                      ACE_CLEANUP_FUNC cleanup_hook,
                      void *param);      其实所有的单例对象都是通过这种方式实现销毁的,ACE_Singleton 中创建单例后就立即将它注册到了 Object_Manager:
          if (singleton == 0)
            {
#endif /* ACE_MT_SAFE */
            ACE_NEW_RETURN (singleton, (ACE_Singleton<TYPE, ACE_LOCK>), 0);

            // Register for destruction with ACE_Object_Manager.
            ACE_Object_Manager::at_exit (singleton);
#if defined (ACE_MT_SAFE) && (ACE_MT_SAFE != 0)
            }      同理,ACE_Thread_Manager 也支撑线程退出时对象的销毁,它的接口与上面的非常类似:
int at_exit (ACE_At_Thread_Exit* cleanup);
int at_exit (void *object,
               ACE_CLEANUP_FUNC cleanup_hook,
               void *param);      其实这种机制就是基于 TSS 自动清理机制,和以前一样,先看一个测试程序:
#include "stdafx.h"
#include "ace/Thread_Manager.h"

//#define USE_EXIT

void tss_cleanup (void *obj, void *param)
{
    ACE_DEBUG ((LM_DEBUG, "(%t) clean up 0x%x (%u).\n", obj, *(int *)param));
    delete (int *)param;
}

ACE_THR_FUNC_RETURN thr_func (void *arg)
{
    ACE_DEBUG ((LM_DEBUG, "(%t) start\n"));
    ACE_Thread_Manager *mgr = ACE_Thread_Manager::instance ();
    mgr->at_exit (0, tss_cleanup, arg);

#if defined (USE_EXIT)
    ACE_DEBUG ((LM_DEBUG, "(%t) exit\n"));
    ACE_OS::thr_exit (1);
#endif
    ACE_DEBUG ((LM_DEBUG, "(%t) end\n"));
    return 0;
}

int ACE_TMAIN(int argc, ACE_TCHAR* argv[])
{
    ACE_DEBUG ((LM_DEBUG, "(%t) main start\n"));
    ACE_Thread_Manager *mgr = ACE_Thread_Manager::instance ();
    mgr->spawn (thr_func, new int(42));
    mgr->wait ();
    ACE_DEBUG ((LM_DEBUG, "(%t) main end\n"));
    return 0;
}      程序启动一个线程,并在该线程中注册一个需要在线程退出时调用的清理函数,编译运行后,输出如下:
(5548) main start
(3232) start
(3232) end
(3232) clean up 0x0 (42).
(5548) main end
请按任意键继续. . .      可以看到,清理函数确实被调用了!而且时间就是在子线程销毁,主线程还在运行的时候。那么这套机制是如何实现的呢? 在清理函数中打一个断点,看下堆栈:
>    thr_atexit.exe!tss_cleanup(void * obj=0x00000000, void * param=0x01458218)行10    C++
   ACE5.4.1d.dll!ACE_At_Thread_Exit_Func::apply()行77 + 0x18 字节    C++
   ACE5.4.1d.dll!ACE_Thread_Descriptor::at_pop(int apply=1)行81 + 0xf 字节    C++
   ACE5.4.1d.dll!ACE_Thread_Descriptor::do_at_exit()行132 + 0xa 字节    C++
   ACE5.4.1d.dll!ACE_Thread_Descriptor::terminate()行148    C++
   ACE5.4.1d.dll!ACE_Thread_Manager::exit(unsigned long status=0, int do_thr_exit=0)行1723    C++
   ACE5.4.1d.dll!ACE_Thread_Control::exit(unsigned long exit_status=0, int do_thr_exit=0)行82 + 0x12 字节    C++
   ACE5.4.1d.dll!ACE_Thread_Control::~ACE_Thread_Control()行72    C++
   ACE5.4.1d.dll!ACE_Thread_Exit::~ACE_Thread_Exit()行89 + 0x8 字节    C++
   ACE5.4.1d.dll!ACE_Thread_Exit::`scalar deleting destructor'()+ 0x16 字节    C++
   ACE5.4.1d.dll!ACE_TSS<ACE_Thread_Exit>::cleanup(void * ptr=0x01458450)行85 + 0x1c 字节    C++
   ACE5.4.1d.dll!ACE_TSS_Cleanup::exit(void * __formal=0x00000000)行772 + 0x1b 字节    C++
   ACE5.4.1d.dll!ACE_OS::cleanup_tss(const unsigned int main_thread=0)行1093    C++
   ACE5.4.1d.dll!ACE_Thread_Adapter::invoke_i()行192 + 0x7 字节    C++
   ACE5.4.1d.dll!ACE_Thread_Adapter::invoke_i()行164 + 0xc 字节    C++
   ACE5.4.1d.dll!ACE_Thread_Adapter::invoke()行93 + 0xf 字节    C++
   ACE5.4.1d.dll!ace_thread_adapter(void * args=0x01458330)行131 + 0xe 字节    C++
   msvcr80d.dll!_callthreadstartex()行348 + 0xf 字节    C
   msvcr80d.dll!_threadstartex(void * ptd=0x014585f0)行331    C      又是 ACE_OS::cleanup_tss!在线程函数退出时会调用它,这个已经在上面分析过了,它主要是用来清理 TSS 的,看来线程退出的清理机制确实有赖于 TSS 清理机制,经过一番源码探究,现在把它们两个的关系概括如下,ACE_Thread_Manager 中有一个静态成员 thr_exit_:
/// Global ACE_TSS (ACE_Thread_Exit) object ptr.
static ACE_TSS_TYPE (ACE_Thread_Exit) *thr_exit_;      它是在 set_thr_exit 方法中被设置的,而仅有 ACE_Thread_Exit::instance 方法会调用该方法进行设置:
      if (ACE_Thread_Exit::is_constructed_ == 0)
      {
          ACE_NEW_RETURN (instance_,
                        ACE_TSS_TYPE (ACE_Thread_Exit),
                        0);

          ACE_Thread_Exit::is_constructed_ = 1;

          ACE_Thread_Manager::set_thr_exit (instance_);
      }      而 ACE_Thread_Exit::instance 又只有在 ACE_Thread_Adapter::invoke 中被调用:
ACE_Thread_Exit *exit_hook_instance = ACE_Thread_Exit::instance ();      该函数之前见过,它就是所有 ACE_Thread_Manager 创建的线程的回调函数,也就是说只要使用 ACE_Thread_Manager 创建了线程,这个对象也就会随之一起创建。关键是看下这个对象的实际类型:
ACE_TSS <ACE_Thread_Exit>      ACE_TSS 是 ACE 用来简化 TSS 操作的模板类,使用它的 operator-> 操作可以直接访问位于线程专用数据中的对象。关键在于它在背后使用了 thr_keycreate 方法,会将一个清理函数注册给 TSS 机制:
      if (ACE_Thread::keycreate (ACE_const_cast (ACE_thread_key_t *, &this->key_),
#if defined (ACE_HAS_THR_C_DEST)
                                 &ACE_TSS_C_cleanup,
#else
                                 &ACE_TSS<TYPE>::cleanup,
#endif /* ACE_HAS_THR_C_DEST */
                                 (void *) this) != 0)      也就是说线程退出时,ACE_TSS<ACE_Thread_Exit>::cleanup 清理函数会被调用,这又是什么东东:
template <class TYPE> void
ACE_TSS<TYPE>::cleanup (void *ptr)
{
// Cast this to the concrete TYPE * so the destructor gets called.
delete (TYPE *) ptr;
}      直接明了,销毁对象,看下 ACE_Thread_Exit 的析构函数做了什么:
ACE_Thread_Exit::~ACE_Thread_Exit (void)
{
ACE_OS_TRACE ("ACE_Thread_Exit::~ACE_Thread_Exit");
}      居然是空的,怎么可能 ?! 原来它还包含一个名为 ACE_Thread_Control 的内嵌对象:
/// Automatically add/remove the thread from the
/// <ACE_Thread_Manager>.
ACE_Thread_Control thread_control_;      那么它的析构函数是什么呢?
ACE_Thread_Control::~ACE_Thread_Control (void)
{
ACE_OS_TRACE ("ACE_Thread_Control::~ACE_Thread_Control");

#if defined (ACE_HAS_RECURSIVE_THR_EXIT_SEMANTICS) || defined (ACE_HAS_TSS_EMULATION) || defined (ACE_WIN32)
this->exit (this->status_, 0);
#else
this->exit (this->status_, 1);
#endif /* ACE_HAS_RECURSIVE_THR_EXIT_SEMANTICS */
}      真是踏破铁鞋无觅处,这里,它调用了 ACE_Thread_Control::exit:
ACE_Thread_Control::exit (ACE_THR_FUNC_RETURN exit_status, int do_thr_exit)
{
ACE_OS_TRACE ("ACE_Thread_Control::exit");

if (this->tm_ != 0)
    return this->tm_->exit (exit_status, do_thr_exit);
else
    {
#if !defined (ACE_HAS_TSS_EMULATION)
      // With ACE_HAS_TSS_EMULATION, we let ACE_Thread_Adapter::invoke ()
      // exit the thread after cleaning up TSS.
      ACE_OS::thr_exit (exit_status);
#endif /* ! ACE_HAS_TSS_EMULATION */
      return 0;
    }
}      后者又回调了 ACE_Thread_Manager 的 exit 方法:
{
    ACE_MT (ACE_GUARD_RETURN (ACE_Thread_Mutex, ace_mon, this->lock_, 0));

    // Find the thread id, but don't use the cache.It might have been
    // deleted already.
#if defined (VXWORKS)
    ACE_hthread_t id;
    ACE_OS::thr_self (id);
    ACE_Thread_Descriptor* td = this->find_hthread (id);
#else/* ! VXWORKS */
    ACE_thread_t id = ACE_OS::thr_self ();
    ACE_Thread_Descriptor* td = this->find_thread (id);
#endif /* ! VXWORKS */
    if (td != 0)
   {
       // @@ We call Thread_Descriptor terminate this realize the cleanup
       // process itself.
       td->terminate();
   }
}      这个函数里最重要的工作就是调用线程描述符 ACE_Thread_Descriptor 之 terminate,在该方法中,除了将自己从 Thread_Manager 表中移除,它将之前程序注册在本线程中的清理函数一一回调,测试程序的清理函数就是在这个时候调用的。由于 clean_tss 在线程调用 thr_exit 时也会被调用,所以基于 TSS 清理机制的这套线程退出机制也会起作用。打开 USE_EXIT 编译开关,重新编译并运行,输出如下:
(6300) main start
(2148) start
(2148) exit
(2148) clean up 0x0 (42).
(6300) main end
请按任意键继续. . .      可以看到清理函数依然被调用了。
      至此,对 ACE TSS 清理机制的分析及应用就都介绍完了,感兴趣的读者可以自己试一下这些例子,或者在实际中直接使用 ACE 的这一功能。

yunh 发表于 2014-3-19 15:58:57

人都哪去了……

liulinqi206 发表于 2014-6-29 13:05:18

版主好文章!每月出一篇啊!
页: [1]
查看完整版本: ACE TSS 自动清理机制分析与应用