Win32学习3

这篇具有很好参考价值的文章主要介绍了Win32学习3。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

9、创建线程

①什么是线程?

<1>线程是附属在进程上的执行实体,是代码的执行流程。 <2> 一个进程可以包含多个线程,但一个进程至少要包含一个线程。

可以这么理解,进程属于是空间上的概念,是代表了4GB 的虚拟内存,而线程属于是时间上的概念,也就是说线程也就是当前正在运行中的实际的代码。在任务管理器中可以看到,一个进程包含数个线程,也就是当前这个进程有数段代码正在执行。(但不一定都是同时执行) 举个例子,当前电脑中是单个单核CPU,它也是可以执行同时执行多个线程的。在单核CPU上运行多线程并非真正意义上的多线程。我们可以这么理解,在某个时间点上,只能有一段代码在执行。但CPU执行的效率特别高,切换的比较快,这会执行的A线程,这一会执行的B线程,就仿佛两段代码在同时跑。单核的情况下是不存在多线程的。在某一个时间点上只能有一段代码在执行。一个CPU核只有一套寄存器,一套寄存器必定不能同时执行多个不同的代码。

总结上可以这么理解,一个单核CPU执行代码,多线程实际上是分时段的,这个时间段执行这段代码,另一个时间段执行另一段代码。但是无论是单核还是多核,给我们的感觉都好像是多个线程同时在跑。

②创建线程

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES IpThreadAttributes, //SD(安全描述符)
SIZE_T dwStackSize, // initial stack size
LPTHREAD_START_ROUTINE IpStartAddress,// thread function
LPVOID IpParameter // thread argument
DWORD dwCreationFlags, // creation option
LPDWORD IpThreadld // thread identifier
);

dwStackSize 初识堆栈大小(如果不填写就是操作系统默认给一个值) IpStartAddress 当前线程真正执行的代码的地址 IpParameter 线程所需要的参数在哪(实质上是一个指针) dwCreationFlags 创建线程的一个标识(这个成员如果填0的话,可以立即进入执行的状态),或者用 CREATE_SUSPENDED这个宏标识的话,这个线程就会处于一个挂起的状态。除非使用 ResumeThread 这个API 解除挂起状态,这个线程都不会执行。 IpThreadld 这个成员是整个函数返回的线程ID(区别于这个函数的返回值是线程句柄)

我们利用CreateThread这个API 可以创建一个新的线程,而因为没有进行线程控制,会出现“分时段性的多线程”(上方有解释),即为一段时间执行这一段,另一段时间执行另一段。我们可以用下面这段代码进行测试。

#include<stdio.h>
#include<windows.h>
DWORD WINAPI ThreadProc(LPVOID IpParameter)
{
for(int i = 0;i<5;i++)
{
Sleep(500);
printf("+++++++++%d\n",i);
}
return 0;
}

int main()
{
HANDLE hThread;
hThread = CreateThread(NULL,0,ThreadProc,NULL,0,NULL);
CloseHandle(hThread);

for(int i = 0;i<5;i++)
{
Sleep(500);
printf("---------%d\n",i);
}
return 0;
}

③向线程函数传递变量

ThreadProc 这个函数我们称为线程函数,每次创建线程的时候都要提供这样一个函数,而且 这个函数有特定的格式要求(有一个参数有一个返回值)。(但是这个函数只是告诉代码的位置在哪,其他的函数类型都也能用,只不过需要强制转换类型,如果遵守格式要求就能够减少麻烦)

<1> 线程参数。

如果我们想要将参数传进线程函数的话,原定参数 IpParameter 是空指针类型的,我们可以通过强制转换类型,如下:

DWORD WINAPI ThreadProc(LPVOID IpParameter)
{
   int * p = (int *)IpParameter; //强制转化
   
for(int i = 0;i < *p;i++)
{
Sleep(500);
printf("+++++++++%d\n",i);
}
return 0;
}

int main()
{
HANDLE hThread;
   int n;
   
   n = 10;
hThread = CreateThread(NULL,0,ThreadProc,(LPVOID)&n,0,NULL);
CloseHandle(hThread);

for(int i = 0;i<5;i++)
{
Sleep(500);
printf("---------%d\n",i);
}
return 0;
}

<2> 全局变量。

但是在我们传递线程参数的时候需要注意的一件事:举个实例,就是上方这串代码我们传递的线程参数,是位于 main 函数中的,属于局部变量位于堆栈中,就需要保证当我们创建的这个线程执行的时候,当前的这个堆栈没有被回收,即需要创建的这个线程必须要在这个 main 函数执行结束之前执行。我们通常这样理解,我们如果需要向线程传递参数我们就需要保证,参数的存活时长要大于该线程的存活时长。或者我们可以使用全局变量,因为全局变量的生命周期是一直存在的。

10、线程控制

①如何让线程停下来?

让自己停下来: Sleep()函数 让别人停下来: SuspendThread()函数 线程恢复: ResumeThread()函数

Sleep函数的意义是,当线程执行到某个阶段的时候,我们期望它停下来,就可以利用Sleep函数。但是Sleep函数只能让当前自己的函数停下来。如果我们希望让别的线程停下来,我们就可以利用 SuspendThread 函数将线程挂起,挂起的意义就是当一个线程挂起的时候,它就处于堵塞状态了,将不再占用 CPU 的时间,在该线程恢复之前都会处于堵塞状态。即一直不占用 CPU 的时间。一个线程是可以被挂起多次的,但是挂起几次就得恢复几次。

②等待线程结束:

<1> WaitForSingleObject(); <2> WaitForMultipleObjects(); <3> GetExitCodeThread();

WaitForSingleObject()这个函数的意义是,程序到这个函数这里就会处于堵塞状态,直到传进来的句柄(进程、线程)的状态发生变更(执行完毕)或者耗尽设置的等待时间,才会恢复。

DWORD WaitForSingleobject(
HANDLE hHandle, //handle to object
DWORD dwMilliseconds //time-out interval
);

如果想要一直等待直到,线程执行完毕,就可以将第二个参数设置为宏(INFINITE)。

如果想要等待多个线程,我们就需要使用WaitForMultipleObjects() 这个函数。

DWORD WaitForMultipleObjects(
DWORD nCount, //number of handles in array(句柄数量)
CONST HANDLE *1pHandles,//object-handle array(句柄数组)
BOOL bWaitAll, //wait option(等待模式)
DWORD dwMilliseconds//time-out interval(等待时间)
)

最后一个成员和WaitForSingleObject() 含义相同,第三个成员是等待模式,就是你可以指定所有对象都发生状态改变的时候结束等待(TRUE ),也可以指定有任何一个对象状态改变就结束等待。

线程函数都是有一个返回值的,其意义就是反馈函数是否执行成功,如果执行成功了返回某个值,失败了就会返回另一个值。我们就可以通过接受返回值来得到该函数是否执行成功。

GetExitCodeThread()这个函数就是能够接受到线程执行完成的返回结果的。

BOOL GetExitCodeThread(
HANDLE hThread, // handle to the thread(线程句柄)
LPDWORD 1pExitCode // termination status(返回状态)
)

使用的时候可以如这个格式将反馈结果接收:GetExitCodeThread(hThread,&dwResult);

③设置、获取线程上下文

如果说单核CPU 处理多个线程的时候,因为只有一套寄存器。当在某个时间点之后,由一个线程切换到另一个线程的时候,需要将正在进行中的线程的数据保存在一个结构体中再执行另一个线程。这个结构体为 CONTEXT,它用于存放一堆寄存器。所以我们就可以通过将线程挂起,再查看 CONTEXT中的各个寄存器的数据。

因为 CONTEXT结构体中存放了许多的寄存器,所以有个成员函数称作 ContextFlags。他能够让你想要查询那一部分的寄存器,就可以查询哪 一部分。

hThread = CreateThread(NULL,0,ThreadProc,NULL,0,NULL);

SuspendThread(hThread);

CONTEXT context;
context.ContextFlags = CONTEXT_INTEGER;

CONTEXT context; context.ContextFlags = CONTEXT_INTEGER; 利用这样一段代码,给结构体赋值就完成了一大半了。利用相应的 API 函数我们就可以将结构体赋好值,即获取线程的上下文。

BOOL GetThreadContext(
HANDLE hThread, //handle to thread with context(线程句柄)
LPCONTEXT IpContext // context structure(对应的结构体指针)
)

BOOL SetThreadContext(
HANDLE hThread, // handle to thread
CONST CONTEXT *IpContext // context structure
)

GetThreadContext(hThread,&context)我们这样使用这个 API 函数,我们需要的寄存器的值就会存在这个结构体中了。然后我们就可以打印或者获取我们想要的寄存器的值了。而且我们可以进行修改,然后利用 SetThreadContext 这个函数就可以将 CPU 中的寄存器的值进行修改了。

11、临界区

①线程安全问题:

每个线程都有自己的栈,而局部变量是存储在栈中的,这就意味着每个线程都有一份自己的“局部变量”,如果线程仅仅使用“局部变量”那么就不存在线程安全问题。 那如果多个线程共用一个全局变量呢? 如果多个线程对于全局变量的权限仅限于读,也不存在安全问题。所以如果要避免线程安全问题,就要减少多个线程共用一个全局变量而且需要保证它的权限仅限于读而非写。

②解决方法:

就需要想办法将线程变得安全。有一种解决方法是将全局变量定义为临界资源(临界资源是一次仅允许一个线程使用的资源),访问临界资源的那段程序又被称为临界区。

据此,如果我们想要保证该全局变量的访问是安全的,我们就需要自己构建一个临界区,我们可以自己写代码实现,也可以通过操作系统的提供的 API来实现。

操作系统提供的这个 API 实现原理大概如下:设置一个全局变量为令牌。如果想要访问对应的全局变量 X 线程首先需要获取这个令牌,查看这个令牌是否有人使用。然后这个令牌的值如果代表了正在使用的话,就无法获取到全局变量X,如果代表了未被使用的话,就可以获取到该全局变量。并且在执行完代码之后,将会归还令牌,即恢复成未被使用的值。其他的线程就能够继续获取到全局变量了。

③临界区实现之线程锁:

<1> 创建全局变量 CRITICAL_SECTION cs; <2>初始化全局变量 InitializeCriticalSection(&cs); <3>实现临界区 EnterCriticalSection(&cs);//进入临界区 //使用临界资源 LeaveCriticalSection(&cs);//离开临界区

而如果我们想要保证足够的安全的话,我们需要将所有对于全局变量的读写的逻辑都放在临界区里。这才算是真正的安全。循环是while(turn != i); 这种情况下,满足了互斥的条件,这时肯定只有一个线程符合条件进入,但这种情况无法满足前进的条件。因为无法判断其他代码是否想要进入。

利用一个flag和一个true来保证逻辑,flag表示想要进入,ture表示轮到谁进入临界区。这样就能够保证访问全局变量的线程安全。(此方法也称之为线程锁)

12、互斥体

①内核级临界资源

如果临界资源是一个内核级的临界资源,如一个跨进程共享的物理页或一个文件。此时同时有两个进程想要访问一个内核级的临界资源,这时应该怎么办?

这时候就需要一种跨进程的方式来实现不同进程中的线程访问同一个内核级临界资源。

image-20230717075217022

在线程锁中我们使用的是一个全局变量,其是在应用层中,而当我们碰到内核级的临界资源的时候,我们就需要一个互斥体(可以理解成为放置在内核中的令牌)。

③互斥体的使用:

我们可简化如图,可以将互斥体理解成为在零环中(内核中)的一个令牌。与之前的线程锁唯一的区别就在于这个互斥体是置于内核中的。这样才实现了跨进程的不同线程共同访问的目的。

HANDLE WINAPI CreateMutex(
 __in_opt  LPSECURITY_ATTRIBUTES lpMutexAttributes,//安全描述符(内核级对象都有)
 __in      BOOL bInitialOwner,
 __in_opt  LPCTSTR lpName//名字
);

bInitialOwner 如果此值为 TRUE 并且调用方创建了互斥锁,则调用线程将获得互斥锁对象的初始所有权。否则,调用线程不会获得互斥锁的所有权。若要确定调用方是否创建了互斥锁,请参阅返回值部分。

函数返回值: 如果函数成功,则返回值是新创建的互斥对象的句柄。 如果函数失败,则返回值为 NULL。若要获取扩展的错误信息,请调用 GetLastError。 如果互斥锁是命名互斥锁,并且对象在此函数调用之前存在,则返回值是现有对象的句柄,GetLastError 返回ERROR_ALREADY_EXISTS,忽略 bInitialOwner,并且不授予调用线程所有权。但是,如果调用方的访问权限有限,则该函数将失败并ERROR_ACCESS_DENIED,调用方应使用 OpenMutex 函数。

释放一个互斥体就利用 ReleaseMutex这个API ,参数只有一个句柄。

#include<stdio.h>
#include<windows.h>

int main()
{
   //创建一个互斥体
HANDLE g_hMutex = CreateMutex(NULL,FALSE,"123");

   //获取令牌
WaitForSingleObject(g_hMutex,INFINITE);

for(int i = 0;i<15;i++)
{
Sleep(1000);
printf("--AAAAA-----%d\n",i);
}
   //释放令牌
ReleaseMutex(g_hMutex);


return 0;
}

利用上方这个测试代码,将其放于两个编译器中执行,可以发现当 A程序执行的时候,B程序就会处于堵塞的状态,一直等待直到A程序执行完毕,

bInitialOwner 这个成员如果填的是 FLASE ,就意味着这个互斥体创建出来就能直接使用,这个成员如果填写的是 TRUE,则代表了这个互斥体是属于当前这个进程的,而如果我们想要获取“令牌”,第一个是要有对应的“信号”,第二个就是线程的拥有者。

④互斥体与线程锁的区别 <1>线程锁只能用于单个进程间的线程控制 <2>互斥体可以设定等待超时,但线程锁不能(即可以利用如WaitForSingleObject这样的API设置等待时间 ) <3>线程意外终结时, Mutex可以避免无限等待 <4>Mutex效率没有线程锁高(如果是在同一个进程中,利用线程锁能够避免创建内核对象)

我们也可以利用如下这段测试代码,保证一个进程只能运行一份(防止多开)文章来源地址https://www.toymoban.com/news/detail-571785.html

#include<stdio.h>
#include<windows.h>

int main()
{
HANDLE hMutex = CreateMutex(NULL,FALSE,"防止多开");

DWORD dwRet = GetLastError();

if(hMutex)
{
if(ERROR_ALREADY_EXISTS == dwRet)
{
CloseHandle(hMutex);
return 0;
}
}

else
{
printf("创建失败\n");
CloseHandle(hMutex);
return 0;
}

while(1)
{
Sleep(1000);
printf("6666\n");
}

return 0;
}

到了这里,关于Win32学习3的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 并发——什么是线程,什么是进程

    并发——什么是线程,什么是进程

    进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。 在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

    2024年02月13日
    浏览(10)
  • Java并发(三)----创建线程的三种方式及查看进程线程

    Java并发(三)----创建线程的三种方式及查看进程线程

    例如: 输出 注意:这里通过 @Slf4j 注解打印的日志 把【线程】和【任务】(要执行的代码)分开 Thread 代表线程 Runnable 可运行的任务(线程要执行的代码) 例如: 输出 Java 8 以后可以使用 lambda 精简代码 小结 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开

    2023年04月24日
    浏览(10)
  • 什么是进程、线程、协程

    什么是进程、线程、协程

    我们都知道计算机的核心是CPU,它承担了所有的计算任务;而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序则是具有某种功能的程序,程序是运行于操作系统之上的。 进程是一个具有一定独立功能的程序在一个数据集上

    2024年02月13日
    浏览(14)
  • 什么是多线程?进程和线程的区别是什么?如何使用Java实现多线程?

    什么是多线程?进程和线程的区别是什么?如何使用Java实现多线程?

    前面我们了解了什么是进程以及如何实现进程调度,那么今天我将为大家分享关于线程相关的知识。在学习线程之前,我们认为进程是操作系统执行独立执行的单位,但其实并不然。线程是操作系统中能够独立执行的最小单元。只有掌握了什么是线程,我们才能实现后面的并

    2024年02月13日
    浏览(16)
  • 【面试合集】说说什么是进程?什么是线程?区别?

    【面试合集】说说什么是进程?什么是线程?区别?

    操作系统中最核心的概念就是进程,进程是对正在运行中的程序的一个抽象,是系统进行资源分配和调度的基本单位 操作系统的其他所有内容都是围绕着进程展开的,负责执行这些任务的是 CPU 进程是一种抽象的概念,从来没有统一的标准定义看,一般由程序、数据集合和进

    2024年01月20日
    浏览(13)
  • 线程和进程的区别是什么?

    线程(Thread)和进程(Process)是操作系统中两个重要的概念,用于管理程序的执行。它们有以下区别: 定义: 进程:进程是程序的一个执行实例,它包含了程序的代码、数据以及执行上下文。进程是操作系统分配资源和调度的基本单位。 线程:线程是进程的子执行单元,一

    2024年02月11日
    浏览(13)
  • 06 为什么需要多线程;多线程的优缺点;程序 进程 线程之间的关系;进程和线程之间的区别

    06 为什么需要多线程;多线程的优缺点;程序 进程 线程之间的关系;进程和线程之间的区别

    CPU、内存、IO之间的性能差异巨大 多核心CPU的发展 线程的本质是增加一个可以执行代码工人 多线程的优点 多个执行流,并行执行。(多个工人,干不一样的活) 多线程的缺点 上下文切换慢,切换上下文典型值1us vs 0.3ns/cycle CPU在执行A任务(A没有执行完)时,切换到任务B,需

    2024年02月14日
    浏览(10)
  • 【Linux】 由“进程”过渡到“线程” -- 什么是线程(thread)?

    【Linux】 由“进程”过渡到“线程” -- 什么是线程(thread)?

    如何看待地址空间和页表: 地址空间是进程能看到的资源窗口 页表决定,进程真正拥有资源的情况(页表映射多少才是拥有多少) 合理的对地址空间+页表进行资源划分,我们就可以对一个进程所有的资源进行分类 虚拟地址如何找到物理地址: 最后一级页表存放的是页框的起

    2024年02月15日
    浏览(11)
  • 【JavaEE】什么是多线程?进程和线程的区别是什么?如何使用Java实现多线程?

    【JavaEE】什么是多线程?进程和线程的区别是什么?如何使用Java实现多线程?

    前面我们了解了什么是进程以及如何实现进程调度,那么今天我将为大家分享关于线程相关的知识。在学习线程之前,我们认为进程是操作系统执行独立执行的单位,但其实并不然。线程是操作系统中能够独立执行的最小单元。只有掌握了什么是线程,我们才能实现后面的并

    2024年02月09日
    浏览(9)
  • 测试不够快?试试这招!让pytest多进程/多线程执行测试用例,轻松提升测试效率!

    测试不够快?试试这招!让pytest多进程/多线程执行测试用例,轻松提升测试效率!

    目录 :导读 前言: 多进程执行用例之pytest-xdist pytest-xdist分布式测试的原理: pytest-xdist分布式测试的流程: 多线程执行用例之pytest-parallel 常用参数配置 pytest-parallel与pytest-xdist对比说明: 结语 大家好!我是不二。 相信很多测试工程师都会遇到一个问题,那就是测试用例的执

    2024年02月06日
    浏览(11)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包