专栏首页Eureka伽罗的技术时光轴如何解决在DLL的入口函数中创建或结束线程时卡死

如何解决在DLL的入口函数中创建或结束线程时卡死

先看一下使用Delphi开发DLL时如何使用MAIN函数,

通常情况下并不会使用到DLL的MAIN函数,因为delphi的框架已经把Main函数隐藏起来

而工程函数的 begin end 默认就是MAIN函数的DLL_PROCESS_ATTACH事件的处理代码,如需要完整的处理其他事件,

如 DLL_PROCESS_DETACH,DLL_THREAD_ATTACH, DLL_THREAD_DETACH,可在工程文件中做如下处理:

procedure DLLEntryPoint(Reason:DWord);
begin
  case Reason of
    DLL_PROCESS_ATTACH:
      StartMyThreadsAndWaitBegin(); // 创建并等待线程开始,这样会导致卡死
    DLL_PROCESS_DETACH: 
      StopMyThreadsAndWaitEnd(); // 停止并等待线程结束(或直接结束进程),这样会导致卡死
    DLL_THREAD_ATTACH:;  
    DLL_THREAD_DETACH:;
  end;
end;

begin
  DllProc := @DLLEntryPoint;
  DLLEntryPoint(DLL_PROCESS_ATTACH);
end.

其中 DllProc 是SysInit中的全局变量,可简单理解为保存DLL Entry Point入口函数的地址(实际上RTL内部还有InitLib 和StartLib函数,由编译器自动处理)。

以上都是题外话,本文主要说明在DLL入口函数里面创建和退出线程为什么卡死和如何解决的问题。

1)在 DLL_PROCESS_ATTACH 事件中 创建线程 出现卡死的问题

通常情况下在这事件中仅仅是创建并唤醒线程,是不会卡死的,但如果同时有等待线程正式执行的代码,则会卡死,因为在该事件中,任何启动的线程都会由于LdrLoadDll中的LdrpLoaderLock 进入锁定状态而处于等待,无法进入线程函数,所以也就永远无法检测到正式执行的机会。

LdrpLoaderLock是系统的PE Loader的一个重要锁,保证系统资源的安全,而DLL 入口函数是在PE Loader 结束前执行的,LdrInitializeThunk等函数处理PE 映像 到内存中的过程中,LdrpLoaderLock是处于锁定状态的。

所以解决办法就是 在 DLL_PROCESS_ATTACH 事件中,仅创建并唤醒线程即可(此时即使是唤醒了,线程也是处理等待状态),线程函数会在DLL_PROCESS_ATTACH事件结束后才正式执行(实际上如果是通过LoadLibrary加载DLL,则会在LoadLibrary结束前后的某一时刻正式执行)。

2)在DLL_PROCESS_DETACH中结束线程出现卡死的问题

同样的原因,该事件是调用LdrUnloadDll中执行的,LdrpLoaderLock仍然是锁定状态的,而结束线程最终会调用LdrShutdownThread,均会释放PE Loader所维护的系统内部的共同资源(包括PEB 和TEB等模块信息和线程TLS数据等),此类共同资源刚好都是使用LdrpLoaderLock进行同步,所以在DLL_PROCESS_DETACH中调用ExitThread->LdrShutdownThread,必然导致卡死。

另外有一个特殊的现象,就是DLL_PROCESS_DETACH事件中,线程处于挂起状态,这是因为系统分配线程执行时间片的过程中由于PE Loader有资源处于锁定而导致线程无法进行下一个时间片,最终表现为线程函数处于假死状态,此状态基本上等同于线程的挂起(suspend)状态。

解决办法同样是避免在 DLL_PROCESS_DETACH事件中结束线程,那么我们可以在该事件中,创建并唤醒另外一个线程,在该新的线程里,结束需要结束的线程,并在完成后结束自身即可。唯一需要注意的是,一旦DLL_PROCESS_DETACH结束,内存中与DLL相关的PE映像资源可能会被释放掉,所以在后续的操作中尽量不要再对原来的数据进行操作,否则容易导致内存溢出(但其实释放与否是由内核决定的,也许将来经过某一个版本的补丁后,相关资源仍然会保留在内存可以使用)。

提醒: 标准的做法还是建议遵循MS的规则,不要在DLL入口函数中做线程相关的创建和释放操作。

总体上代码如下:

procedure DLLEntryPoint(Reason:DWord);
begin
  case Reason of
    DLL_PROCESS_ATTACH:
      TThread.CreateAnonymousThread(procedure begin
        StartMyThreadsAndWaitBegin();
      end).Start;
    DLL_PROCESS_DETACH:
      TThread.CreateAnonymousThread(procedure begin
        StopMyThreadsAndWaitEnd();
      end).Start;
    DLL_THREAD_ATTACH:;  
    DLL_THREAD_DETACH:;
  end;
end;

begin
  DllProc := @DLLEntryPoint;
  DLLEntryPoint(DLL_PROCESS_ATTACH);
end.

注: 此问题是属于系统多线程处理的问题,或者说是属于Windows API的使用方法问题,使用其他VB VC等开发的人员也可以参考此解决方法。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • windows 常用thread方法

    1.HANDLE CreateThread( _In_opt_LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_S...

    战神伽罗
  • VCL线程的同步方法 Synchronize(用消息来同步)

    RTL(Run-Time library),运行时库,包括System、SysUtils、Math三个单元,提供的函数与语言、编译器、操作系统及进程有关

    战神伽罗
  • Windows APC机制 & 可警告alertable的线程等待状态

    摘要:Windows APC的全称为(asynchronous procedure call)翻译为中文即“异步过程调用”。《Windows APC机制(一)》...

    战神伽罗
  • 线程有多少种状态?Runnable 一定在执行任务吗?

    哈喽,大家好,我是狗哥。好久没有更新原创文章了。主要是因为今年上半年这段时间都在忙着运营我的小号:上路的狗哥,主要分享一些职场以及生活中的高效又有趣的工具,比如...

    一个优秀的废人
  • 哪些经常在java面试官嘴上的面试题

    具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运...

    java乐园
  • day5(面向对象2)

    wait notify notifyAll 都使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,以为只有同步才具有锁。 为什么这些操作线程...

    小二三不乌
  • 为什么局部变量是线程安全的?

    当多个线程访问add方法的时候 并操作add方法下的变量,永远都不会导致数据竞争,为什么呢? look at the next line↓:

    奕仁
  • 【Python之旅】第六篇(四)

        我们知道,不同进程之间的内存空间数据是不能够共享的,试想一下,如果可以随意共享,谈何安全?但是一个进程中的多个线程是可以共享这个进程的内存空间中的数据的...

    py3study
  • Lock

    从上面的synchronized释放锁可以看出,只有synchronized代码块执行完毕或者异常才会释放,如果代码块中的程序因为IO原因阻塞了,那么线程...

    爱撒谎的男孩
  • 并发编程

    线程安全 线程安全概念 : 当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的. ...

    海仔

扫码关注云+社区

领取腾讯云代金券