Windows编程_Lesson006_初识线程

进程内容的回顾

我们知道,在启动一个线程的时候,操作系统会自动的为我们创建进程内核对象和线程内核对象;
进程内核对象主要代表了当前进程中的地址空间等,而线程内核对象才是执行进程地址空间的代码,所以线程才是Windows中的重点,也是较为难以理解的一部分内容。

线程的创建

其实在进入main函数之前就已经启动了主线程mainStartUp,并且这个线程就会调用指定的函数,这个函数我们是可以手动设置的,比如在vs2015中,我们可以通过项目属性进行设置:
这里写图片描述
(但是只是修改这一点还不行,以后再补充别的内容,使我们能够真正的修改线程的入口函数)

我们通常的main函数,其实就是一个主线程函数,因为我们的线程在启动的时候,必须要告诉他当前要运行娜个函数,然后再依次的执行这个函数中的代码。

那么,如何才能创建我们自己的线程呢?我们可以使用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
);

lpThreadAttributes这个参数一般设置为nullptr,但是,如果你想让你的子系统能够继承当前这个线程对象的句柄,那么就需要传递这个结构的对象指针,结构体如下所示,

typedef struct _SECURITY_ATTRIBUTES {
  DWORD  nLength;
  LPVOID lpSecurityDescriptor;
  BOOL   bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

将bInheritHandle设置为true,就能够被子进程继承。
这里写图片描述

dwStackSize这个参数是用来标线程的栈空间的大小,我们可以设置一个数值,也可以设置为0,当设置为0的时候,它会根据编译器设置的栈保留大小,一般为1MB,来决定新创建函数的堆栈大小。
这里写图片描述
实际上,编译器会将我们传入的这个堆栈大小的参数和默认的堆栈大小来做比较,最终会选择较大的作为此线程函数的堆栈空间。
问题又来了,我们的主线程一般分配的是1Mb的堆栈空间,但是我们在创建新的线程函数的时候,最小也是1Mb的空间,毫无疑问,此时主线程的堆栈空间已经不够使用了,那么就会抛出栈溢出的异常,这个时候,异常会被当前程序捕获,就会再次分配更多的栈空间。
如果一次性分配的空间太大,导致栈溢出处理之后的栈空间还是不够用,此时就会将这个异常抛出到本程序之外的异常处理。这个时候有两种解决方法。一种是在创建线程函数时,就指定更大的栈空间大小;另一种情况是通过分批次的方式,多次分配栈空间来达到分配更大的栈空间。

lpStartAddress这个参数表示的是函数的起始地址,即函数的入口地址(函数名)。需要注意的是,这个入口函数并不是任意的,它是有一个函数原型的,如下所示:

DWORD WINAPI ThreadProc(_In_ LPVOID lpParameter);

必须满足上面的形式的函数才能作为新线程的入口函数。

lpParameter这个参数就是线程入口函数的参数,它是一个void*的指针。

dwCreationFlags这个参数表示创建的线程的运行方式,如果是0,则表示立即运行,如果是CREATE_SUSPENDED,则表示创建的线程处于挂起的状态,需要调用ResumeThread函数才能运行。

lpThreadId这是一个输出参数,将会传递新线程的id值。一般来说,我们不需要这个值,直接传入nullptr即可,因为我们有这个线程的HANDLE值。

在创建完线程之后,如果我们不需要对线程函数做相应的操作时,我们应该使用CloseHandle函数将线程内核对象关闭掉。

主线程和新线程之间的关系
请看下面的例子:
这里写图片描述
从结果上来看,什么也没有打印出来,但是,我们在主线程函数里面做一个延时,再看结果又会变得怎样呢?
这里写图片描述
此时就能将-1打印出来了。这是为什么呢?
来,我们了解一下父进程这子进程与主线程和子线程的区别:
我们都知道,对于进程,子进程不会因为父进程的消亡而消亡;而对于线程,就不想进程那样了,因为子线程和主线程都会以抢占的方式进行执行,当主线程结束的时候,子线程也会跟着死亡,这是因为,在主线程结束的时候,我们整个的进程就消亡了,所以当前的所有的子线程也会跟着消亡。
为了让所有线程能够正常的退出,我们必须保证在主线程结束之前,其它的所有子线程必须都要全部正常退出,否则会有可能会出现问题,比如内存泄漏等!
下面我们来看一个线程执行顺序的例子:

#include <Windows.h>
#include <tchar.h>

class ThreadClass
{
};

DWORD WINAPI ThreadFunc(LPVOID lParam)
{
    _tprintf(TEXT("In ThreadFunc...\r\n"));
    Sleep(100);
    _tprintf(TEXT("Out ThreadFunc...\r\n"));

    return 0;
}

int main()
{
    ThreadClass param;
    HANDLE hHandle = CreateThread(nullptr, 0, ThreadFunc, &param, 0, nullptr);
    _tprintf(TEXT("WaitForSingleObject...\r\n"));
    WaitForSingleObject(hHandle, INFINITE);
    CloseHandle(hHandle);

    system("pause");
    return 0;
}

执行结果如下:
这里写图片描述
但是根据我们的代码,这样的执行顺序并不是绝对的,因为我们的线程是抢占式运行的,所以有可能先打印出In ThreadFunction…。
使用WaitForSingleObject函数就可以避免了主线程退出了,子线程还没有退出的情况,这样就会使得我们的线程函数变得安全啦。

线程函数的参数传递

我们再看一个例子,代码如下:

#include <Windows.h>
#include <tchar.h>

class ThreadClass
{
public:
    int m_iNum;
};

DWORD WINAPI ThreadFuncOther(LPVOID lParam)
{
    ThreadClass *pDemo = (ThreadClass *)lParam;
    _tprintf(TEXT("%d\r\n"), pDemo->m_iNum);

    return 0;
}

DWORD WINAPI ThreadFunc(LPVOID lParam)
{
    ThreadClass demo;
    demo.m_iNum = 100;
    CloseHandle(CreateThread(nullptr, 0, ThreadFuncOther, &demo, 0, nullptr));

    return 0;
}

int main()
{
    ThreadClass param;
    HANDLE hHandle = CreateThread(nullptr, 0, ThreadFunc, &param, 0, nullptr);
    _tprintf(TEXT("WaitForSingleObject...\r\n"));
    WaitForSingleObject(hHandle, INFINITE);
    CloseHandle(hHandle);

    system("pause");
    return 0;
}

运行结果如下:
这里写图片描述
从结果上来看,是我们想要的结果啊,并看不出有什么问题,但是实际上,我们的代码是有问题的,这次运行结果正确可能是我们比较幸运,才打印出了正确的结果。
我们可以对上面的例子稍作修改,将ThreadFuncOther线程函数最前面添加一个延时函数,比如延时100毫秒,ThreadFuncOther代码如线程函数的代码下所示:

DWORD WINAPI ThreadFuncOther(LPVOID lParam)
{
    Sleep(100);

    ThreadClass *pDemo = (ThreadClass *)lParam;
    _tprintf(TEXT("%d\r\n"), pDemo->m_iNum);

    return 0;
}

那么,我们再来看一下打印结果。
这里写图片描述
此时并没有打印出结果,并且还出现了一个异常。这是为什么呢?因为我们在ThreadFunc线程函数中申请了一个局部变量,如下所以:

DWORD WINAPI ThreadFunc(LPVOID lParam)
{
    ThreadClass demo;
    demo.m_iNum = 100;
    CloseHandle(CreateThread(nullptr, 0, ThreadFuncOther, &demo, 0, nullptr));

    return 0;
}

当ThreadFunc线程函数结束后,它会清理所有的堆栈,使得ThreadFuncOther线程函数的参数变成了一个野指针,从而导致异常错误。

DWORD WINAPI ThreadFuncOther(LPVOID lParam)
{
    Sleep(100);

    ThreadClass *pDemo = (ThreadClass *)lParam;
    _tprintf(TEXT("%d\r\n"), pDemo->m_iNum);

    return 0;
}

所以我们在进行参数传递的时候,一定不要传递局部变量,我们可以传递堆空间或者使用static 修饰的参数。

深入理解时间片

 启动两个线程
     在第一个线程打印1--100
     在第二个线程打印101--200
     那么他们会以什么顺序打印出来呢?

 有两种情况

1.未知 以为线程是抢占式的
2. 有一定规律,要么先打印1–100,再打印101–200,要么先打印101-200,再打印1–100,因为中间是一个for循环,连续的

#include <Windows.h>
#include <tchar.h>

DWORD WINAPI ThreadNo1(LPVOID lParam)
{
    for (int i=1; i<=100; ++i)
    {
        _tprintf(TEXT("No1:%d\r\n"), i);
    }
    return 0;
}

DWORD WINAPI ThreadNo2(LPVOID lParam)
{
    for (int i = 101; i <= 200; ++i)
    {
        _tprintf(TEXT("No2:%d\r\n"), i);
    }
    return 0;
}

int main()
{
    HANDLE hThread[2];
    hThread[0] = CreateThread(nullptr, 0, ThreadNo1, nullptr, 0, nullptr);
    hThread[1] = CreateThread(nullptr, 0, ThreadNo2, nullptr, 0, nullptr);

    WaitForMultipleObjects(2, hThread, TRUE, INFINITE);

    system("pause");
    return 0;
}

打印结果如下所示:
这里写图片描述
从结果来看,打印出来的并没有规律,所以结果是未知的。
那么我们的主线程会不会抢占资源呢?
我们在主线程打印201–300的数字,看看运行结果
这里写图片描述
从运行结果可以看出,这三个线程也是以同一优先级来抢占CPU的执行时间。
那么我们有没有办法来控制CPU执行的时间片呢?目前来说是没有办法的。
如果我们有一个特殊的需求,就是想让这三个线程交叉的来打印呢?可以实现吗?答案是肯定的,这就需要我们知道线程中的信号有一定的了解。让我们来看看是怎么实现的!!!
其实在线程中都有一个信号量,当线程发生阻塞时,这个信号量就变成无信号量了,此时该线程就会把CPU执行的时间片让出来,只有线程是有信号量的时候,CPU才会分给这些线程分配时间片。

#include <Windows.h>
#include <tchar.h>

enum ThreadSignal
{
    eNo1,
    eNo2,
    eNo3
};

ThreadSignal g_ThreadSignal;

DWORD WINAPI ThreadNo1(LPVOID lParam)
{
    for (int i=1; i<=100; ++i)
    {
        while (g_ThreadSignal != eNo1)
        {
            Sleep(1);
        }
        _tprintf(TEXT("No1:%d\r\n"), i);

        g_ThreadSignal = eNo2;
    }
    return 0;
}

DWORD WINAPI ThreadNo2(LPVOID lParam)
{
    for (int i = 101; i <= 200; ++i)
    {
        while (g_ThreadSignal != eNo2)
        {
            Sleep(1);
        }
        _tprintf(TEXT("No2:%d\r\n"), i);

        g_ThreadSignal = eNo3;
    }
    return 0;
}

int main()
{
    HANDLE hThread[2];
    hThread[0] = CreateThread(nullptr, 0, ThreadNo1, nullptr, 0, nullptr);
    hThread[1] = CreateThread(nullptr, 0, ThreadNo2, nullptr, 0, nullptr);

    g_ThreadSignal = eNo1;

    for (int i = 201; i <= 300; ++i)
    {
        while (g_ThreadSignal != eNo3)
        {
            Sleep(1);
        }
        _tprintf(TEXT("No3:%d\r\n"), i);
        g_ThreadSignal = eNo1;
    }

    WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
    CloseHandle(hThread[0]);
    CloseHandle(hThread[1]);
    system("pause");
    return 0;
}

执行结果如下:
这里写图片描述
上面的执行结果就是按照我们想要的执行顺序打印的,实现的原理很简单,就是通过一个全局变量来控制线程的执行顺序,这样的方式简单易懂,有很多地方都这么用,值得借鉴!!!

线程的退出

我们都知道,当一个进程正常结束的时候,它会做一些事情如下:
1. 销毁进程里面的临时对象,也就是调用析构函数;
2. 释放进程里面栈,并且释放进程所拥有的内核对象;
3. 将返回值设置为退出代码,进程入口函数的返回值设置为退出代码;
4. 减少进程内核对象的使用计数。

那么对于线程正常结束时,又会做什么呢?它跟进程做的事情是一模一样的。
1. 销毁线程里面的临时对象,也就是调用析构函数(窗口对象和hook对象,这两个内核对象是属于线程的,其它内核对象大部分都属于进程的);
2. 释放线程里面栈,并且释放线程所拥有的内核对象;
3. 将返回值设置为退出代码,线程入口函数的返回值设置为退出代码;
4. 减少线程内核对象的使用计数。

实际上进程的退出就是主线程的退出,所以进程的退出代码就是主线程的退出代码,从这个角度来看,进程确实是“惰性“的,就连退出代码也是由主线程来做的!!!
我们也可以强制退出一个线程,有TerminateThread和ExitThread这两个函数。
ExitThread函数表示立即结束当前线程,TerminateThread函数可以结束其它的线程。
我们需要注意的是:当我们的线程碰到ExitThread函数时,它会立即终止运行,然后会将属于当前线程的栈销毁,但是不会去调用析构函数,所以就很容造成内存泄漏。(比如我们在构造函数里面new了一段空间,但是通过ExitThread函数结束本线程就不会调用析构函数来进行堆空间的释放,从而造成内存的泄漏!!!)。
所有在一个合格的程序里面,不应该出现这两个函数来强制结束线程函数!!!我们应该通过正常的逻辑代码来实现线程的正常退出。

线程的启动

进程在启动的时候,会分配一些内存空间,这些内存空间是属于进程的,而不是属于线程。所以线程在启动的时候,会做下面的一些事情:
1. 内核对象,包括使用计数、退出码、signaled(CPU通过signaled来切换线程)、context(线程上下文,存储了当前线程的CPU寄存器状态,IP指令寄存器,SP栈寄存器,这两个寄存器是线程上下文存储的基础,指令寄存器存储的是CPU下一条要执行的指令,栈寄存器指明了栈分配的空间地址);
2. 线程去进程中申请一块内存空间,作为当前线程的栈(传递两个参数,线程函数的开始地址和线程函数的参数),线程的入口函数对于逆向工程来说是很重要的。

总复习

内核对象

内核对象是用来做什么的?我们现在接触了哪些内核对象?
每个内核对象都有使用计数,我们每次看到一个HANDLE时候,我们就需要注意了,它里面会有一个使用计数,这个使用计数是用来给内核进行关闭的。
现在已经接触到的内核对象有:
设备:(目录、磁盘、串口、并口、邮件、命名管道、匿名管道、套接字,也就是说我们对于所有的设备进行的I/O操作都是一致的,都会有同步、异步等操作。)
进程:进程和进程之间是独立的,哪怕父进程和子进程之间也是独立的,它们所拥有的内存也是独立的。
进程是惰性的,每一个进程都有一块独立的内存,但是它是虚拟内存。
线程:线程是积极的,它会去争取CPU的执行时间片, 来进行运行。
线 程和线程之间是独立的,这个独立只是针对它们的栈(数据)是独立,但是线程和进程是非独立的,线程需要运行进程空间中的代码,进程需要线程来运行代码,这两者缺一不可,缺了任何一方,整个进程都会消亡,比如说所有的线程会在进程结束时消亡,因为进程中的代码和空间已经不存在了;进程在没有任何一个线程在运行的时候,它也会消亡,因为它里面的空间地址和代码已经没有意义了。先这么简单理解吧,因为线程分为主线程和次线程,这样绕起来可能有点晕,以后有时间再详细的理一遍。
所以我们需要让没没有任何一个线程运行时,再让进程消亡,这才是正常的程序设计。
如果是某一个线程被强行关闭,但是进程还存在时,可能导致内存泄漏(因为进程在退出的时候会清理堆栈),在进程不存在时,可能导致内核对象泄漏。
这里有一个疑问,如果我们强行关闭的是主线程(也就是说强制退出主线程和强制退出进程造成的结果一样吗?它们之间又有什么联系和区别呢?),那么此时会不会有可能导致内核对象泄漏和内存泄漏同时出现呢?我觉得是有可能的!!!

进程启动的权限问题

在进程启动的时候有两个函数可以使用,一个是CreateProcess,另一个是ShellExcute,那么这两个函数有什么区别吗?区别大了!!!
CreateProcess函数是用当前进程里面的一个线程来启动一个进程,新启动的进程将会是当前进程的子进程,它的使用计数会变成2,所以我们一定要CloseHandle,否则当前的子进程的内核对象将无法被操作系统正确的释放,直到当前父进程消亡的时候,子进程的内核对象才会被释放;
ShellExcute是Vista操作系统之后,使用UAC对子进程进行提权的时候使用的一种方式,它是Shell启动的进程,因为只有Shell(资源管理器)启动的进程才能够拥有提权的操作(因为一个没有管理员权限的进程是没有办法来进行提权操作的),使用Shell启动进程是在被逼无奈的时候才使用的,一般情况下是不会去使用的。

线程从启动到死亡的详细讲解

当进程中的某一个线程执行了CreateThread之后,做了下面一些事情:
线程创建:
一、创建一个线程的内核对象
在线程内核对象中还有下面内容:
1. 使用计数,使用计数将会决定当前内核对象何时被销毁,刚初始化后的使用计数是2;
2. 暂停计数,它是一个unsigned int 类型,因为我们可以对一个线程暂停多次,除非暂停计数等于0的时候才能够运行,其它情况下都不能运行,并且刚初始化的线程的暂停计数是等于1的,也就是说不能够被立即运行;
3. 退出代码,STILL_ACTIVE);
4. Signaled,FALSE;
5. CONTEXT,值为空。

二、分配一段空间作为栈(操作系统帮我们做的这些事情)
1. 会在栈的最上面压入两个值,第一个值是lParam,第二个值是lpFuncAddr

三、线程上下文结构体 CONTEXT
下面是CONTEXT结构体,这个寄存器中包含了CPU寄存器中的一些值,总的来说,CONTEXT保存的是CPU上一次运行时的寄存器状态
1. IP(指令寄存器),决定下一条指令在哪儿运行,它会指向RtlUserThreadStart函数,这是一个未公开的函数(即.dll中未导出的函数),也就是不能被直接调用的,RtlUserThreadStart函数的参数有两个,分别是(lpParam,lpFuncAddr),返回值是void;
2. SP(栈寄存器),将指向栈顶,会指向lpFuncAddr;
(当CONTEXT被填充完成后,我们就可以进入第四步了!!!)

四、交给CPU调度
………………………….
中间一直在运行
………………………….

到了最后,我们需要知道RtlUserThreadStart函数到底做了什么事情,

也就是线程结束
会做下面的事情:
1.设置一个结构化异常,SEH;
2.调用线程函数,并且将了lParam参数传递进去;
3.等待线程函数的返回;
4.调用ExitThread函数。

typedef struct _CONTEXT {

    //
    // The flags values within this flag control the contents of
    // a CONTEXT record.
    //
    // If the context record is used as an input parameter, then
    // for each portion of the context record controlled by a flag
    // whose value is set, it is assumed that that portion of the
    // context record contains valid context. If the context record
    // is being used to modify a threads context, then only that
    // portion of the threads context will be modified.
    //
    // If the context record is used as an IN OUT parameter to capture
    // the context of a thread, then only those portions of the thread's
    // context corresponding to set flags will be returned.
    //
    // The context record is never used as an OUT only parameter.
    //

    DWORD ContextFlags;

    //
    // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
    // set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT
    // included in CONTEXT_FULL.
    //

    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    DWORD   Dr3;
    DWORD   Dr6;
    DWORD   Dr7;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
    //

    FLOATING_SAVE_AREA FloatSave;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_SEGMENTS.
    //

    DWORD   SegGs;
    DWORD   SegFs;
    DWORD   SegEs;
    DWORD   SegDs;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_INTEGER.
    //

    DWORD   Edi;
    DWORD   Esi;
    DWORD   Ebx;
    DWORD   Edx;
    DWORD   Ecx;
    DWORD   Eax;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_CONTROL.
    //

    DWORD   Ebp;
    DWORD   Eip;
    DWORD   SegCs;              // MUST BE SANITIZED
    DWORD   EFlags;             // MUST BE SANITIZED
    DWORD   Esp;
    DWORD   SegSs;

    //
    // This section is specified/returned if the ContextFlags word
    // contains the flag CONTEXT_EXTENDED_REGISTERS.
    // The format and contexts are processor specific
    //

    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

C语言创建多线程方法

C语言的函数在多线程的时候很容易出现问题,主要是因为出现在全局函数的上面,比如errno函数,它是属于C语言的错误处理机制,它是一个全局的,非线程安全的。举一个不安全的例子:假如有三个线程,它们共用同一个errno,因为线程是抢占式运行的,这样得到的errno的值就很难区分是哪个线程出现的这个错误码,从而导致线程变得不安全了。
所以为了解决C语言中多线程不安全的问题,设计了_beginthreadex这个函数(它并非属于Windows API)。
那么_beginthreadex函数是怎么做到让C语言函数进行多线程编程时变得安全呢?
它是将errno这些全局的函数写到了线程里面,使得这些全局函数变成了当前线程所独有的函数,也就是变成了非全局函数了,还是那errno举例,此时的errno函数就相当于_errno函数,_errno函数其实在我们当前线程的堆区(如果有的话)中获取errno错误码,但是,如果当前线程并不存在这一块堆区,那么,我就会去全局变量里面来获取我们的errno错误码,那么这个堆区是怎么来的呢?它就是通过_beginthreadex而来的,_beginthreadex函数的参数和CreateThread函数的参数是一模一样的,虽然参数意义一样,但是参数的类型变得不一样了。因为C语言函数中不应该包含Windows中任何的变量类型。

uintptr_t _beginthreadex( // NATIVE CODE  
   void *security,  
   unsigned stack_size,  
   unsigned ( __stdcall *start_address )( void * ),  
   void *arglist,  
   unsigned initflag,  
   unsigned *thrdaddr   
); 
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. 从上面可以看出,_beginthreadex函数实际上比CreateThread函数多做了一些事情,多开辟了一块空间,多开辟的这块空间是分配在堆上面的,多开辟的这块空间是用来存储C语言的全局变量,使得C语言的全局变量变成线程安全的,把C语言的全局变量的意义赋予线程中变量的意义,然后再调用CreateThread函数。
2. _beginthreadex函数跟endthreadex函数是搭配使用的,endthreadex函数主要是用来清理_beginthreadex函数多分配的一段空间,还有_beginthread函数跟endthread函数是搭配使用的。
3. 推荐使用_beginthreadex函数跟endthreadex函数进行新线程的创建,并且更加具有移植性,而不推荐使用CreateThread函数,而_beginthread函数并没有进行堆的新建,所以也不推荐使用。

线程的状态

这里我们需要借助VS编译器的spy+++工具,这个工具可以窗口、进程、线程、日志等很多信息,这里我们借助这个工具主要来查看线程的信息,我们以QQ程序的某一个线程为例,如下图所示:
这里写图片描述
其中用红色圈起来的我们重点讲一下,其它的就先不讲了,也不是很重要。
其中线程状态是处于等待的状态,结合我们前面所学的知识,可以知道,线程内核对象中的等待计数肯定是大于0的;还有上下文开关的数值是20515794,因为我们的电脑中CPU是被抢占式的执行的,从而也说明了Windows系统是一个非实时性的操作系统,从而导致线程被不停地切换,在切换的时候,我们的线程上下文CONTEXT会被不停地加载,写入,加载、写入……,就这样一直重复下去,20515794这个惊人的数字就是线程上下文加载、写入的次数。

下面是一个简单的例子,注释里面详细介绍了线程的挂起、休眠等状态

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


unsigned int WINAPI ThreadProc(void *)
{
    int nNum = 0;
    while (true)
    {
        _tprintf(TEXT("%d\r\n"), nNum++);
    }

    return 0;
}

int main()
{
    HANDLE handle = (HANDLE)_beginthreadex(nullptr, 0, ThreadProc, nullptr, 0, nullptr);

    Sleep(100);
    SuspendThread(handle);  // 这个函数会使线程函数的暂停计数加 1,问题来了,既然这个函数可以让暂停计数加1,那么运行两次就是加2了,
                            // 如果想让线程回复运行,就必须调用两次ResumeThread函数才能让线程恢复运行,事实证明确实也是这样的。
    Sleep(1000);
    ResumeThread(handle);   // 这个函数会使线程函数的暂停计数减 1
    _tprintf(TEXT("pause\r\n"));
    Sleep(100);
    SuspendThread(handle);
    WaitForSingleObject(handle, INFINITE);


    // 线程的状态:
        // 1. 启动
            // CONTEXT
            // 使用计数     = 2
            // 暂停计数 = 1,但是在CreateThread完成之后,会减1,也就是最终=0(可以进入CPU的调度,当前的线程是可执行的状态)
        // 2. 运行
            // CPU调度
            // 执行我们的函数,然后时不时的切换我们的线程    ->  写入CONTEXT   ->  读取CONTEXT
        // 3. 挂起
            // 暂停线程的运行,在暂停状态下,是不会被CPU调度的
            // 我们不应该进行挂起线程的操作,因为我们无法确保线程里面不会出现任何问题,尤其是有new操作的时候,更容易出问题。
            // SuspendThread函数的返回值表示的是已经被挂起了多少次,不包括当前这一次挂起的次数
            // 需要注意的是,这和CPU调度时的挂起是完全两码事,因为CPU调度时的挂起是通过操作系统复杂的算法来完成的,这都是在CPU调度池
            //     中的内存里面进行的,而我们使用SuspendThread函数是将线程从CPU调度池中拿出来,是的这个线程进行挂起。
        // 4. 等待,也等同于休眠
            // 调用Sleep(100)函数来让线程处于等待(休眠)状态,就相当于告诉CPU在100毫秒之内,我们的线程是无需调度的,操作系统只需要去
            //     调度可调度的线程即可,并且Sleep(100)函数不会更改我们线程内核对象的暂停计数,等过了100毫秒之后,CPU会再次的来对本线程
            //     进行调度,但是后面的100毫秒并非是一个绝对的值,因为我们的操作系统并非是实时的,它只是一个接近值。
            // 当我们调用了Sleep(100)函数,它做了如下事情:
            //     立即放弃剩余的时间片,告诉操作系统我想休眠100毫秒,此时操作系统将不会再来调度我们的线程
            // Sleep函数有两种极端的用法,当Sleep(INFINITE)表示操作系统会永远等待,知道进程消亡时,它才随着进程一块消亡;
            //     另一种极端用法是Sleep(0),它代表放弃当前剩余时间片,将这个调度交给其它线程,但是此时时间片已经经过了切换;
            //     SwitchToThread()这个函数会将我本线程的CPU时间片转给另外一个线程,多转给的线程是不确定的,但是CPU中有一套算法,叫做CPU时间
            //     饥饿度,它会转给CPU时间饥饿度比较高的线程。
            //     总的来说Sleep(0)表示放弃当前的CPU时间片(该用法比较多),而SwitchToThread()表示将我本次的CPU时间片转给非常需
            //     要CPU时间片的线程上(该用法比较少);
        // 5. 消亡

    system("pause");
    return 0;
}

CONTEXT结构体

通过前面的学习,我们都知道,线程之间不停地被切换,才能保证每个线程有机会被执行到,每次切换的时候都会涉及到一个非常重要的结构体,这就是CONTEXT结构体。线程运行结束的时候,会把运行当前线程 的寄存器里面的值保存到CONTEXT结构体中,当线程要进行下次调度的时候,又会把当前CONTEXT的结构体加载到寄存器中,从而保证接着上次的运行状态继续运行,所以CONTEXT是我们线程中非常重要的一个结构体,因为计算机是通过寄存器进行工作的。而CONTEXT保存了寄存器的值,如果我们能够获取或者修改CONTEXT的值,我们就能够知道这个线程运行的状态和即将运行的状态。我们通过下面一个小示例来演示一下:

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

unsigned int __stdcall ThreadRun(void *lParam)
{
    int nNum = 0;
    while (true)
    {
        _tprintf(TEXT("ThreadRun:%d\r\n"), nNum++);
    }

    return 0;
}

unsigned int __stdcall ThreadMonitor(void *lParam)
{
    HANDLE hThread = (HANDLE)lParam;
    while (true)
    {
        CONTEXT context;
        context.ContextFlags = CONTEXT_ALL;
        GetThreadContext(hThread, &context);
        _tprintf(TEXT("EAX:0X%X    ESP:0X%X    EIP:0X%X\r\n"), context.Eax, context.Esp, context.Eip);
    }

    return 0;
}

int main()
{
    HANDLE hThreads[2];
    hThreads[0] = (HANDLE)_beginthreadex(nullptr, 0, ThreadRun, nullptr, 0, nullptr);
    hThreads[1] = (HANDLE)_beginthreadex(nullptr, 0, ThreadMonitor, hThreads[0], 0, nullptr);

    WaitForMultipleObjects(sizeof(hThreads)/sizeof(HANDLE), hThreads, TRUE, INFINITE);

    for (int i=0; i<sizeof(hThreads) / sizeof(HANDLE); ++i)
    {
        CloseHandle(hThreads[i]);
    }

    return 0;
}

下图是打印的结果:
这里写图片描述
从结果看起来好像还好,其实这些值并不是很真实的,因为我们的线程是以抢占式的方式运行的,所以获取到的CONTEXT值是不准确的,严格来说是滞后的。
我们之前提到过SuspendThread和ResumeThread函数,它们分别代表暂停线程和恢复线程,哦们不妨试试这两个函数,看看能不能达到我们的目的。

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


unsigned int __stdcall ThreadRun(void *lParam)
{
    int nNum = 0;
    while (true)
    {
        _tprintf(TEXT("ThreadRun:%d\r\n"), nNum++);
    }

    return 0;
}

unsigned int __stdcall ThreadMonitor(void *lParam)
{
    HANDLE hThread = (HANDLE)lParam;
    while (true)
    {
        CONTEXT context;
        context.ContextFlags = CONTEXT_ALL;
        GetThreadContext(hThread, &context);
        SuspendThread(hThread);
        _tprintf(TEXT("EAX:0X%X    ESP:0X%X    EIP:0X%X\r\n"), context.Eax, context.Esp, context.Eip);
        ResumeThread(hThread);
    }

    return 0;
}

int main()
{
    HANDLE hThreads[2];
    hThreads[0] = (HANDLE)_beginthreadex(nullptr, 0, ThreadRun, nullptr, 0, nullptr);
    hThreads[1] = (HANDLE)_beginthreadex(nullptr, 0, ThreadMonitor, hThreads[0], 0, nullptr);

    WaitForMultipleObjects(sizeof(hThreads)/sizeof(HANDLE), hThreads, TRUE, INFINITE);

    for (int i=0; i<sizeof(hThreads) / sizeof(HANDLE); ++i)
    {
        CloseHandle(hThreads[i]);
    }

    return 0;
}

可是结果会让我们大吃一惊,竟然什么也没有打印出来!!!
这里写图片描述
这是怎么回事呢?我们可以通过单步调试,会发现它先会去运行ThreadRun线程中的_tprintf函数,还没有等_tprintf函数执行完成,它就会立刻去执行ThreadMonitor线程的_tprintf函数,并且此时已经将ThreadRun线程挂起了,然后就会一直卡死在这个函数中,为什么会出现这样的结果呢?这是因为_tprintf函数导致的问题,_tprintf函数会使用I/O,并且这个I/O变量是全局的,当ThreadRun线程被挂起的时候,ThreadRun线程中的_tprintf函数还没有执行完成,这个I/O全局变量还没有被释放,导致ThreadMonitor线程的_tprintf函数也无法使用全局I/O变量,所以此时再使用时就会出现问题,这也说明了_tprintf函数是非线程安全的,C语言中类似于这样的非线程安全的函数有很多很多。那么我们有没有办法让它正常运行呢?答案是肯定的。我们只需要做一件事情,那就是确保_tprintf函数运行完成后才被暂停,我们使用一个全局变量来实现这样一个逻辑,代码如下:

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

BOOL g_bUsing = FALSE;
unsigned int __stdcall ThreadRun(void *lParam)
{
    int nNum = 0;
    while (true)
    {
        // 这就相当于上锁的功能
        if (!g_bUsing)
        {
            g_bUsing = TRUE;
            _tprintf(TEXT("ThreadRun:%d\r\n"), nNum++);
            g_bUsing = FALSE;
        }
    }

    return 0;
}

unsigned int __stdcall ThreadMonitor(void *lParam)
{
    HANDLE hThread = (HANDLE)lParam;
    while (true)
    {
        CONTEXT context;
        context.ContextFlags = CONTEXT_ALL;
        GetThreadContext(hThread, &context);
        SuspendThread(hThread);
        if (!g_bUsing)
        {
            g_bUsing = TRUE;
            _tprintf(TEXT("EAX:0X%X    ESP:0X%X    EIP:0X%X\r\n"), context.Eax, context.Esp, context.Eip);
            g_bUsing = FALSE;
        }
        ResumeThread(hThread);
    }

    return 0;
}

int main()
{
    HANDLE hThreads[2];
    hThreads[0] = (HANDLE)_beginthreadex(nullptr, 0, ThreadRun, nullptr, 0, nullptr);
    hThreads[1] = (HANDLE)_beginthreadex(nullptr, 0, ThreadMonitor, hThreads[0], 0, nullptr);

    WaitForMultipleObjects(sizeof(hThreads)/sizeof(HANDLE), hThreads, TRUE, INFINITE);

    for (int i=0; i<sizeof(hThreads) / sizeof(HANDLE); ++i)
    {
        CloseHandle(hThreads[i]);
    }

    return 0;
}

打印结果如下所示:
这里写图片描述
这就相当于给我们的_tprintf函数上了一把锁,这样避免了_tprintf函数的非线程安全在多线程调用之间带来的问题。上面的CONTEX值是实时的,并且也避免了_tprintf函数带来的非线程安全的问题。

原子操作

我们先来看一下一个全局变量被多个线程同时操作时到底会出现什么问题,代码如下所示:

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

int g_num = 0;
unsigned int __stdcall ThreadRun1(void *lParam)
{
    g_num++;
    return 0;
}

unsigned int __stdcall ThreadRun2(void *lParam)
{
    g_num++;
    return 0;
}

int main()
{
    HANDLE hThreads[2];
    hThreads[0] = (HANDLE)_beginthreadex(nullptr, 0, ThreadRun1, nullptr, 0, nullptr);
    hThreads[1] = (HANDLE)_beginthreadex(nullptr, 0, ThreadRun2, hThreads[0], 0, nullptr);

    WaitForMultipleObjects(sizeof(hThreads)/sizeof(HANDLE), hThreads, TRUE, INFINITE);
    _tprintf(TEXT("g_num:%d\r\n"), g_num);

    for (int i=0; i<sizeof(hThreads) / sizeof(HANDLE); ++i)
    {
        CloseHandle(hThreads[i]);
    }

    system("pause");
    return 0;
}

运行结果如下:
这里写图片描述
没错,你看的就是实际打印出来的,但是为什么会这样呢?不是应该为2吗?当然了,这个结果大多数是等于2的,只不过我运行了好多次,才碰巧打印出1这个结果,但是不管怎么说,这个值是不在我们的意料中的!!!
要想知道真正的原因,我们应该需要使用反汇编,也就是更底层的代码来进行分析。
这里写图片描述
红色方框圈起来的是最关键的代码,CPU在执行的时候,它的最小执行单元就是一条汇编指令,CPU既然是这样工作的,那么我们就可以找到为什么会出现我们刚才的错误结果了。分析如下所示:

/下面是是在两个线程中的汇编指令:

/正常的执行顺序应该是这样的
g_num = 0
ThreadRun1
00854B7E mov eax,dword ptr [g_num (0859148h)]
00854B83 add eax, 1
00854B86 mov dword ptr[g_num(0859148h)], eax
g_num = 1

ThreadRun2
g_num = 1
00854B7E mov eax,dword ptr [g_num (0859148h)]
00854B83 add eax, 1
00854B86 mov dword ptr[g_num(0859148h)], eax
g_num = 2

但是我们的线程是抢占式运行的,所以有的时候,并不能像我
们上面的代码那样运行,很有可能会出现下面的情况:

// g_num = 0
ThreadRun1
00854B7E  mov         eax,dword ptr [g_num (0859148h)]
// 保存CONTEXT            此时的eax寄存器保存的值是0

// 切换线程
// g_num = 0
// ThreadRun2
00854B7E  mov         eax,dword ptr [g_num (0859148h)]
00854B83  add         eax, 1
00854B86  mov         dword ptr[g_num(0859148h)], eax
// g_num = 1

// 切换线程
// ThreadRun1
// 加载CONTEXT            eax = 0
00854B83  add         eax, 1
00854B86  mov         dword ptr[g_num(0859148h)], eax
// g_num = 1

所以最终的结果导致是1,虽然总的指令代码没有少运行,但是结果却错了!!!
面对这样的问题,有的同学就会提出,那么我们在多线程中避免使用全局变量不就行了吗?答案是否定的!!!因为我们在项目开发的时候,很多时候都需要多线程和全局变量。那么既然这样,我们有没有办法在多线程情况下安全操作全局变量呢?答案是肯定的!!!在Windows下有关于原子操作(锁操作)的API。
使用Windows API进行原子操作的代码如下:

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

long g_num = 0;
unsigned int __stdcall ThreadRun1(void *lParam)
{
    InterlockedExchangeAdd(&g_num, 1);
    //g_num++;
    return 0;
}

unsigned int __stdcall ThreadRun2(void *lParam)
{
    InterlockedExchangeAdd(&g_num, 1);
    //g_num++;
    return 0;
}

int main()
{
    HANDLE hThreads[2];
    hThreads[0] = (HANDLE)_beginthreadex(nullptr, 0, ThreadRun1, nullptr, 0, nullptr);
    hThreads[1] = (HANDLE)_beginthreadex(nullptr, 0, ThreadRun2, nullptr, 0, nullptr);

    WaitForMultipleObjects(sizeof(hThreads)/sizeof(HANDLE), hThreads, TRUE, INFINITE);
    _tprintf(TEXT("g_num:%d\r\n"), g_num);

    for (int i=0; i<sizeof(hThreads) / sizeof(HANDLE); ++i)
    {
        CloseHandle(hThreads[i]);
    }

    system("pause");
    return 0;
}

执行结果如下所示:
这里写图片描述
随你怎么运行,结果肯定都是2!!!
那么什么是原子操作呢?同一资源在同一时间只有一个线程能够访问!!!
内部的实现说简单也简单,说复杂也复杂。它的内部实现是给我们传进去的变量地址加了一个硬件锁,当有其它线程来访问时,如果这个变量正在被访问,那么就会拒绝其它线程的访问,从而保证了同一个变量同一时间只能被一个线程访问。
除了InterlockedExchangeAdd这个函数,我们经常使用的还有InterlockedExchange函数,下面我们就用InterlockedExchange函数实现一个旋转锁!Windows给我们提供了这些函数,就是为了方便我们进行多线程程序的开发。这些函数能够帮助我们对一些变量进行原子级别的操作。Windows在我们每一次调用这一类的函数时,在对它的值修改之前,会在硬件上发出一个信号来告诉其它线程这个变量已经被占用了,就跟我们上面那个通过一个全局变量来模拟上锁功能一样,只是Windows提供的这些函数是硬件级别的操作,无论是执行速度还是执行效率都是很高的,也不会占用太多的CPU资源。这些原子间的操作有时候不符合我们的需求,有些时候需要我们自己实现一些类似于这些函数的功能,这种做法我们称为上锁。

旋转锁

这种上锁方式是通过原子级别来进行的,所以效率高、运行速度快!旋转锁只是上锁的一种方式。
先看代码:

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

BOOL g_bUsing = FALSE;
unsigned int __stdcall ThreadRun1(void *lParam)
{
    while (InterlockedExchange((long *)&g_bUsing, TRUE) == TRUE)
        Sleep(0);

    // 上锁后的代码
    _tprintf(TEXT("hello one\r\n"));

    InterlockedExchange((long *)&g_bUsing, FALSE);

    return 0;
}

unsigned int __stdcall ThreadRun2(void *lParam)
{
    while (InterlockedExchange((long *)&g_bUsing, TRUE) == TRUE)
        Sleep(0);


    // 上锁后的代码
    _tprintf(TEXT("hello two\r\n"));

    InterlockedExchange((long *)&g_bUsing, FALSE);

    return 0;
}

int main()
{
    HANDLE hThreads[2];
    hThreads[0] = (HANDLE)_beginthreadex(nullptr, 0, ThreadRun1, nullptr, 0, nullptr);
    hThreads[1] = (HANDLE)_beginthreadex(nullptr, 0, ThreadRun2, nullptr, 0, nullptr);

    WaitForMultipleObjects(sizeof(hThreads)/sizeof(HANDLE), hThreads, TRUE, INFINITE);

    for (int i=0; i<sizeof(hThreads) / sizeof(HANDLE); ++i)
    {
        CloseHandle(hThreads[i]);
    }

    system("pause");
    return 0;
}

但是这种旋转锁也会存在一些问题,因为我们原来也了解过线程的优先级,优先级的存在导致有些优先级比较低的线程一直得不到CPU的调度,从而形成了饥饿度比较高的线程。CPU在调度的时候,它有一个调度区(指的所有能够被调度的线程),我们上面列举的旋转锁中的两个线程的优先级如果比较高,并且它们又形成了旋转锁的形式,也就是说只相互调用这两个优先级比较高的线程,而排在它们后面的优先级比较低的线程就会一直得不到运行,此时我们可以使用SwitchToThread()来放弃当前的时间片,并且将它调度给饥饿度比较高的线程来获得CPU的时间片。但是在我们平时开发的时候,我们不会去设置线程的优先级,这样就会导致所有线程的优先级是一致的,所以也不会存在饥饿线程,这是旋转锁的一个劣势旋转锁的另一个劣势 是它会占用大量的CPU执行时间,使得CPU的使用率过高,从上面的例子我们也能看到,旋转锁使用了一个死循环来实现的。但是旋转锁可以达到我们的效果。

我们通过两个全局变量来控制线程函数的依次顺序执行,代码如下:

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

volatile long g_lNum = 1;
volatile BOOL g_bUsing = FALSE;
unsigned int __stdcall ThreadRun(void *lParam)
{
    volatile

    long lTemp = (long)(lParam);
    while (true)
    {
        if (g_lNum != lTemp)
        {
            Sleep(0);
        }
        else
        {
            if (!g_bUsing)
            {
                ++g_lNum;
                g_bUsing = TRUE;
                _tprintf(TEXT("%d\r\n"), lTemp);
                g_bUsing = FALSE;
                break;
            }
        }
    }

    return 0;
}

int main()
{
    HANDLE hThreads[1000];
    for (int i=0; i<1000; ++i)
    {
        hThreads[i] = (HANDLE)_beginthreadex(nullptr, 0, ThreadRun, (void *)(1+i), 0, nullptr);
    }

    //WaitForMultipleObjects(1000, hThreads, TRUE, INFINITE);
    Sleep(3000);

    for (int i = 0; i < 1000; ++i)
    {
        CloseHandle(hThreads[i]);
    }

    system("pause");
    return 0;
}

这样就能够让我们创建的1000个线程按照开始创建的顺序一次执行,代码的具体逻辑就不讲解了,也很简单,我们需要说一个函数,那就是WaitForMultipleObjects函数,我们这个例子里面没有使用这个函数,这是为什么呢?因为我们创建的线程个数超过了WaitForMultipleObjects函数的最大个数64,所以这个函数将不会再起作用,我们就通过Sleep方式来进行等待所有线程函数的执行完毕。

还有另一种实现方式,使用了旋转锁的方式来实现代码块的原子操作,代码如下所示:

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

const int THREADNUMBER = 64;
volatile long g_lNum = 1;
volatile BOOL g_bUsing = FALSE;
unsigned int __stdcall ThreadRun(void *lParam)
{
    long lTemp = (long)(lParam);
    while (true)
    {
        if (g_lNum != lTemp)
        {
            Sleep(0);
        }
        else
        {
            while (InterlockedExchange((long *)&g_bUsing, TRUE) == TRUE)
            {
                Sleep(0);
            }

            ++g_lNum;
            _tprintf(TEXT("%d\r\n"), lTemp);

            InterlockedExchange((long *)&g_bUsing, FALSE);
            break;
        }
    }

    return 0;
}

int main()
{
    HANDLE hThreads[THREADNUMBER] = { INVALID_HANDLE_VALUE };
    for (int i = 0; i < THREADNUMBER; ++i)
    {
        hThreads[i] = (HANDLE)_beginthreadex(nullptr, 0, ThreadRun, (void *)(1 + i), 0, nullptr);
    }

    WaitForMultipleObjects(THREADNUMBER, hThreads, TRUE, INFINITE);

    for (int i = 0; i < THREADNUMBER; ++i)
    {
        CloseHandle(hThreads[i]);
    }

    system("pause");
    return 0;
}

volatile关键字

为了避免编译器在编译的时候对代码进行优化,在debug的模式下还好,几乎不怎么优化,但是在release版本下,编译器就会尽可能的对我们的代码进行优化,但是这种优化有的时候是我们不想要的结果。比如,我们有一个全局变量,经过编译器优化后的代码有可能直接取该变量的值,而不去该变量的地址取它的实际值,所以如果我其它的地方修改了该变量的值,这边还不知道已经被修改了,还是用来原来存储好的值来使用,这样就会导致我们的代码不会按照我们的逻辑进行执行。为了避免被编译器进行优化,应该在该变量的最前面加上一个volatile关键字。

线程函数的参数传递问题

按照_beginthreadex这个函数的所要传递的参数类型,第4个参数应该传入的是一个指针,那么我们就将局部变量i的地址传进出,在线程中解析的时候,就按照解引用的方式解析,请看代码:

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

int g_iNum = 0;
const int LOOPCOUNT = 1000;
const int THREADCOUNT = 1000;

unsigned int __stdcall ThreadFunc(void *lParam)
{
    int *pThreadNo = (int*)lParam;
    g_iNum = 0;
    for (int i=0; i<LOOPCOUNT; ++i)
    {
        g_iNum += i;
    }
    _tprintf(TEXT("Thread%4d:g_iNum=%d\r\n"), *pThreadNo, g_iNum);
    return 0;
}

int main()
{
    HANDLE hThreads[THREADCOUNT] = { nullptr };
    for (int i=0; i<THREADCOUNT; ++i)
    {

        hThreads[i] = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, &i, 0, nullptr);
    }
    Sleep(100000);

    for (int i=0; i<THREADCOUNT; ++i)
    {
        CloseHandle(hThreads[i]);
    }

    return 0;
}

运行结果如下所示:
这里写图片描述
怎么线程看起来还会重复执行呢?因为我们传递的是变量的地址,并不是传递的变量的值,由于线程之间是抢占式运行的,所以当主线程里面的变量i的值已经被修改了好多次,它所创建的线程才得到CPU的调度,假如当时创建这个线程时传入的i的值是10,但是在这个线程得到CPU调度的时候,此时i的值已经变成12了,所以此时的值就不准了,那么我们该怎么解决这个问题,其实很简单,我们不能传递局部变量的地址,要传递值,就能得到解决,代码如下所示:

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

int g_iNum = 0;
const int LOOPCOUNT = 1000;
const int THREADCOUNT = 1000;

unsigned int __stdcall ThreadFunc(void *lParam)
{
    int iThreadNo = (int)lParam;
    g_iNum = 0;
    for (int i=0; i<LOOPCOUNT; ++i)
    {
        g_iNum += i;
    }
    _tprintf(TEXT("Thread%4d:g_iNum=%d\r\n"), iThreadNo, g_iNum);
    return 0;
}

int main()
{
    HANDLE hThreads[THREADCOUNT] = { nullptr };
    for (int i=0; i<THREADCOUNT; ++i)
    {
        hThreads[i] = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, (void *)i, 0, nullptr);
    }
    Sleep(100000);

    for (int i=0; i<THREADCOUNT; ++i)
    {
        CloseHandle(hThreads[i]);
    }
    system("pause");
    return 0;
}

当我们传递的是i的值的时候,那么线程的编号肯定就不会出现问题了,结果如下所示:
这里写图片描述
但是运行结果是正确的吗?让我们看看吧!
这里写图片描述
经过仔细的查找,终于找到一个错误的结果,这说明我们的程序还是存在问题,这个问题究竟又是出在哪里了呢?这是因为我们的全局变量g_iNum在多个线程之间切换时出现了问题,这个实验我们在上面的通过反汇编例子已经讲过了。但是有的人会说,我们不是学过InterlockedExchangeAdd这个原子操作的函数吗?那我们试试这个函数,看看结果如何:

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

long g_iNum = 0;
const int LOOPCOUNT = 1000;
const int THREADCOUNT = 1000;

unsigned int __stdcall ThreadFunc(void *lParam)
{
    int iThreadNo = (int)lParam;
    g_iNum = 0;
    for (int i=0; i<LOOPCOUNT; ++i)
    {
        InterlockedExchangeAdd(&g_iNum, i);
        //g_iNum += i;
    }
    _tprintf(TEXT("Thread%4d:g_iNum=%d\r\n"), iThreadNo, g_iNum);
    return 0;
}

int main()
{
    HANDLE hThreads[THREADCOUNT] = { nullptr };
    for (int i=0; i<THREADCOUNT; ++i)
    {
        hThreads[i] = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, (void *)i, 0, nullptr);
    }
    Sleep(100000);

    for (int i=0; i<THREADCOUNT; ++i)
    {
        CloseHandle(hThreads[i]);
    }
    system("pause");
    return 0;
}

这里写图片描述
我们看到这个结果更离谱了,结果都不对,让我们用汇编代码看一下到底为什么吧!!!

InterlockedExchangeAdd(&g_iNum, 1);
01343869  mov         eax,1  
0134386E  mov         ecx,offset g_iNum (01349148h)  
01343873  lock xadd   dword ptr [ecx],eax  

从汇编代码可以看出,使用InterlockedExchangeAdd函数只能保证加的操作是正确的,而不能保证整个for循环不能被打断。那么我们又该怎么做才能达到我们的要求呢?我们通过旋转锁就能达到我们的目的!!!

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

volatile long g_iNum = 0;
volatile BOOL g_bUsing = FALSE;
const int LOOPCOUNT = 1000;
const int THREADCOUNT = 1000;


unsigned int __stdcall ThreadFunc(void *lParam)
{
    int iThreadNo = (int)lParam;

    while (InterlockedExchange((long *)&g_bUsing, TRUE) == TRUE)
    {
        Sleep(0);
    }

    g_iNum = 0;
    for (int i=0; i<LOOPCOUNT; ++i)
    {
        InterlockedExchangeAdd(&g_iNum, i);
        //g_iNum += i;
    }
    _tprintf(TEXT("Thread%4d:g_iNum=%d\r\n"), iThreadNo, g_iNum);

    InterlockedExchange((long *)&g_bUsing, FALSE);
    return 0;
}

int main()
{
    HANDLE hThreads[THREADCOUNT] = { nullptr };
    for (int i=0; i<THREADCOUNT; ++i)
    {
        hThreads[i] = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, (void *)i, 0, nullptr);
    }
    Sleep(100000);

    for (int i=0; i<THREADCOUNT; ++i)
    {
        CloseHandle(hThreads[i]);
    }
    system("pause");
    return 0;
}

运行结果如下:
这里写图片描述
这样结果就不会出现任何问题了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值