问题引入:
当程序需要执行多个逻辑时,如何设计代码?以游戏为例,一个游戏中,要设计人物能过够并行的同时完成两个操作,行走和砍怪。
解决思路:
启用两片内存,内存1与内存2共享同一片物理内存中的代码,由cpu为每片内存分配时间片,依次按顺序执行两片内存的操作,模拟一个同时进行的现象。即为轻量级进程的概念。
线程:
线程的本质就是轻量级线程,但是不共享栈。Windows以线程为执行代码的基本单位,进程为管理资源的基本单位。
-
主线程的作用
在大多数编程语言和操作系统中,通常只有主线程(也称为主执行线程)可以创建新的线程。这是因为线程的创建通常需要在操作系统级别进行资源分配和管理,而主线程是程序启动时自动创建的,并且通常具有特殊的权限来创建和管理其他线程。
在某些特定的编程环境中,可能会有例外情况,允许非主线程创建新的线程,但这种情况相对较少见,而且可能会受到严格的限制或者需要特殊的权限。
一些基本概念:
三个主要状态:运行状态(消耗时间片,正在执行),等待状态(已分配时间片,等待被执行),挂起状态(没有分配时间片,不等待),Sleep函数就是让当前线程挂起一段时间,挂起时间为输入的参数。
优先级:32个优先级
动态优先级调度算法(抢占式):
32个优先级则有32个链表,CPU一直执行优先级最高那个链表。当低优先级等待时间过长时,会自动提升到高优先级的链表。当高优先级链表插入对象时,当前低优先级的线程会停止,直接执行高优先级的线程,称为抢占式。当线程被抢占后,他都时间片会通过期待算法补全。
Windows提供API设置线程的优先级,但是只能设置4级
消息队列和窗口都是以线程为单位,一个线程维护一个消息队列。因此在需要用窗口程序进行死循环时,可用多个线程来回切换时间片实现
线程切换:
在操作系统中,线程切换是通过上下文切换来实现的。当操作系统决定要执行另一个线程时,它会执行以下步骤来进行线程切换:
- 保存当前线程的上下文(Context Switch):当前执行线程的状态(包括寄存器的值、程序计数器、堆栈指针等)被保存到内存中的一个数据结构中,以便稍后重新恢复执行。
- 加载下一个线程的上下文:操作系统从调度队列中选择下一个要执行的线程,并将其之前保存的上下文加载到 CPU 的寄存器中。
- 更新调度信息:操作系统更新线程的调度信息,例如更新其运行时间、状态等。
- 切换堆栈(可选):在某些情况下,操作系统可能需要切换线程的堆栈。这可能涉及将当前线程的堆栈指针切换到下一个线程的堆栈,以确保在执行新线程时使用正确的堆栈。
- 恢复执行:最后,CPU 开始执行新线程,从其保存的状态继续执行。
这些步骤需要涉及对硬件(如 CPU 寄存器)和内核数据结构的访问,因此需要操作系统内核的支持。同时,上下文切换是一种开销较大的操作,因为它涉及将数据从寄存器保存到内存,这会导致一定的性能损失。
代码演示:
以下是一个使用Windows API创建线程的简单示例代码:
#include <windows.h>
#include <iostream>
using namespace std;
// 线程函数
DWORD WINAPI ThreadFunction(LPVOID lpParam)
{
int threadID = *(int*)lpParam; // 从参数中获取线程ID
cout << "Thread " << threadID << " is running." << endl;
return threadID; //线程退出码
}
int main()
{
const int numThreads = 5;
HANDLE threads[numThreads];
DWORD threadIds[numThreads];
DWORD dwExitCode[numThreads];
// 创建多个线程
for (int i = 0; i < numThreads; ++i)
{
threads[i] = CreateThread(NULL, 0, ThreadFunction, &i, 0, &threadIds[i]);
if (threads[i] == NULL)
{
cerr << "Failed to create thread." << endl;
return 1;
}
}
// 等待所有线程结束,否则可能会出现主线程已经结束,此时主线程被挂起不分配时间片
//子线程还没跑完的情况
//(主线程跑完了。进程就结束了,资源也清空了,子线程被强制结束)
WaitForMultipleObjects(numThreads, threads, TRUE, INFINITE);
//WaitForSingleObject 针对单个对象
//WaitForSingleObject(hThread, INFINITE);
// 关闭线程句柄
for (int i = 0; i < numThreads; ++i)
{
//获取线程退出码
GetExitCodeThread(threads[i], &dwExitCode[i]);
printf("main thread exit:%d\\n", dwExitCode[i]);
CloseHandle(threads[i]);
}
return 0;
}
这个示例代码创建了5个线程,每个线程打印其ID并返回。主函数等待所有线程结束后关闭线程句柄。
-
CreateThread参数表
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, __drv_aliasesMem LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );
LPSECURITY_ATTRIBUTES lpThreadAttributes
:- 这是一个指向SECURITY_ATTRIBUTES结构的指针,用于指定新线程的安全描述符。如果不需要使用安全描述符,可以将此参数设置为NULL。
SIZE_T dwStackSize
:- 这是新线程的初始堆栈大小,以字节为单位。如果设置为0,则使用默认堆栈大小,通常为1MB。
LPTHREAD_START_ROUTINE lpStartAddress
:- 这是指向线程函数的指针,即线程在启动时执行的函数。此函数的原型应该是DWORD WINAPI,接受一个LPVOID类型的参数并返回一个DWORD类型的值。
LPVOID lpParameter
:- 这是传递给线程函数的参数,它可以是任何类型的指针。如果不需要传递参数,则可以将其设置为NULL。
DWORD dwCreationFlags
:- 这是用于控制线程的创建方式的标志。常见的标志包括:
- 0:默认标志,表示创建线程后立即运行。
- CREATE_SUSPENDED:创建线程后暂停执行,直到调用ResumeThread函数。
- STACK_SIZE_PARAM_IS_A_RESERVATION:指示dwStackSize参数指定的值是堆栈的保留大小,而不是提交大小。
- 这是用于控制线程的创建方式的标志。常见的标志包括:
LPDWORD lpThreadId
:- 这是一个指向DWORD变量的指针,用于接收新线程的ID。如果不需要获取线程ID,则可以将其设置为NULL。
-
概念拓展
超线程技术(Hyper-Threading Technology)是Intel推出的一项CPU技术,旨在提高处理器的性能和效率。超线程技术通过在单个物理处理器核心上模拟多个逻辑处理器核心来实现。这样,每个物理处理器核心就能够同时执行多个线程,从而提高CPU的利用率,提升系统的整体性能。
超线程技术的工作原理涉及到利用处理器资源的闲置周期。在传统的单线程处理中,当处理器执行某些指令时,可能会有一些处理器资源处于空闲状态,无法充分利用。超线程技术允许在这些空闲周期内执行另一个线程的指令,从而使得处理器的资源得到更充分的利用。
具体来说,超线程技术通过在物理处理器核心上实现两个或更多的逻辑处理器核心(称为线程),让每个逻辑核心都能够独立执行指令流。这样,在同一时钟周期内,处理器可以同时执行多个线程的指令,提高了处理器的并行度。超线程技术并不增加物理处理器核心的数量,而是在现有的硬件结构上进行优化(如:一个cpu上有两套寄存器)。
超线程技术对于那些具有大量线程且需要高度并行处理的任务来说,可以带来显著的性能提升。例如,多线程应用程序、服务器应用程序、科学计算和数据处理等领域都能够从超线程技术中受益。然而,在某些特定情况下,超线程技术可能会导致性能下降,例如某些单线程性能敏感的应用程序。
需要注意的是,超线程技术是Intel特有的技术,不同厂商的处理器架构可能采用不同的技术来实现类似的功能。AMD处理器采用的是称为“模块化多线程(Clustered Multi-Threading,CMT)”的技术,与超线程技术有一些区别。
线程操作:
注意:
SuspendThread和ResumeThread使用有引用计数
ExitThread可用于线程中多层次函数调用时的退出线程
一些Demo:
//以计算器为例,抢掉计算器的时间片
HWND hWnd = FindWindow(NULL, "计算器");
printf("hWnd:%d\\n", hWnd);
DWORD dwCalcId = GetWindowThreadProcessId(hWnd, NULL);
printf("dwCalcId:%d\\n", dwCalcId);
HANDLE hCalcThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwCalcId);
printf("hCalcThread:%d\\n", hCalcThread);
SuspendThread(hCalcThread);
//ResumeThread(hCalcThread); //恢复线程
//TerminateThread(hCalcThread, 0); //强制关闭线程
CloseHandle(hCalcThread);
system("pause");
return 0;
//挂目标线程,然后退出,使目标进程无法运行
//要恢复时再调用ResumeThread再运行一次即可
//可配合快照,全部挂起
//合理的限制进程跑的时间,并正确的退出
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
printf("ThreadProc begin\\n");
int num = 0;
while(g_isRunning) {
code();
}
printf("ThreadProc End\\n");
return num; //线程结束
}
int main()
{
//创建一个新的线程
DWORD dwTid = 0;
g_isRunning = true; //设置标志,一种重要的设计思想
HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwTid);
if (hThread == NULL)
{
printf("CreateThread Error:%d\\n", GetLastError());
return 0;
}
printf("hThread:%d dwTid:%d\\n", hThread, dwTid);
//设置线程运行时间为1s
Sleep(1000);
//自动退
g_isRunning = false;
WaitForSingleObject(hThread, INFINITE);
return 0;
}
MFC中的封装
当在 MFC 应用程序中需要创建线程时,有两种常用的方法:使用 CWinThread
类或 AfxBeginThread
函数。这两种方法都提供了创建和管理线程的功能,但具有不同的用法和特点。
-
CWinThread 类:
CWinThread
是 MFC 中表示线程的类,通过继承它可以实现自定义的线程类。- 使用
CWinThread
类需要派生自定义的线程类,并实现其中的 Run 方法来定义线程的主要逻辑。 CWinThread
类提供了一系列的方法和成员函数,用于线程的控制、同步和通信等操作。
-
AfxBeginThread 函数:
AfxBeginThread
是 MFC 中一个方便的函数,用于创建线程并返回一个指向CWinThread
对象的指针。- 可以直接调用
AfxBeginThread
函数来创建线程,而无需派生自定义的线程类。 AfxBeginThread
函数的参数包括线程函数指针、线程参数、线程优先级等,用于指定线程的属性和行为。
tips:
当使用创建的线程要向主线程的控件发送消息时,不要用SendMessage而是用PostMessage。因为当主线程也操作该控件时,两边的消息同时传给同一个控件,会造成访问冲突。(PostMessage不会调用过程函数)
💬使用多线程,将软件功能分工合作,高效 💬