msdn之多线程

线程实质上是程序中的执行路径。也是 Win32 安排的最小执行单元。线程包括堆栈、CPU 寄存器的状态和系统计划程序执行列表中的项。每个线程共享所有进程的资源。

进程包括一个或多个线程和代码、数据和内存中的其他程序资源。典型的程序资源是打开的文件、信号灯和动态分配的内存。当系统计划程序给予其中的一个线程执行控制时,即执行程序。计划程序确定应当运行哪些线程以及它们应当何时运行。较低优先级的线程可能必须等到较高优先级的线程完成任务后才能运行。在多处理器计算机上,计划程序可以将单个线程移到不同的处理器以平衡 CPU 负荷。

进程中的每个线程都独立运行。除非使这些线程相互可见,否则线程分别执行,对进程中的其他线程一无所知。线程共享公共资源,但是,必须使用信号灯或其他进程间的通信方法协调它们的工作。有关同步线程的更多信息,请参见编写多线程 Win32 程序

现在,所有版本的 CRT 都支持多线程处理,但非锁定版本的某些函数除外。有关更多信息,请参见 Multithreaded Libraries Performance

有关 CRT 版本的更多信息,请参见 C Run-Time Libraries

在库中实现 C 运行时库函数时,标准包含文件声明它们。如果使用 完全优化  (/Ox) 或  fastcall 调用约定  (/Gr) 选项,编译器假设应使用寄存器调用约定调用所有函数。运行时库函数是使用 C 调用约定编译的,标准包含文件中的声明通知编译器生成对这些函数的正确外部引用。

所有 Win32 程序都至少有一个线程。任何线程都可以创建附加线程。线程可以快速完成其工作,然后终止;也可以在程序的生存期内保持活动状态。

LIBCMT 和 MSVCRT C 运行时库提供以下用于创建和终止线程的函数:_beginthread, _beginthreadex 和 _endthread, _endthreadex

_beginthread 和 _beginthreadex 函数创建新线程;如果操作成功,则返回线程标识符。线程完成执行时自动终止,或者可通过调用 _endthread 或_endthreadex 自行终止。

Note注意

如果要从使用 Libcmt.lib 生成的程序调用 C 运行时例程,则必须使用 _beginthread 或 _beginthreadex 函数启动线程。不要使用 Win32 函数 ExitThread和 CreateThread。如果有一个以上的线程在等待挂起的线程完成它对 C 运行时数据结构的访问时被阻塞,使用 SuspendThread 会导致死锁。

_beginthread 和 _beginthreadex 函数

_beginthread 和 _beginthreadex 函数用来创建新线程。线程与进程中的其他线程共享进程的代码和数据段,但是线程具有自己的唯一寄存器值、堆栈空间和当前指令地址。系统给予每个线程 CPU 时间,使进程中的所有线程都可以同时执行。

_beginthread 和 _beginthreadex 与 Win32 API 中的 CreateThread 函数类似,但有如下差异:

  • _beginthread 和 _beginthreadex 使您可以将多个参数传递到线程。

  • 它们初始化某些 C 运行时库变量。只有在线程中使用 C 运行时库时,这一点才很重要。

  • CreateThread 帮助提供对安全属性的控制。可以使用此函数启动处于挂起状态的线程。

如果成功的话,_beginthread 和 _beginthreadex 返回新线程的句柄;如果有错误的话,则返回错误代码。

_endthread 和 _endthreadex 函数

_endthread 函数终止由 _beginthread 创建的线程(同样,_endthreadex 终止由 _beginthreadex 创建的线程)。线程会在完成时自动终止。_endthread 和_endthreadex 用于从线程内部进行条件终止。例如,专门用于通信处理的线程若无法获取对通信端口的控制,则会退出。

编写具有多线程的程序时,必须协调这些线程的行为和程序资源的用法。还必须确定每个线程接收自己的堆栈

线程间共享公共资源

Note注意

有关从 MFC 角度进行的类似讨论,请参见多线程编程:编程提示多线程编程:何时使用同步类

每个线程具有自己的堆栈和自己的 CPU 寄存器副本。其他资源(如文件、静态数据和堆内存)由进程中的所有线程共享。使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。

当多线程访问静态数据时,程序必须提供可能的资源冲突。假定有这样一个程序,一个线程更新静态数据结构,该结构包含要由其他线程显示的项的 x,y 坐标。如果更新线程更改了 x 坐标并且在更改 y 坐标之前被取代,则可能会在更新 y 坐标之前安排显示线程。该项可能会在错误的位置显示。通过使用信号灯控制对结构的访问,可以避免此问题。

互斥体(mutual exclusion 的缩写)是异步执行的线程或进程间通信的方式。此通信通常用于协调多个线程或进程的活动,通常通过锁定和取消锁定资源控制对共享资源的访问。为解决此 x,y 坐标的更新问题,更新线程将设置 mutex,在执行更新之前指示数据结构正在使用。更新线程将在两个坐标全部处理完之后清除互斥体。显示线程在更新显示之前必须等待清除互斥体。由于进程被阻止且直到清除 mutex 后才能继续,因此等待 mutex 的进程通常称为在 mutex 上“阻止”。

程序可能有多个线程访问同一文件。由于其他线程可能已经移动了文件指针,因此每个线程在读取或写入之前必须重新设置文件指针。另外,每个线程必须确保在它定位指针和访问文件两个时间之间没有被替换。这些线程应该通过用 WaitForSingleObject 和ReleaseMutex 调用将每个文件的访问括起来,以使用信号灯协调对文件的访问。下面的代码示例演示此项技术:

HANDLE    hIOMutex= CreateMutex (NULL, FALSE, NULL);

WaitForSingleObject( hIOMutex, INFINITE );
fseek( fp, desired_position, 0L );
fwrite( data, sizeof( data ), 1, fp );
ReleaseMutex( hIOMutex);

线程堆栈

应用程序的所有默认堆栈空间都被分配到称为线程 1 的第一个执行线程。因此,必须为程序所需的每个附加线程的单独堆栈指定分配的内存量。如果必要,操作系统将为线程分配附加的堆栈空间,但必须指定默认值。

调用 C 运行时库或 Win32 API 的线程必须要有足够的堆栈空间用于所调用的库和 API 函数。C printf 函数需要 500 多个字节的堆栈空间,调用 Win32 API 例程时应该有 2K 的可用堆栈空间。

因为每个线程都自己的堆栈,因此可以通过尽可能少地使用静态数据而避免潜在的数据项冲突。对于线程可私有的所有数据,将程序设计为使用自动堆栈变量。Bounce.c 程序中仅有的全局变量是 mutex 或初始化之后不再更改的变量。

Win32 还提供“线程本地存储”(TLS),存储基于线程的数据。有关更多信息,请参见线程本地存储

默认情况下,程序以多线程形式进行编译。

从命令行编译和链接多线程程序 xxx.c

  • 编译和链接程序:

    CL xxx.C

在创建、链接或执行多线程 C 程序时可能会遇到几种问题。下表将对一些更常见的问题进行描述。(有关从 MFC 角度进行的类似讨论,请参见多线程编程:编程提示。)

问题 可能的原因

出现一个消息框,显示程序已导致保护冲突。

许多 Win32 编程错误导致保护冲突。导致保护冲突的常见原因是间接将数据分配给空指针。因为这会导致程序试图访问不属于它的内存,所以发出保护冲突。

检测导致保护冲突原因的一个很容易的方式就是使用调试信息编译程序,然后在 Visual C++ 环境中通过调试器运行程序。发生保护错误时,Windows 将控制传输到调试器,光标会定位在导致问题的行上。

程序生成许多编译和链接错误。

通过将编译器的警告等级设置为最高值之一并注意警告消息,可以消除许多潜在问题。通过使用等级为 3 或等级为 4 的警告等级选项,可以检测意外的数据转换、丢失的函数原型和非 ANSI 功能的使用。

线程本地存储 (TLS) 是一个方法,通过该方法,给定的多线程进程中的每个线程都可以分配存储线程特定数据的位置。动态绑定(运行时)线程特定数据是通过 TLS API(TlsAllocTlsGetValueTlsSetValue 和 TlsFree)的方式支持的。除了现有的 API 实现,Win32 和 Visual C++ 编译器现在还支持静态绑定(加载时间)基于线程的数据。

TLS 的 API 实现

线程本地存储是通过 Win32 API 层和编译器实现的。有关更多信息,请参见 Win32 API 文档中的 TlsAllocTlsGetValueTlsSetValue 和 TlsFree

Visual C++ 编译器提供了一个关键字(而不通过 API 层)使 TLS 操作更加自动化。将在下一节(TLS 的编译器实现)描述此语法。

TLS 的编译器实现

为了支持 TLS,已将新属性 thread 添加到了 C 和 C++ 语言,并由 Visual C++ 编译器支持。此属性是一个扩展存储类修饰符,如上一节中所述。使用__declspec 关键字声明 thread 变量。例如,以下代码声明了一个整数线程局部变量,并用一个值对其进行初始化:

__declspec( thread ) int tls_i = 1;

声明静态绑定线程的本地对象和变量时必须遵守下列原则:

  • thread 属性只能应用于数据声明和定义。它不能用于函数声明或定义。例如,下面的代码生成一个编译器错误:

    #define Thread  __declspec( thread )
    Thread void func();     // This will generate an error.
    
  • 只能在具有 static 作用域的数据项上指定 thread 修饰符。包括全局数据对象(包括 static 和 extern)、本地静态对象和 C++ 类的静态数据成员。不可以用 thread 属性声明自动数据对象。下面的代码生成编译器错误:

    #define Thread  __declspec( thread )
    void func1()
    {
        Thread int tls_i;            // This will generate an error.
    }
    
    int func2( Thread int tls_i )    // This will generate an error.
    {
        return tls_i;
    }
    
  • 线程本地对象的声明和定义必须全都指定 thread 属性。例如,下面的代码将生成错误:

    #define Thread  __declspec( thread )
    extern int tls_i;        // This will generate an error, since the
    int Thread tls_i;        // declaration and definition differ.
    
  • thread 属性不能用作类型修饰符。例如,下面的代码生成一个编译器错误:

    char __declspec( thread ) *ch;        // Error
    
  • C++ 类不能使用 thread 属性。但是,可以使用 thread 属性实例化 C++ 类对象。例如,下面的代码生成一个编译器错误:

    #define Thread  __declspec( thread )
    class Thread C       // Error: classes cannot be declared Thread.
    {
    // Code
    };
    C CObject;
    

    因为允许使用 thread 属性的 C++ 对象的声明,因此下面两个示例在语义上是等效的:

    #define Thread  __declspec( thread )
    Thread class B
    {
    // Code
    } BObject;               // OK--BObject is declared thread local.
    
    class B
    {
    // Code
    };
    Thread B BObject;        // OK--BObject is declared thread local.
    
  • 不将线程本地对象的地址视为常数,并且涉及此类地址的任何表达式都不视为常数。在标准 C 中,这种作法的效果是禁止将线程本地变量的地址用作对象或指针的初始值设定项。例如,C 编译器会将下面的代码标记为错误:

    #define Thread  __declspec( thread )
    Thread int tls_i;
    int *p = &tls_i;       //This will generate an error in C.
    

    但是,此限制不适用于 C++。因为 C++ 允许动态初始化所有对象,因此可以用使用线程本地变量地址的表达式初始化对象。实现此操作的方式与实现线程本地对象结构的方式相同。例如,以上显示的代码在作为 C++ 源文件编译时不会生成错误。请注意:只有在其中获取地址的线程仍然存在的情况下,线程本地变量的地址才有效。

  • 标准 C 允许使用涉及引用自身的表达式初始化对象或变量,但只适用于非静态作用域的对象。虽然 C++ 通常允许使用涉及引用自身的表达式动态初始化对象,但是这种类型的初始化不允许用于线程本地对象。例如:

    #define Thread  __declspec( thread )
    Thread int tls_i = tls_i;                // Error in C and C++ 
    int j = j;                               // OK in C++, error in C
    Thread int tls_i = sizeof( tls_i )       // Legal in C and C++
    

    注意,包含正在初始化的对象的 sizeof 表达式不建立对自身的引用且在 C 和 C++ 中都是合法的。

    C++ 不允许此类对线程数据的动态初始化,因为将来可能要对线程本地存储功能进行增强。

  • 如果 DLL 将任何非本地数据或对象声明为 __declspec(线程),动态加载该 DLL 时会导致保护错误。使用 LoadLibrary 加载所有 DLL 后,每当代码引用非本地 __declspec(线程)数据时,将导致系统故障。由于线程的全局变量空间是在运行时分配的,因此此空间的大小是以应用程序的需求和所有静态链接的 DLL 的需求相加为基础计算出来的。使用 LoadLibrary 时,无法扩展此空间以允许放置用 __declspec(线程)声明的线程本地变量。如果 DLL 可能是用 LoadLibrary 加载的,请在 DLL 中使用 TLS API(如 TlsAlloc)来分配 TLS。






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值