Windows线程基础使用

内核对象

在Windows中,有许多不同的资源,如进程、线程、事件、文件,信号量等等,其中大部分都是通过不同的函数请求创建的。系统管理这些不同资源的方式也不尽相同,如使用CreateFile创建文件时,我们需要传入文件的打开模式,若是线程,我们则应该注册并维护线程ID,线程所属进程等信息。

系统为了以记录相关信息的方式来管理各种资源,在其内部生成数据块,就是一个数据结构,用里面的成员来记录对象的相关信息。

这个数据块是由系统内核分配的,也只有内核才能访问。Windows提供了一些函数以使我们来访问各种内核对象,调用这些函数后会返回一个HANDLE句柄,以此句柄来标识创建的对象。

HANDLE其实也是一个指针,只是被刻意模糊了,这样就可以防止用户直接操纵关键性的系统资源。一般它是一个整数值,这个值作为一个隐藏的指针表格的索引,这个表格使我们来间接访问内部系统,虽然这个HANDLE并未提供operator->,但在管理和语义上都很像指针。

系统内核也以引用计数的那种方式来管理一个内核对象(关于引用计数,可参考上篇文章),创建对象时,初始计数,另一进程对他进行访问后,计数加1,进程终止时,自动递减,当为0时才销毁该内核对象。

系统使用一个安全描述符来保护内核对象,这个安全描述符记录着谁拥有此对象,谁可以访问它,使用它。

安全描述符的结构如下:

typedef struct _SECURITY_ATTRIBUTES {
    DWORD  nLength;
    LPVOID lpSecurityDescriptor;
    BOOL   bInheritHandle;
} SECURITY_ATTRIBUTES;

在所有创建内核对象的函数中,基本都会有一个参数指向安全描述符,如CreateFile,其原型如下:

HANDLE CreateFile(
  LPCTSTR lpFileName,           //欲打开或创建的文件名
  DWORD dwDesiredAccess,        //访问模式
  DWORD dwShareMode,            //共享模式
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
                                //指向安全描述符
  DWORD dwCreationDisposition,  //创建方式
  DWORD dwFlagsAndAttributes,   //指定新建文件的属性和对文件操作的方式
  HANDLE hTemplateFile          //文件模块句柄,系统会Copy该文件模板的所有属性到当前创建的文件中。
);

其中lpSecurityAttributes就指向这个安全描述符。

有创建自然就有关闭,我们需要使用Close()函数来表示以结束使用此内核对象。此时计数会减1,若为0,系统便会销毁此内核对象。

CloseHandle()的原型如下:

BOOL CloseHandle(
    HANDLE hObject  //handle to object
);

线程(Thread)

一个多线程程序有多个执行点,在多处理器的机器上,不同的线程会真的存在;而在单处理器的机器上,系统采用一种分时技术来将每个线程切割成较短的时间间隔,一个线程执行时便暂停其它线程,给每个线程一些处理时间,这些时间是非常短的,在用户看来就像是程序支持多线程操作。

因为有了多线程,所以我们可以一边听音乐,一边编辑文档,或是一边聊微信。

若一个程序不支持多线程,当用户执行一个操作,若有别的操作还在运行,程序便会出现无响应,程序不应该允许这种情况发生。

但是线程一旦共享资源就会出现同步问题,因为任何线程都会被其它线程中断,所以使用线程并不难,重点是是如何同步对象来安全的共享资源。

Windows线程

Windows创建线程使用的API原型如下:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  //安全属性
  DWORD dwStackSize,                         //初始线程栈大小
  LPTHREAD_START_ROUTINE lpStartAddress,     //指向线程函数
  LPVOID lpParameter,                        //新线程参数
  DWORD dwCreationFlags,                     //创建的额外标志
  LPDWORD lpThreadId                         //存储系统分配给新线程的ID
);

参数虽多,但一般我们只会用到lpStartAddress和lpParameter这两个,剩余的填0或NULL即可。

线程的终止有4种方式:

  • 正常返回
  • ExitThread
  • TerminateThread
  • 终止进程

我们应该尽力使用第一种方式。

ExitThread函数的原型如下:

VOID ExitThread(
  DWORD dwExitCode   // 退出码
);

ExitThread()会终止主线程的运行,执行后,操作系统会清理该线程使用的所有系统资源,我们所使用的C++对象不会被析构。

TerminateThread函数的原型如下:

BOOL TerminateThread(
  HANDLE hThread,       // 要终止的线程句柄
  DWORD dwExitCode   // 退出码
);

从原型可以看出,TerminateThread()能杀死任何线程。这个函数是异步的,也就是说,在函数返回时并不保证线程已经终止了。这是因为,若是程序中还有其它的线程要引用被杀死的这个线程堆栈上的数据,而它已被销毁,便会出现访问错误。

可以使用WaitForSingleObject函数来保证线程终止。

终止进程的函数也可以终止线程,它们会终止该进程中的所有线程,如ExitProcess和TerminateProcess函数。这样也会影响程序的清理工作,像C++析构操作就不会调用。
安全的C/C++线程函数

在C/C++中,并不推荐使用CreateThread函数,这是由于C要早于Windows操作系统,当时的开发者并未考虑到线程问题。我们应该使用更安全的_beginthreadex函数,其原型如下:

#include <process.h> 
uintptr_t _beginthreadex(  
    void * security,                     // 线程安全相关信息,使用默认设置时传递NULL  
    unsigned stack_size,                 // 要分配给线程的栈大小,传递0时生成默认大小的栈  
    unsigned (* start_address)(void *),  // 传递线程的main函数信息  
    void * arglist,                      // 调用main时传递的参数信息  
    unsigned initflag,                   // 用于指定线程创建后的行为,传递0时,线程创建后立即进入可执行状态  
    unsigned * thrdaddr                  // 用于保存线程ID的变量地址值  
    );
//成功返回线程句柄,失败返回0

这个函数的参数个数和意义以及顺序都和CreateThread相同,只是将类型替换为了C类型,这样就不会依赖于Windows类型了。

C/C++的运行库函数会为每一个新线程准备一个独立的内存块,当使用_beginthreadex时会分配和初始化这个内存块,然后将其与新线程关联。

内存块中有一个数据结构和使用了C/C++运行库的每个线程关联,当使用这些函数时,它们可以去查找所调用线程的内存块,以不影响其它线程。

还有一个函数是_beginthread函数,使用简单,但它会让创建线程时返回的句柄失效,以防止访问内核对象,不应使用这个函数,所以也便不讨论了。

我们可以使用_beginthreadex来开一个线程:

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

//线程函数
unsigned WINAPI ThreadFunc(LPVOID arg);

int main()
{
    HANDLE hThread;      // 线程句柄
    unsigned uThreadID;  // 线程ID
    int iParam = 5;            // 线程参数

    hThread = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, (void*)iParam, 0, &amp;uThreadID);
    assert(hThread != 0);

    Sleep(3000);  // 等待3秒
    std::cout << "end of main" << std::endl;

    std::cin.get();
    return 0;
}

unsigned WINAPI ThreadFunc(LPVOID arg)
{
    int n = (int)arg;
    for (int i = 0; i &lt; n; ++i)
    {
        Sleep(1000);
        std::cout << i << " running thread" << std::endl;
    }
    return 0;
}

输出如下:

10 running thread
21 running thread
3end of main
42 running thread
53 running thread
64 running thread

在内核对象中有个标识使用状态的信息Signaled,当还有地方在引用此线程时,这个状态便为true,直到变为false时线程才退出。

若要保证线程终止再执行返回函数,可以使用WaitForSingleObject或WaitForMultipleObjects等待线程执行完毕,原型如下:

/*用于验证单个内核对象的Signaled状态*/
DWORD WaitForSingleObject(
    HANDLE hHandle,           // 要等待线程的句柄
    DWORD dwMilliseconds  // 超时时间
);
//返回值:进入signaled状态返回WAIT_OBJECT_0,超时返回WAIT_TIMEOUT

/*用于验证多个内核对象的Signaled状态*/
DWORD WaitForMultipleObjects(
    DWORD nCount,                    // 句柄数
    CONST HANDLE *lpHandles,  // 指向句柄数组
    BOOL fWaitAll,                        // 等待标志
    DWORD dwMilliseconds         // 超时时间
);

更改上面的程序使线程返回再执行后续操作:

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

unsigned WINAPI ThreadFunc(LPVOID arg);

int main()
{
    HANDLE hThread;      // 线程句柄
    unsigned uThreadID;  // 线程ID
    int iParam = 5;            // 线程参数

    hThread = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, (void*)iParam, 0, &amp;uThreadID);
    assert(hThread != 0);

    // 传递INFINITE时函数会直到内核对象变为Signaled状态都会返回
    DWORD dwSignal;
    dwSignal = WaitForSingleObject(hThread, INFINITE);
    assert(dwSignal != WAIT_FAILED);

    std::cout << "wait result:" << ((dwSignal == WAIT_OBJECT_0) ? "Signed" : "time-out") << std::endl;
    std::cout << "end of main" << std::endl;

    std::cin.get();
    return 0;
}

unsigned WINAPI ThreadFunc(LPVOID arg)
{
    int n = (int)arg;
    for (int i = 0; i &lt; n; ++i)
    {
        Sleep(1000);
        std::cout &lt;&lt; i &lt;&lt; " running thread" &lt;&lt; std::endl;
    }
    return 0;
}

现在输出如下:

10 running thread
21 running thread
32 running thread
43 running thread
54 running thread
6wait result:Signed
7end of main
posted @ 2018-07-19 16:00  cpluspluser  阅读(415)  评论(0编辑  收藏  举报