TCP/IP 网络编程(十八)---Windows 平台下线程的使用

44 阅读10分钟

内核对象

内核对象(Kernel Object)是操作系统内核用于管理资源的一种数据结构。它们为系统提供了统一的接口,用于管理和同步进程、线程、内存、文件、设备等系统资源。内核对象是由操作系统内核维护的,用户空间的程序无法直接访问它们,而是通过系统调用与它们交互。

内核对象的创建、管理、销毁时机的决定等工作均由操作系统完成。

基于 Windows 的线程创建

(一)进程和线程的关系

需要弄清楚下面的问题:

*“程序开始运行后,调用 `main` 函数的主体是线程还是进程?”*

在程序开始运行时,调用 main 函数的是主线程,它是属于一个进程中的第一个线程。

  1. 进程的创建

    • 当运行一个程序时,操作系统首先创建一个新的进程。进程是程序运行的实例,负责分配资源,比如内存、文件描述符、以及 CPU 时间等。
  2. 主线程的创建

    • 当进程被创建时,操作系统会同时创建一个主线程(有时称为“初始线程”或“主线程”)。这个主线程负责开始执行程序的代码,并首先调用 main 函数。
    • 主线程和后续创建的线程共享同一个进程的资源(如内存空间和打开的文件)。
  3. 调用 main 函数

    • 主线程开始执行程序时,它会从 main 函数开始运行。也就是说,最早运行的代码是在主线程中执行的。
  4. 多线程

    • 如果程序中有多线程部分(比如使用 pthread_create() 创建线程),那么主线程在执行 main 函数时可以创建其他子线程。这些子线程与主线程共享同一个进程的资源空间。
    • 但是,即便程序最终创建了多个线程,最初调用 main 函数的仍然是主线程。

(二)Windows 中创建线程的方法

(1)CreateThread 函数

#include <windows.h>

// 成功时返回线程句柄,失败时返回 NULL
HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  // 线程的安全属性
  SIZE_T dwStackSize,                        // 初始的栈大小
  LPTHREAD_START_ROUTINE lpStartAddress,     // 线程函数的地址
  LPVOID lpParameter,                        // 传递给线程函数的参数
  DWORD dwCreationFlags,                     // 创建标志
  LPDWORD lpThreadId                         // 线程ID的指针
);

  • lpThreadAttributes(线程安全属性):

    • 指向一个 SECURITY_ATTRIBUTES 结构体。如果为 NULL,则线程将使用默认的安全属性。
    • 该结构体指定新线程的安全描述符和是否可以由子进程继承该线程的句柄。
  • dwStackSize(栈的大小):

    • 指定线程的栈大小(以字节为单位)。如果为 0,线程将使用默认的栈大小。
    • 不建议使用过大的栈,因为这会增加内存使用。
  • lpStartAddress(线程函数的地址):

    • 一个指向线程函数的指针,该线程函数将在线程创建后立即执行。该函数的原型必须为 DWORD WINAPI ThreadFunction(LPVOID lpParameter)
    • 线程函数完成后,线程将终止。
  • lpParameter(线程参数):

    • 传递给新线程的参数。可以是任何类型的数据。如果不需要传递参数,可以为 NULL
    • 该参数将作为线程函数的参数传递。
  • dwCreationFlags(创建标志):

    • 控制线程的创建方式。常用的标志有:

      • 0:立即运行线程。
      • CREATE_SUSPENDED:创建线程后保持挂起状态。需要调用 ResumeThread 来恢复线程执行。
  • lpThreadId(线程 ID):

    • 用于存储线程的 ID。可以为 NULL,如果不需要获取线程的 ID。

(2)_beginthreadex 函数

# include <process.h>

// 成功时返回线程句柄,失败时返回0
uintptr_t _beginthreadex(
    void *security,
    unsigned stack_size,
    unsigned (__stdcall *start_address)(void *),
    void *arglist,
    unsigned initflag,
    unsigned *thrdaddr
);

  • security:线程的安全属性。通常传递 NULL,表示默认安全属性。

  • stack_size:线程的栈大小。如果为 0,则使用默认的栈大小。

  • start_address:线程的入口函数,必须符合 unsigned (__stdcall *)(void *) 类型。

  • arglist:传递给新线程的参数,可以是任何类型的数据。

  • initflag:线程创建标志。如果为 0,线程会立即运行;如果为 CREATE_SUSPENDED,线程会被创建但处于挂起状态,必须调用 ResumeThread 以启动它。

  • thrdaddr:返回创建的线程的线程 ID。

(3)两个函数的比较

_beginthreadexCreateThread 都是用于在 Windows 中创建新线程的函数,但它们的适用场景和使用方式略有不同。_beginthreadex 主要是为了支持多线程应用中的 C/C++ 标准库调用,而 CreateThread 是原生的 Windows API,用于一般的线程创建。

  • _beginthreadex 是一个更高级的函数,用于支持 C 运行时库,并且应该用于多线程 C/C++ 程序。

  • CreateThread 是 Windows 的底层 API,不处理 C 运行时库的初始化与清理。如果你在线程中使用了标准库函数,请避免直接使用 CreateThread

(4)_beginthreadex 函数创建线程示例

thread1_win.c:

#include <stdio.h>
#include <windows.h>
#include <process.h>    /* _beginthreadex, _endthreadex */

unsigned WINAPI ThreadFunc(void* arg);

int main(int argc, char* argv[])
{
	HANDLE hThread;
	unsigned threadID;
	int param = 5;

	hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)&param, 0, &threadID);
	if (hThread == 0)
	{
		puts("_beginthreadex() error");
		return -1;
	}
	Sleep(3000);
	puts("end of main");
	
	system("pause");
	return 0;
}


unsigned WINAPI ThreadFunc(void* arg)
{
	int i;
	int cnt = *((int*)arg);
	for (i = 0; i < cnt; i++)
	{
		Sleep(1000);  puts("running thread");
	}
	return 0;
}
  • _beginthreadex 函数

    • 该函数用来创建一个新的线程,和 CreateThread 类似,但它更适合使用 C 运行时库的多线程应用程序,因为它能够确保在使用 C 运行时库时正确处理初始化和清理工作。
  • ThreadFunc 函数

    • 这是线程执行的函数。在本例中,线程将从参数 arg 中获取一个整数值,并在循环中运行指定次数(param 的值为 5)。
  • 线程创建与主线程的执行

    • 通过 _beginthreadex 创建了一个新线程来执行 ThreadFunc,同时主线程会暂停 3 秒,然后输出 "end of main"
  • 运行结果

image.png
  • 运行结果分析
    • running thread 输出

      • 主线程在 Sleep(3000) 之前创建了一个子线程 (ThreadFunc)。
      • 子线程在每次迭代中 Sleep(1000) 秒,然后输出 "running thread"
      • 由于主线程睡眠了 3 秒,子线程在这段时间内会打印 "running thread"
    • end of main 输出

      • 主线程在 3 秒后输出 "end of main"
    • 子线程继续执行

      • 主线程在 3 秒的 Sleep 结束后,输出 "end of main" 并调用 system("pause")
      • 子线程仍在执行,打印剩余的 "running thread",因为主线程结束并不会立即终止子线程。
    • 进程退出

      • 在用户按下任意键后,system("pause") 返回,主线程返回,进程开始终止过程。
      • 操作系统会确保所有线程(包括子线程)完成被强制终止,然后才彻底退出进程。

(5)线程 ID 与线程句柄的区别

区别点线程ID (Thread ID)线程句柄 (Thread Handle)
定义系统为每个线程分配的唯一标识符操作系统返回的内核对象,用于管理和控制线程
作用用于区分线程用于管理线程的生命周期、操作线程
范围全局唯一(系统范围内唯一)在当前进程内有效(跨进程不能直接使用)
获取方法由系统自动分配,或通过GetCurrentThreadId 获取CreateThread_beginthreadex 返回
管理操作不能直接操作线程可以通过句柄管理线程,如等待、终止等操作
销毁方式无需显式销毁必须调用 CloseHandle 关闭句柄以释放资源

内核对象的两种状态及查看

(一)内核对象的两种状态

资源类型不同,内核对象也含有不同信息。内核对象的状态可以被分为两种主要状态:信号状态非信号状态

例如,线程内核对象需要重点关注线程是否已终止,所以终止状态又称 “signaled 状态”,未终止状态称为 “non-signaled 状态”。

进程或线程何时终止?操作系统将这些重要信息保存到内核对象,同时给出如下约定:

*“进程或线程终止时,操作系统会把相应的内核对象改为 signaled 状态。”*

也就是说,进程和线程的内核对象初始状态是 non-signaled 状态。

(二)内核对象的状态查看

(1) WaitForSingleObject 函数

#include <windows.h>

DWORD WaitForSingleObject(
  HANDLE hHandle,      // 内核对象的句柄
  DWORD dwMilliseconds // 超时时间,单位是毫秒
);
  • hHandle:指定要等待的内核对象的句柄。这个句柄可以是事件、互斥量、信号量、计时器等。

  • dwMilliseconds:指定等待的超时时间,以毫秒为单位。可以使用以下特殊值:

    • INFINITE:无限等待,直到对象变为信号状态。
    • 0:立即返回,不等待,检查对象当前是否处于信号状态。
  • 返回值

    • WAIT_OBJECT_0:表示内核对象变为信号状态,函数成功返回。
    • WAIT_TIMEOUT:表示等待超时,内核对象在指定的时间内未变为信号状态。
    • WAIT_ABANDONED:表示等待的对象是一个被丢弃的互斥量(在多线程环境中常见)。
    • WAIT_FAILED:表示函数调用失败,可以通过 GetLastError 获取更多错误信息。

(2)WaitForMultipleObjects 函数

DWORD WaitForMultipleObjects(
  DWORD nCount,                  // 对象的数量
  const HANDLE *lpHandles,       // 内核对象句柄数组
  BOOL bWaitAll,                 // 是否等待所有对象变为信号状态
  DWORD dwMilliseconds           // 超时时间,单位是毫秒
);
  • nCount:指定要等待的内核对象的数量。该值必须小于或等于 MAXIMUM_WAIT_OBJECTS(通常为 64)。

  • lpHandles:指向一个 HANDLE 数组的指针,每个句柄代表一个内核对象。线程将等待这些对象变为信号状态。

  • bWaitAll:指定是否等待所有对象都变为信号状态。

    • TRUE:等待所有指定的内核对象变为信号状态。
    • FALSE:等待其中任何一个内核对象变为信号状态。
  • dwMilliseconds:指定等待的超时时间,以毫秒为单位。可以使用以下特殊值:

    • INFINITE:无限等待,直到至少一个对象变为信号状态(或所有对象,如果 bWaitAllTRUE)。
    • 0:立即返回,不等待,检查对象当前是否处于信号状态。

(3)示例1

thread2_win.c

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

unsigned WINAPI ThreadFunc(void *arg);

int main(int argc, char *argv[]) 
{
	HANDLE hThread;
	DWORD wr;
	unsigned threadID;
	int param=5;

	hThread=(HANDLE)_beginthreadex(NULL, 0, ThreadFunc, (void*)&param, 0, &threadID);
	if(hThread==0)
	{
		puts("_beginthreadex() error");
		return -1;
	}

	if((wr=WaitForSingleObject(hThread, INFINITE))==WAIT_FAILED)
	{
		puts("thread wait error");
		return -1;
	}

	printf("wait result: %s \n", (wr==WAIT_OBJECT_0) ? "signaled":"time-out");

	puts("end of main");
	return 0;
}


unsigned WINAPI ThreadFunc(void *arg)
{
	int i;
	int cnt=*((int*)arg);
	for(i=0; i<cnt; i++)
	{
		Sleep(1000);  puts("running thread");	 
	}
	return 0;
}

运行结果:

image.png

WaitForSingleObject 确保主线程在子线程完成之前不会继续执行。

(4)示例2

thread3_win.c

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

#define NUM_THREAD	50
unsigned WINAPI threadInc(void * arg);
unsigned WINAPI threadDes(void * arg);
long long num=0;

int main(int argc, char *argv[]) 
{
   HANDLE tHandles[NUM_THREAD];
   int i;

   printf("sizeof long long: %d \n", sizeof(long long));
   for(i=0; i<NUM_THREAD; i++)
   {
   	if(i%2)
   	    tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
   	else
   	    tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
   }

   WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
   printf("result: %lld \n", num);
   return 0;
}

unsigned WINAPI threadInc(void * arg) 
{
   int i;
   for(i=0; i<50000000; i++)
   	num+=1;
   return 0;
}
unsigned WINAPI threadDes(void * arg)
{
   int i;
   for(i=0; i<50000000; i++)
   	num-=1;
   return 0;
}

运行结果:

image.png

即使运行多次也无法得到正确结果,而且每次结果都不同,可以用线程同步解决此问题,解决方法在下一章给出。