C++ 多线程(二)Win32线程1

C++ 多线程(二)Win32线程1


概述

Microsoft Windows 操作系统对多线程编程的支持几乎类似于 POSIX 线程提供的支持。区别不在于实际的功能,而在于 API 函数中的名称。

每个 Win32 进程至少有一个线程,我们将其称为主线程。我们将假设操作系统将以循环方式为每个程序线程提供一个时间片。事实上,Win32 程序中的线程将与其他程序中的线程和系统线程竞争 CPU,而这些其他线程可能有更高的优先级。

让我们看一下这个简单的例子。

主线程会生成一个新线程来增加 myThread 函数内的myCounter,同时主线程会一直等待用户输入一个字符(getchar()),并在我们输入除 ‘q’ 字符以外的任何时候打印出计数器值:

要使用 Windows 多线程函数,我们必须包含<windows.h>。为了创建线程,Windows API 提供了 CreateThread() 函数。

每个线程都有自己的堆栈。你可以使用 stackSize 参数指定新线程堆栈的大小(以字节为单位),这是下面例子中 CreateThread() 函数的第二个参数。如果这个整数值是零,那么线程将被赋予一个与创建线程相同大小的堆栈。

#include <windows.h>
#include <iostream>

DWORD WINAPI myThread(LPVOID lpParameter)
{
    unsigned int& myCounter = *((unsigned int*)lpParameter);
    while(myCounter < 0xFFFFFFFF) ++myCounter;
    return 0;
}

int main(int argc, char* argv[])
{
    using namespace std;

    unsigned int myCounter = 0;
    DWORD myThreadID;
    HANDLE myHandle = CreateThread(0,
                                   0, 
                                   myThread, 
                                   &myCounter, 
                                   0, 
                                   &myThreadID);

    char myChar = ' ';
    while (myChar != 'q') 
    {
        cout << myCounter << endl;
        myChar = getchar();
    }

    CloseHandle(myHandle);
    return 0;
}

输出是:

0
868171493
1177338657
3782005161
4294967295
4294967295
...

每个执行线程都从创建过程中调用一个称为线程函数的函数开始。CreateThread() 函数的第三个参数 myThread 是线程函数。线程继续执行,直到线程函数返回。这个函数的地址(也就是线程的入口点)在 threadFunc 中指定。

通过输入字符 ‘q’,我们可以结束程序。

创建 Windows 的线程

最基本的 Windows 应用程序从一个线程开始。我们用来创建子线程的函数调用是 CreateThread()。下面的语法显示了传递给 CreateThread()的参数。

HANDLE WINAPI CreateThread(
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in SIZE_T dwStackSize,
__in LPTHREAD_START_ROUTINE lpStartAddress, 
__in_opt LPVOID lpParameter, 
__in DWORD dwCreationFlags,
__out_opt LPDWORD lpThreadId 
); 

参数

  1. lpThreadAttributes[输入参数, 可选]
    一个指向 SECURITY_ATTRIBUTES 结构的指针,该结构决定返回的句柄是否可以被子进程继承。如果 lpThreadAttributes 为 NULL,则不能继承该句柄。
    结构的 lpSecurityDescriptor 成员为新线程指定了一个安全描述符。如果 lpThreadAttributes 为 NULL,线程将获得默认的安全描述符。线程的默认安全描述符中的 ACL 来自创建者的主令牌。
  2. dwStackSize[输入参数]
    堆栈的初始大小,以字节为单位。系统将这个值舍入到最近的页面。如果此参数为零,则新线程将使用可执行文件的默认大小。
  3. lpStartAddress[输入参数]
    一个指针,指向要由线程执行的应用程序定义的函数。这个指针表示线程的起始地址。
  4. lpParameter[输入参数, 可选]
    一个指向要传递给线程的变量的指针。
  5. dwCreationFlags[输入参数]
    控制线程创建的标志。
  6. lpThreadId[输出参数,可选]
    一个指向接收线程标识符的变量的指针。如果该参数为 NULL,则不返回线程标识符。

函数调用的返回值是线程的处理程序,这是与线程 ID 不同的构造。当调用不成功时,它返回 0。除了要执行的函数地址之外,如果提供了空值,所有参数都将采用默认值。下面的代码展示了如何使用 CreateThread()创建子线程。调用 GetCurrentThreadId() 将返回调用线程的整数 ID。它还捕获已创建线程的 ID。threaddid 不是很有用,因为大多数函数都使用线程句柄作为参数。

#include <Windows.h>
#include <stdio.h>

DWORD WINAPI mythread(__in LPVOID lpParameter)
{
    printf("Thread inside %d \n", GetCurrentThreadId());
    return 0;
}

int main(int argc, char* argv[])
{
    HANDLE myhandle;
    DWORD mythreadid;
    myhandle = CreateThread(0, 0, mythread, 0, 0, &mythreadid;);
    printf("Thread after %d \n", mythreadid);
    getchar();
    return 0;
}

输出是:

Thread after 6784
Thread inside 6784

CreateThread() 告诉操作系统创建一个新线程。但是它没有将线程设置为使用开发人员环境提供的库。

换句话说,尽管 Windows 创建了线程并返回该线程的句柄,但运行库还没有设置它们需要的线程本地数据结构。

因此,我们应该使用运行时库的调用,而不是调用 CreateThread()。创建线程的两种推荐方法是调用 _beginthread() 和 _beginthreadex()。当使用 _beginthread() 和 _beginthreadex() 时,必须记住在多线程库中进行链接。这将因编译器而异。

这两个函数使用不同的参数:

uintptr_t _beginthread( 
   void( *start_address )( void * ),
   unsigned stack_size,
   void *arglist 
);

uintptr_t _beginthreadex( 
   void *security,
   unsigned stack_size,
   unsigned ( *start_address )( void * ),
   void *arglist,
   unsigned initflag,
   unsigned *thrdaddr 
);

参数

  1. start_address
    开始执行新线程的例程的起始地址。对于 _beginthread,调用约定为 __cdecl 或 __clrcall; 对于 _beginthreadex,它是 __stdcall 或 __clrcall。
  2. stack_size
    新线程的堆栈大小或 0。
  3. arglist
    参数列表传递给新线程或 NULL。
  4. security
    指向SECURITY_ATTRIBUTES结构体的指针,该结构体决定返回的句柄是否可以被子进程继承。如果为NULL,则不能继承句柄。
  5. initflag
    新线程的初始状态(0 表示运行,CREATE_SUSPENDED 表示挂起);使用 ResumeThread 执行线程。
  6. thrdaddr
    指向一个 32 位变量,该变量接收线程标识符。可能是 NULL,在这种情况下它不被使用。

这两个函数之间除了参数之外还有另一个区别。_beginthread() 创建的线程将在线程退出时关闭该线程的句柄,而 _beginthreadex() 返回的句柄必须通过调用 CloseHandle() 显式关闭,这类似于 POSIX 中的分离线程。

我们看到在上面的两个函数的描述中,他们也不同的线程执行的函数类型:_beginthread() 是一个空函数,使用默认的调用协定 __cdecl 虽然 _beginthreadex() 返回一个 unsigned int 和使用 __stdcall 调用约定。

_beginthread() 和 _beginthreadex() 函数返回新创建线程的句柄。但是函数调用的实际返回类型是 uintptr_t,它必须被类型转换为一个HANDLE,才能在期望对象句柄的函数调用中使用。

下面的例子使用三种不同的方式创建线程:

#include <Windows.h>
#include <process.h>
#include <stdio.h>

DWORD WINAPI mythreadA(__in LPVOID lpParameter)
{
    printf("CreateThread %d \n", GetCurrentThreadId());
    return 0;
}

unsigned int __stdcall mythreadB(void* data)
{
    printf("_beginthreadex %d \n", GetCurrentThreadId());
    return 0;
}

void mythreadC(void* data)
{
    printf("_beginthread %d \n", GetCurrentThreadId());
}

int main(int argc, char* argv[])
{
    HANDLE myhandleA, myhandleB, myhandleC;

    myhandleA = CreateThread(0, 0, mythreadA, 0, 0, 0);

    myhandleB = (HANDLE)_beginthreadex(0, 0, &mythreadB;, 0, 0, 0);
    WaitForSingleObject(myhandleB, INFINITE);


    myhandleC = (HANDLE)_beginthread(&mythreadC;, 0, 0);
    getchar();

    return 0;
}

输出:

_beginthreadex 5256
CreateThread 5924
_beginthread 4292

WaitForSingleObject() 调用将等待一个对象发出准备就绪信号。换句话说,例程将句柄传递给一个线程,并等待该线程终止。

main() 中创建的线程在主线程退出后将不再继续运行。因此,主线程必须在退出 main() 函数之前等待它创建的线程完成。它通过调用函数 WaitForMultipleObjects() 来做到这一点。

调用 _beginthread() 看起来更方便,因为它接受更少的参数,并且在线程退出后清理句柄,但是,最好使用 _beginthreadex()。

调用 _beginthreadex() 避免了 _beginthread() 的困难。如果线程终止,调用 _beginthread() 返回的句柄将无效甚至重用。因此,不可能查询线程的状态,甚至不可能确信线程的句柄是最初指向的同一个线程的句柄。下面的例子演示了这个问题:

#include <Windows.h>
#include <process.h>
#include <stdio.h>

void mythreadA(void* data)
{
    printf("mythreadA %d \n", GetCurrentThreadId());
}

void mythreadB(void* data)
{
    volatile int i;

    // Most compiler won't eliminate the loop
    // since i is volatile
    for (i = 0; i < 100000; i++) {}

    printf("mythreadB %d \n", GetCurrentThreadId());
}

int main(int argc, char* argv[])
{
    HANDLE myhandleA, myhandleB;

    myhandleA = (HANDLE)_beginthread(&mythreadA, 0, 0);
    myhandleB = (HANDLE)_beginthread(&mythreadB, 0, 0);
    WaitForSingleObject(myhandleA, INFINITE);
    WaitForSingleObject(myhandleB, INFINITE);

    return 0;
}

输出:

mythreadA 5912
mythreadB 3092

mythreadA() 很快终止,可能在主线程调用创建第二个线程时就已经终止了。如果第一个线程已经终止,第一个线程的句柄可以作为第二个线程的句柄重用。使用第一个线程句柄的查询可能会成功,但它们将在错误的线程上工作。对 WaitForSingleObject() 的调用可能没有对任何一个线程使用正确或有效的句柄,这取决于线程的完成时间。虽然从输出中还不清楚,但仍然存在着事情不像我们期望的那样工作的可能性。

下面使用 _beginthreadex() 的例子相当于前面的代码。由 _beginthreadex() 创建的线程需要通过调用 CloseHandle() 来清理。因此,WaitForSingleObject() 的调用肯定会得到正确的句柄:

#include <Windows.h>
#include <process.h>

unsigned int __stdcall mythreadA(void* data) 
{
    return 0;
}

unsigned int __stdcall  mythreadB(void* data)
{
    volatile int i;

    // Most compiler won't eliminate the loop
    // since i is volatile
    for (i = 0; i < 100000; i++) {}

    return 0;
}

int main(int argc, char* argv[])
{
    HANDLE myhandleA, myhandleB;

    myhandleA = (HANDLE)_beginthreadex(0, 0, &mythreadA;, 0, 0, 0);
    myhandleB = (HANDLE)_beginthreadex(0, 0, &mythreadB;, 0, 0, 0);
    WaitForSingleObject(myhandleA, INFINITE);
    WaitForSingleObject(myhandleB, INFINITE);
    CloseHandle(myhandleA);
    CloseHandle(myhandleB);

    return 0;
}

输出:

mythreadA 5860
mythreadB 5312

终止线程

有几种方法可以使线程终止。但是推荐的方法是让线程退出它被指示运行的函数。在下面的例子中,线程将打印出它的 ID,然后退出:

DWORD WINAPI mythreadA(__in LPVOID lpParameter)
{
    printf("CreateThread %d \n", GetCurrentThreadId());
    return 0;
}

也可以使用 ExitThread() 或 TerminateThread()

  1. 但是不建议使用这些函数调用,因为它们可能会使应用程序处于未指定的状态。线程没有机会释放任何持有的互斥锁或释放任何其他已分配的资源。
  2. 它们也不让运行时库有机会清理它们已经分配给线程的任何资源。

一个线程可以通过调用 _endthread() 或 _endthreadex() 来终止,只要注意确保线程已经获得的资源被适当地释放。这个调用(_endthread() 或 _endthreadex())需要与用于创建线程的调用相匹配。如果线程退出时调用了 _endthreadex(),该线程的句柄仍然需要被另一个调用cloaseHandle() 的线程关闭。

对于 fork-join 类型模型,会有一个创建多个工作线程的主线程,然后等待工作线程退出。主线程可以使用两个例程来等待工作线程完成:WaitForSingleObject() 或 WaitForMultipleObjects()。这两个函数要么等待单个线程的完成,要么等待一系列线程的完成。这些例程将线程的句柄作为参数,并带有一个超时值,该超时值指示主线程应该等待工作线程完成的时间。通常,INFINITE 值是合适的。下面的代码演示了等待单个线程完成所需的代码:

#include <Windows.h>
#include <process.h>
#include <stdio.h>

unsigned int __stdcall mythread(void* data) 
{
    printf("Thread %d\n", GetCurrentThreadId());
    return 0;
}

int main(int argc, char* argv[])
{
    HANDLE myhandle[2];

    myhandle[0] = (HANDLE)_beginthreadex(0, 0, &mythread, 0, 0, 0);
    myhandle[1] = (HANDLE)_beginthreadex(0, 0, &mythread, 0, 0, 0);
    WaitForSingleObject(myhandle[0], INFINITE);
    WaitForSingleObject(myhandle[1], INFINITE);
    CloseHandle(myhandle[0]);
    CloseHandle(myhandle[1]);
    getchar();

    return 0;
}

运行输出:

Thread 4360
Thread 1368

下面的代码与前面的代码相同。但是这个例子使用了 WaitForMultipleObjects()。

  1. 函数调用的第一个参数是要等待的线程数。
  2. 第二个参数是指向这些线程句柄数组的指针。
  3. 第三个参数是布尔值。如果为 true,则表示该函数应在所有线程完成时返回。如果为 false,则表示该函数应在第一个工作线程完成时返回。
  4. 最后一个参数是主线程在返回之前应该等待的时间长度。
#include <Windows.h>
#include <process.h>
#include <stdio.h>

unsigned int __stdcall mythread(void* data) 
{
    printf("Thread %d\n", GetCurrentThreadId());
    return 0;
}

int main(int argc, char* argv[])
{
    HANDLE myhandle[2];

    myhandle[0] = (HANDLE)_beginthreadex(0, 0, &mythread, 0, 0, 0);
    myhandle[1] = (HANDLE)_beginthreadex(0, 0, &mythread, 0, 0, 0);

    WaitForMultipleObjects(2, myhandle, true, INFINITE);

    CloseHandle(myhandle[0]);
    CloseHandle(myhandle[1]);
    getchar();

    return 0;
}

输出:

Thread 4712
Thread 4244

即使调用 _beginthreadex 创建的线程已经退出,它仍将继续持有资源。这些资源需要通过调用线程句柄上的 CloseHandle() 函数来释放。下面的例子演示了创建一个线程,等待它完成,然后释放它的资源的完整顺序:

#include <Windows.h>
#include <process.h>
#include <stdio.h>

unsigned int __stdcall mythread(void* data) 
{
    printf("Thread %d\n", GetCurrentThreadId());
    return 0;
}

int main(int argc, char* argv[])
{
    HANDLE myhandle;

    myhandle = (HANDLE)_beginthreadex(0, 0, &mythread;, 0, 0, 0);

    WaitForSingleObject(myhandle, INFINITE);

    CloseHandle(myhandle);

    return 0;
}

输出:

Thread 4272

恢复挂起线程

挂起的线程是当前未运行的线程。线程可以在挂起状态下创建,然后在稍后启动。如果线程处于挂起状态,那么启动正在执行的线程的调用是 ResumeThread()。它以线程的句柄作为参数。

有一个 SuspendThread() 调用将导致正在运行的线程被挂起。这个调用只能被调试器之类的工具使用。如果线程当前持有互斥等资源,挂起正在运行的线程可能会导致问题。

下面的代码演示了如何创建一个挂起的线程,然后在该线程上调用 ResumeThread()。代码使用 getchar() 调用,等待回车键被按下,将线程的创建与恢复线程的行为分开:

#include <Windows.h>
#include <process.h>
#include <stdio.h>

unsigned int __stdcall mythread(void* data) 
{
    printf("Thread %d\n", GetCurrentThreadId());
    return 0;
}

int main(int argc, char* argv[])
{
    HANDLE myhandle;

    myhandle = (HANDLE)_beginthreadex(0, 0, &mythread;, 0, CREATE_SUSPENDED, 0);
    getchar();
    ResumeThread(myhandle);
    getchar();
    WaitForSingleObject(myhandle, INFINITE);
    CloseHandle(myhandle);

    return 0;
}

我们按下返回键后得到的输出:

(hit Return key)
Thread 4580

线程的挂起状态作为一个计数器处理,因此对 SuspendThread() 的多次调用需要与对 ResumeThread() 的多次调用相匹配。

每个执行线程都与它相关联一个挂起计数。如果此计数为零,则线程不挂起。如果非零,则线程处于挂起状态。每次调用 SuspendThread() 都会增加暂停次数。每次调用 ResumeThread() 都会减少挂起计数。挂起的线程只有在其挂起计数达到零时才会恢复。因此,恢复挂起的线程意味着对 ResumeThread() 的调用次数必须与对 SuspendThread() 的调用次数相同。

内核资源

许多 Windows API 函数返回句柄。正如我们在前面的类型转换讨论中看到的,这些实际上只是无符号整数。然而,它们有一个特殊的目的。返回句柄的 Windows API 调用实际上是在内核空间中创建了一个资源。句柄只是资源的索引。当应用程序使用完资源后,对 CloseHandle() 的调用使内核能够释放相关的内核空间资源。

具有句柄的资源可以在进程之间共享。一旦资源存在,其他进程就可以打开该资源的句柄,或者复制该资源的现有句柄。重要的是要知道,内核资源的句柄只有在访问该资源的进程的上下文中才有意义。将句柄的值传递给另一个进程并不会使另一个进程能够访问该资源。内核需要支持对资源的访问,并为新进程中的现有资源提供一个新的句柄。

有些函数不返回句柄。对于这些函数,没有相关的内核资源。因此,一旦不再需要资源,就没有必要调用 CloseHandle()。

上一篇:C#-使用Win32_API的SendMessage实现指定窗口的模拟点击操作


下一篇:C#调用Win32 API 的方法