设计线程安全的 DLL

通常需要进程内部件能够安全并且有效地用于多线程客户端,Visual Basic 极大地简化了编写这种部件的工作。Visual Basic 创建的所有进程内部件都应用了单元模型线程,提供了对对象的同步访问。

图 8.4 显示了多线程的客户端如何使用单线程的进程内部件。

图 8.4 多线程客户端如何使用单线程的 DLL

注意 如果使用 C++ 编写了 COM,那么用来处理对单线程的进程内部件中的对象的所有调用的客户端线程将是调用 OleInitialize() 的第一个线程。

按这种方式使用单线程的 DLL 是安全的,但速度较慢。线程调度几乎和进程调度一样慢。当进程内部件用于多线程的客户端时,可以将其设为多线程的,从而改善部件的性能。

要使 ActiveX DLL 工程成为多线程的,应在“工程属性”对话框的“通用”选项卡中选定“请求线程”选项。

注意 进程内部件使用的线程只能是客户端用来创建对象和调用对象方法的线程。在进程内部件之内不能创建额外的线程,因此,对 DLL 工程其它线程选项都是不可用的。

重点 选择“无用户界面执行”选项将禁止任何形式的用户交互,其中包括消息框和系统错误对话框。请参阅“多线程部件的事件日志”。

如果将 ActiveX DLL 工程设为单元模型线程化,则可带来如下好处:

图 8.5 可以说明以上诸点。

图 8.5 使用多线程 DLL 的多线程客户端

如果线程 2 已经发出了对 DLL 中它的单元内的某个对象的调用,那么来自线程 3 的对线程 2 的对象的调用将被序列化。当线程 2 空闲以后,就会处理来自线程 3 的调用,并且使用来自线程 3 的参数。当请求被序列化时,线程 3 被阻塞。

当线程 3 请求的调用完成后,返回值以及所有 ByRef 的参数值将被调度回线程 3。这种同步确保了线程 3 的数据的一致。

重点 在单元模式的 DLL 中,对每个请求该 DLL 提供对象的客户线程,都会创建一个新的单元。在客户线程第一次请求这样一个对象时,会为每个新的单元执行 Sub Main。DLL 不能创建自己的线程。

详细信息 在编写对象的属性和方法时,必须遵守重入规则。请参阅“Visual Basic 的单元模型线程”。

在线程安全的DLL中的窗体和控件

如果在运行时创建窗体的实例, 那么Visual Basic 遵循的规则和它用在其它私有对象上的规则是一样的。也就是说,创建新窗体的线程就是执行 New 操作符的那个线程。如果隐式地创建窗体(例如,使用被声明为“As New Form1”的变量来访问一个窗体属性),那么创建这个窗体的线程就是执行该代码的线程。

单元线程化的 DLL 不能创建自己的线程;客户线程第一次请求 DLL 所提供的对象时,将创建一个新的单元,并为这个单元执行 Sub Main。客户请求的所有公有对象将驻留在同一个单元中,并共享全局数据。由这些公有对象创建的所有私有对象(包括窗体)也将存在于这个单元中。

Visual Basic 不提供任何能够让单元彼此识别的方法。但是多线程客户可以在线程 A 上得到对某个对象的引用,并且可以将这个引用传递给线程 B 上的一个对象。如果希望 DLL 这样做,那么应该阅读“Visual Basic 中的单元线程模型”中的关于重入的信息。

重点 由一个线程显示的模态窗体不会阻塞其它线程中代码的执行,并且对于其它线程中的窗体来说也不是模态的。显示窗体时会移交控件,就象调用了 DoEvents,同时也会导致对象中的代码被重入。

和窗体不同, ActiveX 控件是可以被客户窗体创建和使用的公有对象。 ActiveX 控件的实例总是驻留在使用它的客户窗体所在的线程中

注意 创建隐式窗体的一种特殊情况是预先声明的标识号(例如,Form1),即 Visual Basic 为每个窗体类维护的隐藏的全局窗体变量。因为 Visual Basic 为每个单元复制全局数据,因此每个线程都有独立的 Form1。为了避免混淆,可以通过显式地声明自己的窗体变量可以避免在单元线程化的部件中使用预先声明的标识号。

注意 复制全局数据意味着在标准模块中声明为 Public 的变量只对该线程来说是全局的;每个线程都有该变量的一个实例。这同时还意味着 App 对象的属性,比如 App.ThreadID 在每个线程中具有独立的实例。