3 Windows多线程

发布时间:2024年01月09日

1 线程基础

在这里插入图片描述

1.1 进程是什么?

  1. 狭义上讲,进程是一个正在执行程序的实例。
  2. 规范点说,是处于执行期的程序以及它所管理的资源(如打开的文件、挂起的信号、进程状态、地址空间等等)的总称。
  3. 从操作系统核心角度来说,进程是操作系统调度除 CPU 时间片外进行的资源分配和保护的基本单位,它有一个独立的虚拟地址空间,用来容纳进程映像(如与进程关联的程序与数据),并以进程为单位对各种资源实施保护,如受保护地访问处理器、文件、外部设备及其他进程(进程间通信)

1.2 线程是什么?

  1. 线程是在进程中产生的一个执行单元。
  2. 是 CPU 调度和分配的最小单元。
  3. 其在同一个进程中与其他线程并行运行,他们可以共享进程内的资源,比如内存、地址空间、打开的文件等等。

在这里插入图片描述

1.3 线程创建函数

// 创建在调用进程的虚拟地址空间内执行的线程,Windows API提供的函数
HANDLE CreateThread(
  [in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,//线程内核对象的安全属性,一般传入 NULL 表示使用默认设置。
  [in]            SIZE_T                  dwStackSize,//线程栈空间大小。传入0表示使用默认大小 (1MB)
  [in]            LPTHREAD_START_ROUTINE  lpStartAddress,//新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。
  [in, optional]  __drv_aliasesMem LPVOID lpParameter,//传给线程函数的参数
  [in]            DWORD                   dwCreationFlags,//指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为 CREATE_SUSPENDED 则表 示线程创建后暂停运行,这样它就无法调度,直到调用 ResumeThread()
  [out, optional] LPDWORD                 lpThreadId//返回线程的ID 号,传入 NULL 表示不需要返回该线程ID号
);
//C运行时库提供的函数,使用了本地代码实现
uintptr_t _beginthreadex( // NATIVE CODE
   [in]  void *security,// 用于指定线程安全特性,通常为NULL(表示使用默认安全设置)。
   [in]  unsigned stack_size,// 用于指定线程的栈大小,通常为0(表示使用默认栈大小)。
   [in]  unsigned ( __stdcall *start_address )( void * ),// 指向线程函数的指针,这个函数会作为线程的入口点来执行。
   [in]  void *arglist,// 传递给线程函数的参数。
   [in]  unsigned initflag,// 用于指定线程的启动标志,通常为0(表示使用默认标志)。
   [out]  unsigned *thrdaddr// 接收用于返回新线程标识符的指针。
);
  • CreateThread 是一种微软在 Windows API中提供了建立新的线程的函数,该函数在主线程的基础上创建一个新线程。线程终止运行后,线程对象仍然在系统中,必须通过 CloseHandle 函数来关闭该线程对象

  • ?_beginthreadex?函数则更适合在使用C/C++运行时库的环境中创建线程。

1.4 简单多线程示例

1. 第一阶段:主线程和子线程的结束时间

main 函数返回后,整个进程终止,同时终止其包含的所有线程
代码如下:

#include <stdio.h>
#include <windows.h> 
#include <process.h>
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"); 
	return 0;
}
unsigned WINAPI ThreadFunc(void* arg) 
{ 
	int i;
	int cnt = *((int*)arg);// 将函数参数类型转换回int *;再取一下该指针指向的内容
	for (i = 0; i < cnt; i++) {
		Sleep(1000);
		puts("running thread");
	}
	return 0;
}

结果:线程函数里是每隔一秒钟打印一次"running thread",但是却只打印2次就结束了,这是因为主线程这边已经等了3s,执行到return 0了导致整个进程结束。
在这里插入图片描述

2. 第二阶段WaitForSingleObject 来等待一个内核对象变为已通知状态

?WaitForSingleObject?函数可以通过操作系统内部的机制接收到线程结束的信号,并相应地处理等待结果。
需要注意的是,?WaitForSingleObject?函数只能等待一个对象的状态对齐。如果需要等待多个对象,可以考虑使用 ?WaitForMultipleObjects?函数。

// 等待指定的对象处于信号状态或超时间隔已过,用于等待各种内核对象:线程、进程、事件、互斥体
DWORD WaitForSingleObject(
  [in] HANDLE hHandle,// 对象的句柄
  [in] DWORD  dwMilliseconds// 超时间隔(以毫秒为单位)。
);

代码如下

unsigned int __stdcall ThreadFun(LPVOID p)
{ 
	int cnt = *((int*)p); 
	for (int i = 0; i < cnt; i++)
	{ 
		Sleep(1000);
		puts("running thread"); 
	}
	return 0;
}
int main() 
{
	printf("main begin\n"); 
	int iParam = 5; 
	unsigned int dwThreadID; 
	DWORD wr; 
	HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, (void*)&iParam, 0, &dwThreadID);
	if (hThread == NULL) 
	{
		puts("_beginthreadex() error"); 
		return -1;
	}
	printf("WaitForSingleObject begin\n"); 
	// WaitForSingleObject?函数在等待线程?hThread?的结束时,使用INFINITE?作为等待时间,表示无限期等待。
	if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED)
	{ 
		puts("thread wait error"); 
		return -1;
	}
	printf("WaitForSingleObject end\n");
	printf("main end\n");
	system("pause");
	return 0; 
}

结果:等待子线程完整的打印了5次以后,结束子线程。然后主线程才继续执行后面的代码
在这里插入图片描述 3. 第二阶段起两个线程,一个加+1,一个减 1

DWORD WaitForMultipleObjects(
  [in] DWORD        nCount,//lpHandles 指向的数组中的对象句柄数。不可为0
  [in] const HANDLE *lpHandles,// 对象句柄的数组。
  //如果此参数为 TRUE,则当发出 lpHandles 数组中**所有对象的状态信号**时,函数将返回 。 
  //如果 为 FALSE,则当任一对象的状态设置为“已发出信号”时,函数将返回。在后一种情况下,返回值指示其状态导致函数返回的对象。
  [in] BOOL         bWaitAll,
  [in] DWORD        dwMilliseconds//超时间隔(以毫秒为单位)。
);

代码如下:

#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)); 

	// 奇数线程++;偶数线程--;每个线程执行循环500000次
	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;
}
// num++
unsigned WINAPI threadInc(void* arg) 
{
	int i;
	for (i = 0; i < 500000; i++)
		num += 1;
		return 0;
}
// num--
unsigned WINAPI threadDes(void* arg)
{ 
	int i; 
	for (i = 0; i < 500000; i++)
		num -= 1;
	return 0;
}

结果: 我们可以看到每一次num的结果并不相同,这是因为没有对 num?进行同步,不同线程之间可能会出现竞争条件,导致最终的结果不确定。
在这里插入图片描述
在这里插入图片描述

2 Windows内核对象与句柄

2.1 内核对象

  • Windows 中每个内核对象都只是一个内存块,它由操作系统内核分配,并只能由操作系统内核进行访问,应用程序不能在内存中定位这些数据结构并直接更改其内容。这个内存块是一个数据结构,其成员维护着与对象相关的信息。少数成员(安全描述符和使用计数)是所有内核对象都有的,但大多数成员都是不同类型对象特有的。
  • 常用的内核对象比如file文件对象、event 事件对象、process进程、thread线程、iocompletationport 完成端口(windows 服务器)、mailslot 邮槽、mutex 互斥量和 registry 注册表等

2.2 内核对象的使用计数与生命期

  • 内核对象的所有者是操作系统内核,而非进程。换言之也就是说当进程退出,内核对象不一定会销毁
  • 操作系统内核通过内核对象的使用计数,知道当前有多少个进程正在使用一个特定的内核对象。初次创建内核对象,使用计数为1。当另一个进程获得该内核对象的访问权之后,使用计数加 1。如果内核对象的使用计数递减为 0,操作系统内核就会销毁该内核对象。
  • 也就是说内核对象在当前进程中创建,但是当前进程退出时,内核对象有可能被另外一个进程访问。这时,进程退出只
    会减少当前进程对引用的所有内核对象的使用计数,而不会减少其他进程对内核对象的使用计数(即使该内核对象由当前进程创建)。那么内核对象的使用计数未递减为 0,操作系统内核不会销 毁该内核对象。
    在这里插入图片描述在这里插入图片描述

2.3 操作内核对象

Windows 提供了一组函数进行操作内核对象。成功调用一个创建内核对象的函数后,会返回一个句柄,它表示了所创建的内核对象,可由进程中的任何线程使用。在 32 位进程中,句柄是一个 32 位值,在 64 位进程中句柄是一个 64 位值。我们可以使用唯一标识内核对象的句柄,调用内核操作函数对内核对象进行操作。

2.4 内核对象与其他类型的对象

Windows 进程中除了内核对象还有其他类型的对象,比如窗口,菜单,字体等,这些属于用户对 象和 GDI 对象。要区分内核对象与非内核对象,最简单的方式就是查看创建这个对象的函数,几乎所有创建内核对象的函数都有一个允许我们指定安全属性的参数

2.5 注意

  • 一个对象是不是内核对象,通常可以看创建此对象 API 的参数中是否需要: PSECURITY_ATTRIBUTES 类型的参数。

  • 内核对象只是一个内存块,这块内存位于操作系统内核的地址空间,内存块中存放一个数据结构(此数据结构的成员有如:安全描述符、使用计数等)。

  • 每个进程中有一个句柄表(handle table),这个句柄表仅供内核对象使用,如下图: 在这里插入图片描述 4. 调用
    (1) hThread = CreateThread(... , &threadId);当调用了 CreateThread 、CreateFile 等创建内核对象的函数后, 就是相当于操作系统多了一个内存块,这个内存块就是内核对象 也是此时内核对象被创建,其数据结构中的引用计数初始为 1(这样理解:只要内 核对象被创建,其引用计数被初始化为1),这里实则发生两件事:创建了一个内核对象和创建线程的函数打开(访问)了此对象,所以内核对象的引用计数加 1, 这时引用计数就为 2 了。
    (2)当调用 CloseHandle(hThread); 时发生这样的事情:

    • 系统通过 hThread 计算出此句柄在句柄表中的索引

    • 然后把那一项处理后标注为空闲可用的项,内核对象的引用计数减 1 即此时此内核对象的引用计数为 1。

    • 之后这个线程句柄与创建时产生的内核对象已经没有任何关系了。不能通过 hThread 句柄去访问内核对象了 只有当内核对象的引用计数为 0 时,内核对象才会被销毁,而此时它的引用计数 为 1,那它什么时候会被销毁?

    • 当此线程结束的时候,它的引用计数再减 1 即为 0,内核对象被销毁。此时又有一 个新问题产生:我们已经关闭了线程句柄,也就是这个线程句柄已经和内核对象没 有瓜葛了,那么那个内核对象是怎么又可以和此线程联系起来了呢?

    • 其实是创建 线程时产生的那个线程 ID,代码如下:

DWORD WINAPI ThreadProc(LPVOID lpParameter) 
{ 
	printf("I am comming..."); 
	return 0;
}
int main() {
	HANDLE hThread;
	HANDLE headle2;
	DWORD threadId;
	hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &threadId); 
	CloseHandle(hThread); // 关闭了线程句柄
	// 打开先用的线程对象
	headle2 = OpenThread(THREAD_QUERY_INFORMATION, FALSE, threadId);
	system("pause");
	return 0;
}

其中对于openThread

// 打开现有线程对象
HANDLE OpenThread(
  [in] DWORD dwDesiredAccess,// 对线程对象的访问
  [in] BOOL  bInheritHandle,// 如果此值为 TRUE,则此进程创建的进程将继承句柄。 否则,进程不会继承此句柄。
  [in] DWORD dwThreadId// 要打开的线程的标识符。
);
OpenThread 返回的句柄可用于需要线程句柄的任何函数(例如 wait 函数),前提是你请求了适当的访问权限。

3 线程同步

3.1 互斥对象

  1. 互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。
  2. 互斥对象包含一个使用数量,一个线程 ID 和一个计数器。其中线程 ID 用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。
    a. 创建互斥对象:调用函数 CreateMutex。调用成功,该函数返回所创建的互斥 对象的句柄。
    b. 请求互斥对象所有权:调用函数 WaitForSingleObject 函数。
  3. 线程必须主动请求共享对象的所有权才能获得所有权。
    a. 释放指定互斥对象的所有权:调用 ReleaseMutex 函数。线程访问共享资源结束后,线程要主动释放对互斥对象的所有权,使该对象处于已通知状态。
3.1.2 相关函数
// 1 创建或打开命名的或未命名的互斥体对象。
HANDLE CreateMutexA(
  //指向 SECURITY_ATTRIBUTES 结构的指针。 如果此参数为 NULL,则子进程无法继承句柄。
  [in, optional] LPSECURITY_ATTRIBUTES lpMutexAttributes,
  //如果此值为TRUE并且调用方创建了互斥体,则调用线程获取互斥对象的初始所有权。 否则,调用线程不会获得互斥体的所有权。 
  [in]           BOOL                  bInitialOwner,
  //互斥对象的名称。 名称限制为 MAX_PATH 个字符。
  [in, optional] LPCSTR                lpName
);
// 2 等待指定的对象处于信号状态或超时间隔已过。
DWORD WaitForSingleObject(
  [in] HANDLE hHandle,
  [in] DWORD  dwMilliseconds
);
// 3 释放指定互斥对象的所有权
BOOL ReleaseMutex(
  [in] HANDLE hMutex
);
3.1.3 实例
#include <stdio.h>
#include <windows.h>
#include <process.h>

#define NUM_THREAD 10 
// 定义线程数量

unsigned WINAPI threadInc(void* arg);
// 线程函数:对全局变量进行加1操作

unsigned WINAPI threadDes(void* arg);
// 线程函数:对全局变量进行减1操作

long long num = 0;
// 全局变量

HANDLE hMutex;
// 互斥锁句柄

int main(int argc, char* argv[]) {
    HANDLE tHandles[NUM_THREAD];
    int i;
	//1 创建互斥锁
    hMutex = CreateMutex(NULL, FALSE, NULL);
    
    for (i = 0; i < NUM_THREAD; i++) {
        if (i % 2) {
            tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
            // 创建偶数线程,对num进行加1操作
        }
        else {
            tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
            // 创建奇数线程,对num进行减1操作
        }
    }

    WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
    // 等待所有线程执行完毕

    CloseHandle(hMutex);
    // 关闭互斥锁句柄

    printf("result:%lld\n", num);
    // 打印最终结果

    return 0;
}

unsigned WINAPI threadInc(void* arg) {
    int i;
	//2 请求互斥锁
    WaitForSingleObject(hMutex, INFINITE);
    printf("这是threadInc()\n");
    for (i = 0; i < 5; i++) {
        num += 1;
        printf("num: %d\n", num);
        // 对全局变量进行加1操作,并打印循环变量i的值
    }
    printf("threadInc()调用结束\n");
     //3 释放互斥锁
    ReleaseMutex(hMutex);
    return 0;
}

unsigned WINAPI threadDes(void* arg) {
    int i;
	//2 请求互斥锁
    WaitForSingleObject(hMutex, INFINITE);
    printf("这是threadDes()\n");
    for (i = 0; i < 5; i++) {
        num -= 1;
        printf("num: %d\n", num);
        // 对全局变量进行减1操作,并打印循环变量i的值
    }
    printf("threadDes()调用结束\n");
    //3 释放互斥锁
    ReleaseMutex(hMutex);
    return 0;
}

在这里插入图片描述

3.2 事件对象

  1. 事件对象也属于内核对象,它包含以下三个成员:
    ● 使用计数;
    ●用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值;
    ● 用于指明该事件处于已通知状态还是未通知状态的布尔值
  2. 事件对象有两种类型:人工重置的事件对象自动重置的事件对象。这两种事件对象的区别 在于当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程
  3. 具体过程:
    1. 创建事件对象 调用 CreateEvent 函数创建或打开一个命名的或匿名的事件对象
    2. 设置事件对象状态 调用 SetEvent 函数把指定的事件对象设置为有信号状态。
    3. 重置事件对象状态 调用 ResetEvent 函数把指定的事件对象设置为无信号状态
    4. 请求事件对象 线程通过调用 WaitForSingleObject 函数请求事件对象。
  4. CreateEvent函数
// 1 创建事件对象
HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes,
  //决定事件对象的自动重置或手动重置。如果为 TRUE,则事件对象会保持触发状态,直到对应的 ?ResetEvent? 函数被调用将其重置;
  // 如果为 FALSE,则事件对象会在有一个等待线程被通知后自动重置为非触发状态。
  BOOL                  bManualReset,
  // 指定事件对象的初始状态。如果为 TRUE,则事件对象初始为触发状态;如果为 FALSE,则事件对象初始为非触发状态。
  BOOL                  bInitialState,
  // 事件对象的名称,在系统中必须唯一。可以为 NULL。
  LPCTSTR               lpName
);
// 2 用于将事件对象(Event Object)设置为触发状态
BOOL SetEvent(
  HANDLE hEvent
);
// 3 用于重置事件对象(Event Object)的触发状态
BOOL ResetEvent(
  HANDLE hEvent
);

5.实例

#define STR_LEN 100 
unsigned WINAPI NumberOfA(void* arg); 
unsigned WINAPI NumberOfOthers(void* arg); 
static char str[STR_LEN];
static HANDLE hEvent;
int main(int argc, char* argv[]) {
    HANDLE hThread1, hThread2; 
    fputs("Input string: ", stdout); 
    fgets(str, STR_LEN, stdin);
    //NUll 默认的安全符 手动 FALSE 初始状态为无信号状态 
    hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); 
    hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
    hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);
    WaitForSingleObject(hThread1, INFINITE);
    WaitForSingleObject(hThread2, INFINITE);
    //直到2个线程执行完之后,再把事件设置为无信号状态 
    ResetEvent(hEvent); 
    CloseHandle(hEvent);
    system("pause");
    return 0;
}
unsigned WINAPI NumberOfA(void* arg) {
    int i, cnt = 0; 
    WaitForSingleObject(hEvent, INFINITE);
    for (i = 0; str[i] != 0; i++) 
    { 
        if (str[i] == 'A') cnt++; 
    }
    printf("Num of A: %d \n", cnt);
    return 0;
}
unsigned WINAPI NumberOfOthers(void* arg)
{
    int i, cnt = 0;
    SetEvent(hEvent); 
    WaitForSingleObject(hEvent, INFINITE);
    for (i = 0; str[i] != 0; i++)
    { 
        if (str[i] != 'A') cnt++;
    }
    printf("Num of others: %d \n", cnt - 1); 
    //把事件对象设置为有信号状态
    SetEvent(hEvent); 
    return 0; 
}

3.3 信号量:用于控制多个并发进程或线程访问共享资源的同步机制

当一个进程或线程想要访问共享资源时,它必须首先获取信号量,如果信号量的计数器大于零,则减少计数器的值并允许访问共享资源。如果计数器的值为零,则进程或线程将被阻塞,直到有另一个进程或线程释放信号量并增加计数器的值。

  1. 信号量的组成
    1. 计数器:该内核对象被使用的次数
    2. 最大资源数量:标识信号量可以控制的最大资源数量(带符号的 32 位)
    3. 当前资源数量:标识当前可用资源的数量(带符号的 32 位)。即表示当前开放资源的个数(注意不是剩下资源的个数),只有开放的资源才能被线程所申请。但这些开放的资源不一定被线程占用完。比如,当前开放 5 个资源,而只有 3 个线程申请,则还有 2 个资源可被申请,但如果这时总共是 7 个线程要使用信 号量,显然开放的资源 5 个是不够的。这时还可以再开放 2 个,直到达到最大资 源数量
  2. 信号量的规则
    1. 如果当前资源计数大于 0,那么信号量处于触发状态(有信号状态),表示有可用资源。
    2. 如果当前资源计数等于 0,那么信号量属于未触发状态(无信号状态),表示没有可用资源。
    3. 系统绝对不会让当前资源计数变为负数
    4. 当前资源计数绝对不会大于最大资源计数
      在这里插入图片描述
  3. 信号量与互斥量不同的地方是:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源
  4. CreateSemaphore
// 1创建信号量
HANDLE CreateSemaphore(
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
  LONG                  lInitialCount,//信号量对象的初始计数值,即可用资源的初始数量
  LONG                  lMaximumCount,//信号量对象的最大计数值,即可用资源的最大数量。
  LPCTSTR               lpName// 信号量对象的名称,在系统中必须唯一。可以为 NULL
);
// 2 增加信号量
BOOL ReleaseSemaphore(
  HANDLE hSemaphore,// 要释放资源的信号量对象的句柄。
  LONG   lReleaseCount,// 要释放的资源数量。每次调用 ?ReleaseSemaphore?,信号量对象的计数值将会增加 ?lReleaseCount?。
  LPLONG lpPreviousCount//用于获取调用 ?ReleaseSemaphore? 前的信号量计数值。可以为 NULL。
);
  1. 实例
unsigned WINAPI Read(void* arg);
unsigned WINAPI Accu(void* arg);
static HANDLE semOne; 
static HANDLE semTwo;
static int num;
int main(int argc, char* argv[]) {
    HANDLE hThread1, hThread2; 
    semOne = CreateSemaphore(NULL, 0, 1, NULL); //semOne 没有可用资源 只能表示0或者1的二进制信号量 无信号 
    semTwo = CreateSemaphore(NULL, 1, 1, NULL); //semTwo 有可用资源,有信号状态 有信号 
    hThread1 = (HANDLE)_beginthreadex(NULL, 0, Read, NULL, 0, NULL);
    hThread2 = (HANDLE)_beginthreadex(NULL, 0, Accu, NULL, 0, NULL);
    WaitForSingleObject(hThread1, INFINITE); 
    WaitForSingleObject(hThread2, INFINITE);
    CloseHandle(semOne);
    CloseHandle(semTwo);
     system("pause");
    return 0;
}
unsigned WINAPI Read(void* arg)
{
    int i;
    for (i = 0; i < 5; i++) {
        fputs("Input num: ", stdout); // 1 5 11 
        printf("begin read\n"); // 3 6 12 //等待内核对象semTwo的信号,如果有信号,继续执行;如果没有信号,等待 
        WaitForSingleObject(semTwo, INFINITE);
        printf("beginning read\n"); //4 10 16 
        scanf("%d", &num); 
        ReleaseSemaphore(semOne, 1, NULL); 
    }return 0;
}
unsigned WINAPI Accu(void* arg)
{ 
    int sum = 0, i; 
    for (i = 0; i < 5; i++) 
    { 
        printf("begin Accu\n"); //2 9 15 //等待内核对象semOne的信号,如果有信号,继续执行;如果没有信号,等待
        WaitForSingleObject(semOne, INFINITE); 
        printf("beginning Accu\n"); //7 13 
        sum += num; 
        printf("sum = %d \n", sum); // 8 14 
        ReleaseSemaphore(semTwo, 1, NULL); 
    }
    printf("Result: %d \n", sum);
    return 0;
}

在这里插入图片描述

3.4 关键代码段

关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。通常把多线程中访问同一种资源的那部分代码当做关键代码段。

  1. 初始化关键代码段:调用 InitializeCriticalSection 函数初始化一个关键代码段
  2. 进入关键代码段:调用 EnterCriticalSection 函数,以获得指定的临界区对象的所有权,该函数等待指定的临界 区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;否则该函数会一直等待,从而 导致线程等待。
  3. 退出关键代码段:线程使用完临界区所保护的资源之后,需要调用 LeaveCriticalSection 函数,释放指定的临 界区对象的所有权。之后,其他想要获得该临界区对象所有权的线程就可以获得该所有权,从而进 入关键代码段,访问保护的资源。
  4. 删除临界区:当临界区不再需要时,可以调用 DeleteCriticalSection 函数释放该对象,该函数将释放一 个没有被任何线程所拥有的临界区对象的所有资源
  5. 相关函数
// 1 初始化一个新的临界区对象
InitializeCriticalSection(
 _Out_ LPCRITICAL_SECTION lpCriticalSection//初始化的临界区对象的指针
  );
// 2 尝试进入一个临界区对象来进行线程同步
VOID WINAPI EnterCriticalSection(
 _Inout_ LPCRITICAL_SECTION lpCriticalSection//要进入的临界区对象的指针。该参数必须是一个有效的地址,并且在调用此函数之前,对应的临界区对象必须已经成功初始化。
  );
 // 3 用于离开一个已进入的临界区对象
  VOID WINAPI LeaveCriticalSection( 
  _Inout_ LPCRITICAL_SECTION lpCriticalSection// 要离开的临界区对象的指针。该参数必须是一个有效的地址,并且在调用此函数之前,对应的临界区对象必须已经成功初始化。
  );
 // 4 删除临界区
  WINBASEAPI VOID WINAPI DeleteCriticalSection( 
  _Inout_ LPCRITICAL_SECTION lpCriticalSection//它是要删除的临界区对象的指针。该参数必须是一个有效的地址,并且在调用此函数之前,对应的临界区对象必须已经成功初始化。
  );
  1. 实例
int iTickets = 10; 
CRITICAL_SECTION g_cs; // A窗口 B窗口 
DWORD WINAPI SellTicketA(void* lpParam) 
{ 
    while (1) 
    { 
        EnterCriticalSection(&g_cs);//进入临界区 
        if (iTickets > 0) { 
            Sleep(1); 
            iTickets--;
            printf("A remain %d\n", iTickets);
            LeaveCriticalSection(&g_cs);//离开临界区 
        }else
        {
            LeaveCriticalSection(&g_cs);//离开临界区 
            break;
        }
    }
    return 0;
}
DWORD WINAPI SellTicketB(void* lpParam) {
    while (1) {
        EnterCriticalSection(&g_cs);//进入临界区 
        if (iTickets > 0) 
        { 
            Sleep(1);
            iTickets--; 
            printf("B remain %d\n", iTickets);
            LeaveCriticalSection(&g_cs);//离开临界区 
        }else {
            LeaveCriticalSection(&g_cs);//离开临界区 
            break; 
        } 
    }
    return 0; 
}
int main() 
{ 
    HANDLE hThreadA, hThreadB;
    InitializeCriticalSection(&g_cs); //初始化关键代码段 
    hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL); //2 
    hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL); //2 
    CloseHandle(hThreadA); //1 
    CloseHandle(hThreadB); //1 
    Sleep(20000); 
    DeleteCriticalSection(&g_cs);//删除临界区 
    system("pause"); 
    return 0; 
}

在这里插入图片描述

3.5 线程死锁

死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进

int iTickets = 10;
CRITICAL_SECTION g_csA; // A窗口 B窗口
CRITICAL_SECTION g_csB;
DWORD WINAPI SellTicketA(void* lpParam)
{
    while (1)
    {
        EnterCriticalSection(&g_csA);//进入临界区 
        Sleep(1);
        EnterCriticalSection(&g_csB);//进入临界区 
        if (iTickets > 0) {
            Sleep(1);
            iTickets--;
            printf("A remain %d\n", iTickets);
            LeaveCriticalSection(&g_csB);//离开临界区
            LeaveCriticalSection(&g_csA);
        }
        else
        {
            LeaveCriticalSection(&g_csB);//离开临界区
            LeaveCriticalSection(&g_csA);
            break;
        }
    }
    return 0;
}
DWORD WINAPI SellTicketB(void* lpParam) {
    while (1) {
        EnterCriticalSection(&g_csB);//进入临界区
        Sleep(1);
        EnterCriticalSection(&g_csA);//进入临界区        
        if (iTickets > 0)
        {
            Sleep(1);
            iTickets--;
            printf("B remain %d\n", iTickets);
            EnterCriticalSection(&g_csA);//进入临界区
            LeaveCriticalSection(&g_csB);//离开临界区 
        }
        else {
            LeaveCriticalSection(&g_csA);//离开临界区 
            LeaveCriticalSection(&g_csB);
            break;
        }
    }
    return 0;
}
int main()
{
    HANDLE hThreadA, hThreadB;
    InitializeCriticalSection(&g_csA); //初始化关键代码段
    InitializeCriticalSection(&g_csB);
    hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL); //2 
    hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL); //2 
    CloseHandle(hThreadA); //1 
    CloseHandle(hThreadB); //1 
    Sleep(1000);
    DeleteCriticalSection(&g_csA);
    DeleteCriticalSection(&g_csB);//删除临界区 
    system("pause");
    return 0;
}

在这里插入图片描述

3.6 线程同步方法总结

  1. windows线程同步的方式一共四种:互斥对象、事件对象、信号量、关键代码段。
  2. 互斥对象、事件对象、信号量属于内核对象,由于利用内核对象进行线程同步,所以速度较慢。但是这也使得可以在多进程中的各个线程中进行同步。
  3. 关键代码段在用户方式下,因此同步速度较快,但在使用关键代码段时,很容易进入死锁状态,这是因为等待进入关键代码段时无法设定超时值,而且只能实现在同进程中的线程同步(当一个线程尝试进入一个已被其他线程占用的关键代码段时,它将会被阻塞,直到获得访问权限为止。这种等待是无限期的,无法设置超时值来避免长时间的阻塞。因此,在一些场景中,如果线程在等待的时间过长,或者正在等待的线程出现问题导致无法释放关键代码段,可能会导致死锁。)

在这里插入图片描述
在这里插入图片描述

3.7 什么是线程安全

假如你的代码在多线程执行和单线程执行永远是完全一样的结果,那么你的代码是线程安全的。

4 多线程实现群聊的服务端和客户端

就是个多线程的群聊+互斥锁

4.1服务端

//多线程+socket编程的一个联合使用
//用互斥体进行线程同步 socket编程 临界区 全局变量 
#include <WinSock2.h>
#include <iostream>
#include <windows.h>
#include <process.h>
#pragma comment(lib, "ws2_32.lib")

#define MAX_CLNT 256
#define MAX_BUF_SIZE 256 
 //所有的连接的客户端socket 
SOCKET clntSocks[MAX_CLNT];
HANDLE hMutex;
int clntCnt = 0; //clntSocks[MAX_CLNT]里的变量

//当前连接的数目 
// 服务端的设计:
// 1 每来一个连接,服务端起一个线程(安排一个工人)维护
// 2 将收到的消息转发给所有的客户端 
// 3 某个连接断开,需要处理断开的连接
//发送给所有的客户端
void SendMsg(char *szMsg, int iLen)
{ 
	int i = 0;
	WaitForSingleObject(hMutex, INFINITE);
	for (i = 0; i < clntCnt; i++) 
	{
		send(clntSocks[i], szMsg, iLen, 0); 
	}
	ReleaseMutex(hMutex); 
}
	//子线程中处理客户端连接的函数
 unsigned WINAPI HandleCln(void* arg)
 { 
	 //1 接收传递过来的参数 
	 SOCKET hClntSock = *((SOCKET*)arg);
	 int iLen = 0, i;
	char szMsg[MAX_BUF_SIZE] = { 0 }; 
	//2 进行数据的收发 循环接收 
	//接收到客户端的数据
	while(1) 
	{
		iLen = recv(hClntSock, szMsg, sizeof(szMsg), 0); 
		if (iLen != -1) 
		{ 
			//收到的数据立马发给所有的客户端
			SendMsg(szMsg, iLen);
		}
		else 
		{	// ilen == -1 说明客户端断开
			break; 
		} 
	}
	printf("此时连接数目为 %d\n", clntCnt);
	//3 某个连接断开,需要处理断开的连接 遍历 
	WaitForSingleObject(hMutex, INFINITE);
	for (i = 0; i<clntCnt; i++) 
	{ 
		if (hClntSock == clntSocks[i]) { //移位 
			while (i++ < clntCnt) 
			{ 
				clntSocks[i] = clntSocks[i+1]; 
			}
			break;
		}
	}
	clntCnt--; //当前连接数的一个自减 
	printf("断开此时连接数目 %d", clntCnt); 
	ReleaseMutex(hMutex); 
	closesocket(hClntSock); 
	return 0; 
 }
 int main() 
 { 
	 // 加载套接字库
	WORD wVersionRequested; 
	WSADATA wsaData; 
	int err; 
	HANDLE hThread;
	wVersionRequested = MAKEWORD(1, 1);
	// 初始化套接字库 
	err = WSAStartup(wVersionRequested, &wsaData); 
	if (err != 0) 
	{ 
		return err; 
	}
	if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) 
	{ 
		WSACleanup();
		return -1; 
	}
	//创建一个互斥对象 
		hMutex = CreateMutex(NULL, FALSE, NULL); 
	// 新建套接字 
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0); 
	SOCKADDR_IN addrSrv; 
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); 
	addrSrv.sin_family = AF_INET; 
	addrSrv.sin_port = htons(9190); 
	// 绑定套接字到本地IP地址,端口号9190 
	if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR) 
	{ 
		printf("bind ERRORnum = %d\n", GetLastError());
		return -1;
	}
	// 开始监听 
	if(listen(sockSrv, 5) == SOCKET_ERROR) 
	{ 
		printf("listen ERRORnum = %d\n", GetLastError());
		return -1;
	}
	printf("start listen\n"); 
	SOCKADDR_IN addrCli; 
	int len = sizeof(SOCKADDR); 
	while (1) 
	{ 
		// 接收客户连接 sockConn此时来的客户端连接
		SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrCli, &len); 
		//每来一个连接,服务端起一个线程(安排一个工人)维护客户端的连接
		 //每来一个连接,全局数组应该加一个成员,最大连接数加1
		WaitForSingleObject(hMutex, INFINITE);
		clntSocks[clntCnt++] = sockConn;
		ReleaseMutex(hMutex); 
		hThread = (HANDLE)_beginthreadex(NULL, 0, HandleCln, (void*)&sockConn, 0, NULL);
		printf("Connect client IP: %s \n", inet_ntoa(addrCli.sin_addr)); 
		printf("Connect client num: %d \n", clntCnt); 
	}
	closesocket(sockSrv); 
	WSACleanup();
	return 0; 
 }

4.2 客户端

// 1 接收服务端的消息 安排一个工人 起一个线程接收消息 
// // 2 发送消息给服务端 安排一个工人 起一个线程发送消息
//  // 3 退出机制 
#include <WinSock2.h> 
#include <iostream>
#include <windows.h>
#include <process.h>
#pragma comment(lib, "ws2_32.lib")

#define NAME_SIZE 32 
#define BUF_SIZE 256 
char szName[NAME_SIZE] = "[DEFAULT]";
char szMsg[BUF_SIZE]; 
//发送消息给服务端 
unsigned WINAPI SendMsg(void* arg) { 
	//1 接收传递过来的参
	SOCKET hClntSock = *((SOCKET*)arg);
	char szNameMsg[NAME_SIZE + BUF_SIZE];
	//又有名字,又有消息 
	// //循环接收来自于控制台的消息 
	while (1) { 
		fgets(szMsg, BUF_SIZE, stdin); //阻塞在这一句 //退出机制 当收到q或Q 退出
		if (!strcmp(szMsg, "Q\n") || !strcmp(szMsg, "q\n")) 
		{
			closesocket(hClntSock); 
			exit(0); 
		}
sprintf(szNameMsg, "%s %s", szName, szMsg);//字符串拼接 
send(hClntSock, szNameMsg, strlen(szNameMsg), 0);//发送
	}return 0; 
}//接收服务端的消息 
unsigned WINAPI RecvMsg(void* arg) { 
	//1 接收传递过来的参数 
	SOCKET hClntSock = *((SOCKET*)arg); 
	char szNameMsg[NAME_SIZE + BUF_SIZE]; //又有名字,又有消息 
	int iLen = 0; while (1) 
	{ //recv阻塞 
		iLen = recv(hClntSock, szNameMsg, NAME_SIZE + BUF_SIZE - 1, 0); //服务端断开 
		if (iLen == -1) 
		{
			return -1;
		}// szNameMsg的0到iLen -1 都是收到的数据 iLen个 
		szNameMsg[iLen] = 0; //接收到的数据输出到控制台 
		fputs(szNameMsg, stdout); 
	}
	return 0;
}// 带参数的main函数,用命令行启动 在当前目录按下shift + 鼠标右键 cmd 
int main(int argc, char *argv[]) { 
	// 加载套接字库 
	 WORD wVersionRequested; 
	 WSADATA wsaData; 
	 int err;
  SOCKET hSock;
  SOCKADDR_IN servAdr;
  HANDLE hSendThread, hRecvThread; 
  wVersionRequested = MAKEWORD(1, 1); // 初始化套接字库
  err = WSAStartup(wVersionRequested, &wsaData); 
  if (err != 0) { 
	  return err;
  }
  if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {
	  WSACleanup(); 
	  return -1;
  }
sprintf(szName, "[%s]", argv[1]); 
//1 建立socket 
hSock = socket(PF_INET, SOCK_STREAM, 0); 
// 2 配置端口和地址 
memset(&servAdr, 0, sizeof(servAdr)); 
servAdr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
servAdr.sin_family = AF_INET; 
servAdr.sin_port = htons(9190); 
// 3 连接服务器
 if (connect(hSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR) 
 { 
	 printf("connect error error code = %d\n",GetLastError());
	 return -1;
 }
 // 4 发送服务端的消息 安排一个工人 起一个线程发送消息 
 hSendThread = (HANDLE)_beginthreadex(NULL, 0, SendMsg, (void*)&hSock, 0, NULL); 
 // 5 接收消息给服务端 安排一个工人 起一个线程接收消息 
 hRecvThread = (HANDLE)_beginthreadex(NULL, 0, RecvMsg, (void*)&hSock, 0, NULL); 
 //等待内核对象的信号发生变化 
 WaitForSingleObject(hSendThread, INFINITE); 
 WaitForSingleObject(hRecvThread, INFINITE); 
 // 6 关闭套接字
 closesocket(hSock); 
 WSACleanup(); 
 return 0; 
}

4.3 结果

在这里插入图片描述

文章来源:https://blog.csdn.net/qq_40489522/article/details/135365551
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。