Windows程序设计6(内存、线程、进程)

 

一、 Windows 内存管理

  1. 地址空间:32位操作系统空间0~ 2^32-14G),地址空间越大程序越易于编写。
  2. 地址空间的划分:

2.1 用户地址空间 0 - 2G0x7FFFFFFF )存放用户的程序和数据。用户空间的代码是不能访问内核空间的数据和代码。

2.1.1 空指针区(NULL区,0-64K)系统将地址小于64K指针,都认为是空指针。

2.1.2 用户区 64K~ 0x7FFEFFFF,存放用户程序的代码和数据

2.1.3 64K禁入区(0x7FFEFFFF - 0x7FFFFFFF )

2.2 内核地址空间 2G - 4G

存放内核的代码和数据,例如系统驱动。

内核空间代码是可以访问用户空间。

  1. 区域:连续的一块内存。区域的大小一般为64K或者64K倍数。每个区域都有自己的状态

1)空闲:没有被使用

2)私有:被预定的区域

3)映像:存放代码

4)映射:存放数据

  1. 物理内存(半导体,内存条):系统可以使用的实际内存。CPU可以直接访问的内存。
  2. 虚拟内存(磁盘交换文件):将硬盘文件虚拟成内存使用。(pagefile.sys 文件)CPU如果要访问虚拟内存数据,必须将虚拟内存数据放到物理内存。经常使用的数据存放在物理内存中,不经常使用的数据存放在虚拟内存中。
  3. 内存页:系统管理内存的最小单位。内存页大小为4K,每个内存页有自己的权限。
  4. 页目表:
  5. 指针地址

 

31        22 21        12 11           0

|------------------|------------------|-------------------|

    10位          10位         12位  

    

2^10=1024         1024           4K

  页目             页表          

包含1K个页表     包含1K个页   包含4K个字节

页目中包含1K项,每项对应一个页表,页表包含1K个项,每项对应一个页,页中包含4k字节  ----   1K*1K*4K=4 G

  1. 内存获取数据,访问过程

a) CPU根据地址在物理内存中查找相应的位置。如果找到物理内存,取回数据。如果未找到,执行b)

b) 根据地址去虚拟内存中查找相应的位置。如果未找到,那么该地址没有内存空间,返回错误(野指针)。如果找到,执行c)

c) 将该地址所在内存页,置换到物理内存中,同时将原物理内存数据,存入到虚拟内存中。

d) 将物理内存中的数据返回给使用者。

  1. 内存分配

a) 虚拟内存分配-适合大内存分配,一般是1M之上的内存。

b) 堆内存分配-适合小内存分配,一般是1M以下的内存。malloc/new

c) 栈内存分配-适合小内存分配,一般是1M以下的内存。

d) 

  1. 虚拟内存:

a) 虚拟内存分配:速度快,大内存效率高。将内存和地址分配分别执行,可以先分配内存地址,在需要的时候再将地址绑定(提交)到内存。常用字大型电子表格等处理。

b) 虚拟内存的分配:

LPVOID   VirtualAlloc(

LPVOID   lpAddress,// NULL或提交地址

SIZE_T   dwSize, //分配的大小

DWORD   flAllocationType, //分配方式

DWORD   flProtect //内存访问方式

);  //分配成功返回虚拟内存地址,失败返回NULL

flAllocationType分配方式:

MEM_COMMIT - 提交内存分配之后返回地址和内存空间.(内存和地址同时分配)

MEM_RESERVE- 保留地址,分配之后只返回地址,内存空间不生成(不绑定到内存)。要使用内存必须再次提交执行,即再次执行VirtualAlloc(第一次返回的提交地址, ** , MEM_COMMIT , ** )

不足一页分配或是跨页地址分配,则操作系统会将从跨页的低位地址开始分配的。即按照页边界对齐的原则。内存绑定(提交)以页(4096字节)为单位。

获取内存状态:

VOID    GlobalMemoryStatus(

       LPMEMORYSTATUS lpBuffer   // 内存状态结构

);

参数说明:内存状态结构:

typedef struct _MEMORYSTATUS { // mst

    DWORD dwLength;        // 结构体字节数 

    DWORD dwMemoryLoad;    // 内存使用率,百分之

    DWORD dwTotalPhys;     // 物理内存总字节数

    DWORD dwAvailPhys;     // 空闲物理内存字节数 

    DWORD dwTotalPageFile; // 分页文件总字节数 

    DWORD dwAvailPageFile; // 空闲分页文件字节数 

    DWORD dwTotalVirtual;  // 虚拟内存总字节数 

    DWORD dwAvailVirtual;  // 空闲虚拟内存字节数 

} MEMORYSTATUS, *LPMEMORYSTATUS;

c) 虚拟内存的释放:

BOOL    VirtualFree(

LPVOID   lpAddress,//释放地址

SIZE_T    dwSize, //释放的大小,字节数

DWORD   dwFreeType //释放方式

); //成功返回true,失败返回false

dwSize 0, 表示全部释放

释放方式:

MEM_DECOMMIT - 只释放内存,不释放地址。

MEM_RELEASE - 地址和内存都释放。

  1. 堆内存:

a) 堆内存分配:适合分配小内存,一般是小于1M的内存。一般每个程序都有自己的堆,默认大小为1M,会根据使用情况需要进行动态调整。

b) 堆的使用

  1. 堆的信息:
    1. 获取调用进程的首个堆:

HANDLE  GetProcessHeap (void);

 //成功返回调用进程首个堆的句柄,失败返回NULL

  1. 获取调用进程的所有堆

DWORD   GetProcessHeaps(

     DWORD  NumberOfHeaps,  //堆句柄数组的容量                       

     PHANDLE  ProcessHeaps  // 保存返回堆句柄数组

);//成功返回进程堆数目,失败返回0

  1. 创建堆:

HANDLE    HeapCreate(

 DWORD    flOptions,//创建选项

 SIZE_T    dwInitialSize, //初始字节数,以后可调整

 SIZE_T    dwMaximumSize //最大字节数,0表示无穷大

); 成功返回堆句柄,失败返回NULL

参数说明:

flOptions

HEAP_GENERATE_EXCEPTIONS   堆内存分配失败则引发异常

HEAP_NO_SERIALIZE      支持不连续存取

  1. 从堆中分配内存

   LPVOID     HeapAlloc(

HANDLE   hHeap,   //堆句柄

DWORD    dwFlags,  //分配方式

SIZE_T     dwBytes  //分配内存大小

); 成功返回地址,失败返回NULL

参数说明:

HEAP_GENERATE_EXCEPTIONS   堆内存分配失败则引发异常

HEAP_NO_SERIALIZE      支持不连续存取,

(若在创建堆的时候已经指定此参数,则此参数将忽略.)

HEAP_ZERO_MEMORY   初始化清零

  1. 释放堆内存:

BOOL   HeapFree(

HANDLE  hHeap,  // 堆句柄

DWORD  dwFlags, // 释放方式,只能取HEAP_NO_SERIALIZE

LPVOID   lpMem   // 释放堆内存地址

); //成功返回true,失败返回false

 

  1. 销毁堆:

BOOL   HeapDestroy(

HANDLE   hHeap   //堆句柄

); //成功返回true ,失败返回false

当堆被销毁后,其中该堆分配的内存均将释放。

c) Win32 malloc / new 实现实际是调用了上述堆函数。即:

VirtualAlloc/HeapAlloc/malloc/newWindows平台上,函数调用关系:

new/malloc -> HeapAlloc ->VirtualAlloc

  1. 栈内存

a) 栈内存-每个线程都具有自己的栈,默认大小1M,一般是系统维护栈。

b) Windows提供了 _alloca 函数, 用于在栈上分配内存。

c) 操作系统负责自动维护栈内存的分配与释放。

  1. 内存映射文件:

a) 本质:将文件映射成内存来使用。当使用内存时,就是在使用文件。

  常用于实现进程间通信,比直接通过文件I/O效率更高。

b) 内存映射的使用:

  1. 创建/ 打开文件CreateFile。设置可读可写属性.
  2. 创建映射:

HANDLE   CreateFileMapping(

HANDLE   hFile, //文件句柄

LPSECURITY_ATTRIBUTES lpAttributes, //安全属性设为NULL

DWORD   flProtect,//访问方式可读写,PAGE_READWRITE

DWORD   dwMaximumSizeHigh,//内存映射文件大小的高32

DWORD   dwMaximumSizeLow, //内存映射文件大小的低32

LPCTSTR lpName //映射名,NULL表示匿名映射,其他进程不可访问

);   // 创建成功返回映射文件句柄,失败返回NULL

 

  1. 加载映射文件:

LPVOID    MapViewOfFile(

HANDLE   hFileMappingObject,//内存映射文件句柄

DWORD dwDesiredAccess,//访问模式,FILE_MAP_ALL_ACCESS

DWORD   dwFileOffsetHigh,  //偏移量的高32

DWORD   dwFileOffsetLow,       //偏移量的低32

SIZE_T    dwNumberOfBytesToMap  //映射的字节数量

); 成功返回地址,失败返回NULL

参数说明:

dwFileOffsetHighdwFileOffsetLow合成的偏移量,必须是区域粒度的整数倍(64K的整数倍)

使用映射,即对该返回地址的操作即可。

  1. 使用映射(内存):以内存的方式使用该映射
  2. 卸载内存映射:

BOOL   UnmapViewOfFile(

LPCVOID   lpBaseAddress //映射的地址

); .//成功返回true,失败返回false

  1. 关闭(销毁)映射:

BOOL    CloseHandle(

  HANDLE   hObject   // 句柄

);

  1. 关闭文件:

CloseHandle,同上

  1. 对于已建好的映射,打开映射:

HANDLE   OpenFileMapping(

  DWORD   dwDesiredAccess,  // 访问方式,FILE_MAP_ALL_ACCESS

  BOOL     bInheritHandle,    // 子进程是否继承此函数所返回的句柄

  LPCTSTR   lpName          //映射名

);// 成功返回映射句柄,失败返回NULL

 

c) 基于内存映射文件的进程间的通信

  1. 写进程:创建文件- 创建映射- 加载映射- 写入映射- 卸载映射 – 

        销毁映射- 关闭文件

  1. 读进程:打开映射- 加载映射- 读取映射- 卸载映射- 销毁映射
  2. 关于句柄:

句柄就是内存对象地址在句柄表中索引。通过句柄不能直接访问内存,但是可以通过APIs函数操作其标识的对象。

 

 

 

 

二、 Windows 进程

1. 基本概念

1) 进程是一个容器,包含程序执行所需要的代码,数据。资源、等信息。

   Windows 是一个多任务操作系统,可以同时执行多个进程。

2. 进程特点

1)每个进程都有自己的ID

2)每个进程都有自己的地址空间,进程之间无法访问对方的地址空间。

3)每个进程都有自己的安全属性

4)每个进程当中至少包含一个线程

   3. 进程环境信息

获取和释放环境信息

获取

   LPVOID  GetEnvironmentStrings(VOID);//返回当前进程环境变量地址

释放

   BOOL    FreeEnvironmentStrings(

    LPTSTR    lpszEnvironmentBlock  // 进程环境块指针

);//成功返回true,失败返回false

   4. 获取和设置环境变量

设置环境变量:

BOOL  SetEnvironmentVariable(

   LPCTSTR   lpName,  //变量名

   LPCTSTR   lpValue  // 变量值

);//成功返回true,失败返回false

获取环境变量:

DWORD  GetEnvironmentVariable(

LPCTSTR  lpName,  // 变量名

LPTSTR   Buffer,   //变量值缓冲区

DWORD  Size // 变量值缓冲区大小(字符为单位含尾空字符)

);//返回存在变量缓冲区的字符数,不含尾空字符

若没有找到缓冲区变量名,则返回0

5. 进程信息:

1) 获取进程ID

DWORD  GetCurrentProcessId(VOID)//返回调用进程的ID

另外,int  _getpid( void );也可以返回进程的ID

 

2) 获取进程句柄:

HANDLE  GetCurrentProcess(VOID)//返回调用进程的伪句柄(-1),可以使用该句柄访问该进程的所用操作。但其子进程不继承该句柄

6. 进程的使用:

1)创建进程:

WinExec  - Win6 遗留,现在基本不用。

ShellExecute  - Shell 操作 ,速度慢。

Createprocess   目前使用最多

BOOL  CreateProcess(

LPCTSTR   lpApplicationName,//应用程序名称路径

LPTSTR    lpCommandLine, //命令行参数

LPSECURITY_ATTRIBUTES  lpProcessAttributes, //进程安全属性

LPSECURITY_ATTRIBUTES  lpThreadAttributes,

//线程安全属性,NULL为缺省属性,同上。

BOOL   bInheritHandles, //子进程是否可以继承父进程的句柄

DWORD   dwCreationFlags, //创建方式,0表示立即启动

LPVOID   lpEnvironment, //子进程环境,NULL表示继承父进程环境

如:”book=c++\0pen=color\00”其中的\0,表示环境变量分隔符,\0\0表示结束分号。

LPCTSTR   lpCurrentDirectory,//子进程工作目录,NULL则继承父进程工作目录

LPSTARTUPINFO   lpStartupInfo, //启动信息

LPPROCESS_INFORMATION   lpProcessInformation

//进程信息,返回进程和线程的句柄ID等。

);//成功返回true,失败返回false

参数说明:

LPPROCESS_INFORMATION

typedef  struct  _PROCESS_INFORMATION { // pi

    HANDLE hProcess;  ///子进程句柄

    HANDLE hThread;   ///子进程的主线程句柄

    DWORD dwProcessId; ///子进程ID

    DWORD dwThreadId; ////子进程的主线程ID

} PROCESS_INFORMATION;

7. 等候进程

等候函数:

等候单个:WaitForSingleObject

DWORD   WaitForSingleObject(

HANDLE   hHandle,        //等待的进程、线程句柄

DWORD   dwMilliseconds // 等候时间(毫秒),INFINITE永远等候

);// 成功返回引起该函数返回的事件码,失败返回WAIT_FAILED(-1).

引起该函数返回的事件码:

WAIT_OBJECT_0    句柄有信号,进线程程已结束

WAIT_ WAIT_TIMEOUT   等候线、进程超时,进线程结束

阻塞函数,等候句柄的信号,只在句柄有信号或超出等候时间,才会结束等候。

 等候多个:WaitForMultipleObjects -

DWORD   WaitForMultipleObjects(

  DWORD  nCount, //句柄数量

  CONST HANDLE   *lpHandles,  //句柄BUFF的首地址

  BOOL    bWaitAll,//等候方式

  DWORD   dwMilliseconds      // 等候时间

);

bWaitAll - 等候方式

TRUE - 表示所有句柄都有信号,才结束等候

FASLE- 表示句柄中只要有1个有信号,就结束等候。

8. 退出进程

VOID ExitProcess(

UINT uExitCode   // 退出码

);

终止进程(终止指定进程及其线程):

BOOL  TerminateProcess(

 HANDLE  hProcess, //进程句柄

   UINT   uExitCode   // 退出码

);//成功返回true,失败返回false

9. 通过进程ID获取句柄

HANDLE   OpenProcess(

  DWORD  dwDesiredAccess,// 访问权限,PROCESS_ALL_ACCESS

  BOOL   bInheritHandle,    // 子进程是否继承父进程句柄

  DWORD   dwProcessId       // 进程ID

); 返回打开进程句柄,失败返回NULL

10. 关闭进程句柄

CloseHandle(HANDLE  handle);

三、 Windows 线程

1. 线程概念:Windows线程是可以执行的代码的实例。系统是以线程为单位调度程序。一个程序当中可以有多个线程,实现多任务的处理。

   Windows 线程特点:

1)线程都具有1个唯一标识  —— TID

2)线程具有自己的安全属性

3)每个线程都具有自己的内存栈

4)每个线程都具有自己的寄存器信息,(用以保存自己的存储状态、现场等)

  进程多任务和线程多任务:

进程多任务是每个进程都使用私有地址空间,相互独立,彼此交换数据困难。

线程多任务是进程内的多个线程使用同一个地址空间即共享。彼此交换数据方便,容易产生冲突。

线程的调度:

    CPU的执行时间划分成若干时间片,分给不同的线程,依次根据时间片轮流执行不同的线程。

   线程轮询:线程A -> 线程B -> 线程A......

2. 线程过程函数

DWORD    WINAPI ThreadProc(

   LPVOID   lpParameter //创建线程时,传递给线程的参数

);

//返回值代表线程执行的成功或失败,可由GetExitCodeThread函数获取。

3. 创建线程

HANDLE   CreateThread(

LPSECURITY_ATTRIBUTES  lpThreadAttributes,//安全属性,NULL

SIZE_T   dwStackSize,        //线程栈的初始大小,0表示与调用线程相同

LPTHREAD_START_ROUTINE  lpStartAddress, //线程处理函数的函数地址

LPVOID   lpParameter,                   //传递给线程处理函数的参数

DWORD   dwCreationFlags,          //线程的创建方式,

LPDWORD   lpThreadId                      //创建成功,返回线程的ID

); 创建成功,返回线程句柄,失败返回NULL

参数说明:dwCreationFlags,

0                        立即运行

CREATE_SUSPENDED 创建后先挂起,直到调用ResumeThread函数再运行。

4. 结束进程

终止指定线程

BOOL   TerminateThread(

   HANDLE   hThread,    // 线程句柄

   DWORD   dwExitCode   // 退出码

);//成功返回true,失败返回false

结束函数所在的线程

  VOID    ExitThread(

DWORD   dwExitCode   // 线程退出码

);

5. 关闭线程句柄

CloseHandle( HANDLE  hadle);

6. 线程的挂起和执行

挂起:

DWORD   SuspendThread(

 HANDLE   hThread   // 线程句柄

);//成功返回线程此前被挂起的次数,失败返回-1.

执行:

DWORD   ResumeThread(

  HANDLE   hThread   //线程句柄

  );//返回线程此前被恢复的次数,失败返回-1.

7. 线程信息

获取当前线程的ID:

DWORD GetCurrentThreadId(VOID);

获取当前线程的句柄:

HANDLE GetCurrentThread(VOID);

8. 打开指定ID的线程,获取其句柄

HANDLE   OpenThread(

    DWORD dwDesiredAccess,  // 访问方式

    BOOL  bInheritHandle, // 子进程是否继承父进程句柄

    DWORD dwThreadId    //线程ID

);//成功返回线程句柄,失败返回NULL。

9. 线程问题:

线程A -> 线程B -> 线程A 。。。。。

当线程A执行printf输出时,如果线程A的执行时间结束,系统会将线程A的相关信息(栈、寄存器)压栈保护,同时将线程B相关信息恢复,然后执行线程B,线程B继续输出字符。由于线程A正输出字符,线程B会继续输出,画面字符会产生混乱。

四、 线程同步技术

同步机制:原子锁、临界区(段)、事件、互斥、信号量

1. 原子锁:

问题描述:

多个线程对同一个数据进行原子操作,会产生结果丢失。比如执行++运算时,当线程A执行g_nValue1++时,如果线程切换时间正好是在线程A将值保存到g_nValue1之前,线程B继续执行g_nValue1++,那么当线程A再次被切换回来之后,会将原来线程A保存的值保存到g_nValue1上,线程B进行的加法操作被覆盖。

S1: g_nValue1  à寄存器

S2:寄存器的值+1

S3:寄存器的值—>g_nValue1

多个线程分别执行此三个过程,使得值发生覆盖。

原子锁的使用:

对单条指令操作的API

LONG  InterlockedIncrementLPLONG  lpAddend);//自增变量的共享指针

//返回自增后的结果。

LONG  InterlockedDecrementLPLONG  lpAddend);//自减变量的共享指针

//返回自减后的结果。

LONG InterlockedCompareExchangeLPLONG  lpAddend);

LONG InterlockedExchangeLPLONG  lpAddend);

LONG InterlockedExchangeAdd(LPLONG  lpAddend);

原子锁的实现:

直接对数据所在的内存操作,并且在任何一个瞬间只能有一个线程访问。

2. 临界区

问题描述:

printf输出混乱,多线程情况下同时使用一段代码。临界区可以锁定一段代码,防止多个线程同时使用该段代码。

临界区结构体:CRITICAL_SECTION  

1) 初始化一个临界区

VOID    InitializeCriticalSection(

  LPCRITICAL_SECTION   lpCriticalSection  //临界区结构体变量

);

2)进入临界区:

添加到被锁定的代码之前

VOID     EnterCriticalSection(

  LPCRITICAL_SECTION  lpCriticalSection  // 临界区结构体变量

);

阻塞函数,直到调用线程获取对指定临界区对象的所有权才返回,任何时候只有一个线程拥有临界区资源。

3) 离开临界区:

添加到被锁定的代码之后

VOID     LeaveCriticalSection(

LPCRITICAL_SECTION  pCriticalSection  // 临界区结构体变量

);

4) 删除临界区

VOID   DeleteCriticalSection(

LPCRITICAL_SECTION   lpCriticalSection  //临界区结构体变量

);

原子锁和临界区:

原子锁 - 单条指令。

临界区 - 单条或多行代码。

3. 互斥体(Mutex)

问题描述:多线程下代码或资源的共享使用。

互斥体的使用:

1) 创建互斥

HANDLE  CreateMutex(

 LPSECURITY_ATTRIBUTES  lpMutexAttributes, //安全属性

    BOOL   bInitialOwner,//调用线程是否初始拥有该互斥体

   LPCTSTR   lpName    //互斥体名字

  ); 创建成功返回互斥句柄,失败返回NULL

2)等待互斥体

添加到资源之前

WaitForSingleObject (hMutex , INFINITE);

若其他线程拥有该互斥体,则hMutex无信号,函数阻塞,直到调用线程获得对互斥体的所有权,此时hMetex有信号,函数返回。

3) 释放互斥体

添加到资源之后

BOOL   ReleaseMutex(

HANDLE hMutex   // 互斥体句柄

);//成功返回true,失败返回false

4)关闭互斥体

CloseHandle(hMutex);

互斥体和临界区的区别:

临界区 - 用户态,执行效率高,只能在同一个进程中使用。

互斥体 - 内核态,执行效率低,可以通过命名的方式跨进程使用。

4. 事件

问题描述:程序之间的通知的问题。

事件的使用:

1)创建事件

HANDLE   CreateEvent(

LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性

BOOL    bManualReset,                       

//事件重置方式,TRUE手动,FALSE自动

BOOL bInitialState,       //事件初始状态,TRUE有信号

LPCTSTR lpName  //事件命名

); 创建成功返回 事件句柄,失败返回NULL

2)等待事件

WaitForSingleObject/

WaitForMultipleObjects

3)触发事件

将事件设置成有信号状态

BOOL    SetEvent(

   HANDLE   hEvent   // 事件句柄

);

将事件设置成无信号状态

BOOL   ResetEvent(

   HANDLE   hEvent   // 事件句柄

);

4)关闭事件

CloseHandle HANDLE  handle);

注意:防止事件造成死锁。

5. 信号量

问题描述:类似于事件,解决通知的相关问题。但是可以提供一个计数器,可以设置次数。解决多个进程共享有限的资源的问题。

信号量的使用:

1) 创建信号量

HANDLE   CreateSemaphore(

  LPSECURITY_ATTRIBUTES  lpSemaphoreAttributes,

   //安全属性

  LONG    lInitialCount,        //初始化信号量资源数量

  LONG    lMaximumCount,   //信号量资源的最大值

  LPCTSTR  lpName           //信号量命名

); 创建成功返回信号量句柄,失败返回NULL

2)等待信号量

添加到资源之前。

WaitForSingleObject hSemaphoreINFINITE);

每等候通过一次,信号量的信号资源数减1,直到为0阻塞。

若资源计数为0,则hSemaphore无信号,函数阻塞,直到资源计数大于0.此时有信号,函数返回同时资源数减1;。

3)释放信号量

添加到资源之后。

BOOL  ReleaseSemaphore(

  HANDLE   hSemaphore, //信号量句柄

  LONG   lReleaseCount, //释放资源数量

  LPLONG   lpPreviousCount   

   //释放前信号量的数量,可以为NULL

);//成功返回true,失败返回false

4)关闭句柄

CloseHandle( HANDLE  handle);

6. 线程局部存储

局部与线程的全局变量

1) 分配线程局部存储

DWORD  TlsAllocVOID);

//成功返回线程局部存储索引,失败返回-1

2) 保存数据到线程局部存储

BOOL  TlsSetValue(

DWORD  dwTlsIndex ,  //线程局部存储索引

LPVOID  lpTlsValue      //数据

)//成功返回true,失败返回false

3) 线程局部储存中获取数据

LPVOID   TlsGetValue(

  DWORD   dwTlsIndex   //线程局部存储索引

);成功返回储存索引,失败返回NULL

4) 释放线程局部储存索引

BOOL     TlsFree(

  DWORD    dwTlsIndex   // 释放的线程局部存储索引

);//成功返回true,失败返回false

5) 静态线程局部存储

--declspec(thread)  int  g_cn =0;

--declspec(thread)  int  static  g_cn =0;

posted @ 2019-10-10 10:59  浩月星空  阅读(552)  评论(0编辑  收藏  举报