Chinaunix首页 | 论坛 | 博客
  • 博客访问: 671666
  • 博文数量: 103
  • 博客积分: 2532
  • 博客等级: 大尉
  • 技术积分: 2039
  • 用 户 组: 普通用户
  • 注册时间: 2008-11-26 16:07
文章分类

全部博文(103)

文章存档

2012年(38)

2011年(28)

2010年(16)

2009年(16)

2008年(5)

分类: C/C++

2011-05-04 10:09:00

MicrosoftVisual C++ 支持在 MicrosoftWindows(Windows XP、Windows 2000、Windows NT、Windows Me 和Windows 98)下创建多线程应用程序。如果您的应用程序需要管理多个活动(如同时进行键盘和鼠标输入),则您应当考虑使用多线程。一个线程可以处理键盘输入,而另一个线程可以筛选鼠标活动。第三个线程可以根据鼠标和键盘线程的数据更新显示屏幕。同时,其他线程可以访问磁盘文件或从通信端口获取数据。

使用 Visual C++ 的多线程编程有两种方式:使用 Microsoft 基础类库 (MFC),或使用 C 运行时库和 Win32 API。

多线程程序

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

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

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

多线程编程的库支持

现在,所有版本的 CRT 都支持多线程处理,但非锁定版本的某些函数除外。

多线程编程的包含文件

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

线程控制的 C 运行时库函数

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

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

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

注意

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

_beginthread 和 _beginthreadex 函数

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

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

  • 1._beginthread_beginthreadex 使您可以将多个参数传递到线程。

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

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

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

_endthread 和 _endthreadex 函数

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

多线程 C 程序示例

Bounce.c 是一个示例多线程程序,每次键入字母 aA 时,它都创建新线程。每个线程都在屏幕的周围弹出不同颜色的笑脸。最多可以创建 32 个线程。键入 qQ 时,发生正常的程序终止。

代码如下:


// sample_multithread_c_program.c

// compile with: /c

//

// Bounce - Creates a new thread each time the letter 'a' is typed.

// Each thread bounces a happy face of a different color around

// the screen. All threads are terminated when the letter 'Q' is

// entered.

//


#include <windows.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <conio.h>
#include <process.h>
#define MAX_THREADS 32

// The function getrandom returns a random number between

// min and max, which must be in integer range.

#define getrandom( min, max ) (SHORT)((rand() % (int)(((max) + 1) - (min))) + (min))
int main( void ); // Thread 1: main

void KbdFunc( void ); // Keyboard input, thread dispatch

void BounceProc( void * MyID ); // Threads 2 to n: display

void ClearScreen( void ); // Screen clear

void ShutDown( void ); // Program shutdown

void WriteTitle( int ThreadNum ); // Display title bar information

HANDLE hConsoleOut; // Handle to the console

HANDLE hRunMutex; // "Keep Running" mutex

HANDLE hScreenMutex; // "Screen update" mutex

int ThreadNr; // Number of threads started

CONSOLE_SCREEN_BUFFER_INFO csbiInfo; // Console information


int main() // Thread One

{ // Get display screen information & clear the screen.

    hConsoleOut = GetStdHandle( STD_OUTPUT_HANDLE );
    GetConsoleScreenBufferInfo( hConsoleOut, &csbiInfo );
    ClearScreen();
    WriteTitle( 0 );
    // Create the mutexes and reset thread count.

    hScreenMutex = CreateMutex( NULL, FALSE, NULL ); // Cleared

    hRunMutex = CreateMutex( NULL, TRUE, NULL ); // Set

    ThreadNr = 0;
    // Start waiting for keyboard input to dispatch threads or exit.

    KbdFunc();
    // All threads done. Clean up handles.

    CloseHandle( hScreenMutex );
    CloseHandle( hRunMutex );
    CloseHandle( hConsoleOut );
}

void ShutDown( void ) // Shut down threads

{
    while ( ThreadNr > 0 )
    {
        // Tell thread to die and record its death.

        ReleaseMutex( hRunMutex );
        ThreadNr--;
    }
    
    // Clean up display when done

    WaitForSingleObject( hScreenMutex, INFINITE );
    ClearScreen();
}

void KbdFunc( void ) // Dispatch and count threads.

{
    int KeyInfo;
    do
    {
        KeyInfo = _getch();
        if ( tolower( KeyInfo ) == 'a' && ThreadNr < MAX_THREADS )
        {
            ThreadNr++;
            _beginthread( BounceProc, 0, &ThreadNr );
            WriteTitle( ThreadNr );
            //Sleep(1000);

        }
    } while( tolower( KeyInfo ) != 'q' );
    ShutDown();
}

void BounceProc( void *pMyID )
{
    char MyCell, OldCell;
    WORD MyAttrib, OldAttrib;
    char BlankCell = 0x20;
    COORD Coords, Delta;
    COORD Old = {0,0};
    DWORD Dummy;
    char *MyID = (char*)pMyID;
    // Generate update increments and initial

    // display coordinates.

    srand( (unsigned int) *MyID * 3 );
    Coords.X = getrandom( 0, csbiInfo.dwSize.X - 1 );
    Coords.Y = getrandom( 0, csbiInfo.dwSize.Y - 1 );
    Delta.X = getrandom( -3, 3 );
    Delta.Y = getrandom( -3, 3 );
    // Set up "happy face" & generate color

    // attribute from thread number.

    if( *MyID > 16)
        MyCell = 0x01; // outline face

    else MyCell = 0x02; // solid face


    MyAttrib = *MyID & 0x0F; // force black background


    do
    {
        // Wait for display to be available, then lock it.

        WaitForSingleObject( hScreenMutex, INFINITE );
        // If we still occupy the old screen position, blank it out.

        ReadConsoleOutputCharacter( hConsoleOut, &OldCell, 1, Old, &Dummy );
        ReadConsoleOutputAttribute( hConsoleOut, &OldAttrib, 1,    Old, &Dummy );
        if (( OldCell == MyCell ) && (OldAttrib == MyAttrib))
            WriteConsoleOutputCharacter( hConsoleOut, &BlankCell, 1, Old, &Dummy );
        // Draw new face, then clear screen lock

        WriteConsoleOutputCharacter( hConsoleOut, &MyCell, 1, Coords, &Dummy );
        WriteConsoleOutputAttribute( hConsoleOut, &MyAttrib, 1, Coords, &Dummy );
        ReleaseMutex( hScreenMutex );
        // Increment the coordinates for next placement of the block.

        Old.X = Coords.X;
        Old.Y = Coords.Y;
        Coords.X += Delta.X;
        Coords.Y += Delta.Y;
        // If we are about to go off the screen, reverse direction

        if( Coords.X < 0 || Coords.X >= csbiInfo.dwSize.X )
        {
            Delta.X = -Delta.X;
            Beep( 400, 50 );
        }
        if( Coords.Y < 0 || Coords.Y > csbiInfo.dwSize.Y )
        {
            Delta.Y = -Delta.Y;
            Beep( 600, 50 );
        }
    }
    // Repeat while RunMutex is still taken.

    while ( WaitForSingleObject( hRunMutex, 75L ) == WAIT_TIMEOUT );
}

void WriteTitle( int ThreadNum )
{
    enum {
        sizeOfNThreadMsg = 80
    };
    char NThreadMsg[sizeOfNThreadMsg];
    sprintf_s( NThreadMsg, sizeOfNThreadMsg, "Threads running: %02d. Press 'A' " "to start a thread,'Q' to quit.", ThreadNum );
    SetConsoleTitle( NThreadMsg );
}

void ClearScreen( void )
{
    DWORD dummy;
    COORD Home = { 0, 0 };
    FillConsoleOutputCharacter( hConsoleOut, ' ', csbiInfo.dwSize.X * csbiInfo.dwSize.Y, Home, &dummy );
}


输入

a
q

编写多线程 Win32 程序

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

线程间共享公共资源

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

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

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

多线程 C 程序示例中显示的 Bounce.c 程序使用名为 ScreenMutex 的 mutex 协调屏幕更新。每当其中的一个显示线程准备写入屏幕时,将调用 WaitForSingleObject,使用 ScreenMutex 的句柄和常数 INFINITE 指示 WaitForSingleObject 调用应该在互斥体上阻止但不应该超时。如果清除了 ScreenMutex,等待函数将设置互斥体,使其他线程不能影响显示并继续执行该线程。否则,线程将阻止,直到清除互斥体。线程完成显示更新后,通过调用 ReleaseMutex 释放互斥体。

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

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

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

线程堆栈

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

_beginthread 调用中的第一个参数是指向将要执行线程的 BounceProc 函数的指针。第二个参数指定线程的默认堆栈大小。最后一个参数是传递到 BounceProc 的 ID 号。BounceProc 用 ID 号为随机数生成器提供种子,并选择线程的颜色属性和显示字符。

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

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

Win32 还提供“线程本地存储”(TLS),存储基于线程的数据。
编译和链接多线程程序

多线程 C 程序示例中对 Bounce.c 程序进行了介绍。

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

从开发环境内编译和链接多线程程序 Bounce.c

  1. 在“文件”菜单上单击“新建”,然后单击“项目”。

  2. 在“项目类型”窗格中,单击“Win32”

  3. 在“模板”窗格中,单击“Win32 控制台应用程序”,然后命名项目。

  4. 将包含 C 源代码的文件添加到项目中。

  5. 在“生成”菜单上,通过单击“生成”命令生成项目。


避免与多线程程序有关的问题

在创建、链接或执行多线程 C 程序时可能会遇到几种问题。下表将对一些更常见的问题进行描述。

问题 可能的原因

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

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

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

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

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

线程本地存储 (TLS)

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

TLS 的 API 实现

线程本地存储是通过 Win32 API 层和编译器实现的。

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

TLS 的编译器实现

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

__declspec( thread ) int tls_i = 1;

阅读(3786) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~