2021/10/31 paradigm 笔记

本文介绍了并行概念,包括进程和线程的区别,详细讲解了Windows环境下利用C语言的<process.h>和<windows.h>创建、运行、挂起和销毁线程的方法,并通过售票员问题展示了线程级并行实现。此外,还介绍了如何使用信号量解决资源竞争问题,防止死锁现象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 并行概念

问题:怎么让1个应用程序中的2个“函数”看起来好像是同时在运行呢?
—— 线程;
问题:怎么让2个“应用程序”看起来好像是在同一个处理器中同时运行呢?
—— 进程;

1.1 虚拟地址空间

虚拟:指的是对于每个应用程序来说,它的地址空间自己看上去好像是无限宽广

1.1.1 进程的概念

实际上,同一时间,只有1个CPU,只有1个寄存器组!

在这里插入图片描述

MMU的作用:将每个应用程序的“虚拟地址”映射到真正的“物理地址”上!通过MMU我们将1个处理器,编程多个“假的处理器”!

1.1.2 线程的概念

在这里插入图片描述

多线程的作用:让1个程序里面的多个函数一起跑起来!!!

各个线程以“轮转”的方式平等占用处理器一段时间(这个时间可能是100 ms)。

示例:售票员问题

问题:假设的售票大厅共有10个售票员,150张票,请模拟10个售票员同时售卖150张票的情形!

模拟代码(非并行):

void SellTickets(int agentID, int numTicketsToSell)
{
	while(numTicketsToSell > 0) {
		printf("Agent %d sells a ticket\r\n", agentID),
		numTicketsToSell--;
	}
	printf("Agent %d: All Down\r\n", agentID);
}

int main()
{
	uint8_t agent;
	int numAgents = 10; // 10个售票员
	int numTickets = 150; // 150 张票
	
	for(agent = 0; agent < numAgents; agent++) {
		SellTickets(agent, numTickets / numAgents);
	}
	return 0;
}

预计结果:160行printf输出!这些票是从第0位售票员开始,依次到第1位售票员、第2位售票员……第9位售票员,1张1张地卖出去的,并不是10个售票员同时售票!!!这样做,10个人弄得跟1个人一样!!!效率太低了!!!

我们肯定希望,10个售票员同时往出买票。

需求:需要寻找1个线程库,来帮助你实现“线程”!

2 windows下的多线程

多线程的意义在于,1个线程在被阻塞的时候,另1个线程还可以运行!你想想啊,最开始学单片机的时候,有个叫“软件延时的”,这是纯粹消耗机器周期以进行延时。在“软件延时”的过程中,CPU干不了别的事情,它只能在那里干等着!这不是浪费资源嘛!

2.1 C的 <process.h> 与 <windows.h>

<process.h>

序号函数名功能
1_beginthread()创建1个新线程(简洁版)
2_endthread()销毁1个线程(简洁版)
3_beginthreadex()创建1个新线程(全面版)
4_endthreadex()销毁1个线程(全面版)

<windows.h>

序号函数名功能
1ResumeThread()恢复线程的运行
2SuspendThread()挂起线程
3GetExiCodeThread()得到1个线程的退出码
4WaitForSingleObject()等待1个对象
5WaitForMultipleObjects()等待多个对象

2.1.1 创建线程的方法

<process.h> 中提供了2种创建线程的方式!1个简洁版本的 _beginthread(),这个获取不了子线程的ID号;另1个是全面版本的 _beginthreadex(),这个可以获取子线程的ID号!!!

2.1.1.1 _beginthread() 原型
_CRTIMP uintptr_t __cdecl _beginthread(void (__cdecl *_StartAddress) (void *),
									unsigned int _StackSize,
									void *_ArgList);
  • 第1个参数 *_StartAddress
    你要执行的函数入口地址(名字);
  • 第2个参数 _StackSize
    分配给这个线程的栈大小,默认0(1MB);
  • 第3个参数 *_ArgList
    传递给线程的参数列表,无参时写NULL;
  • 返回值
    成功则返回新线程的句柄,失败则返回0
2.1.1.2 _beginthreadex() 原型
_CRTIMP uintptr_t __cdecl _beginthreadex(void *_Security,
										unsigned int _StackSize,
										unsigned int (__stdcall *_StartAddress) (void *),
										void *_ArgList,
										unsigned int _InitFlag,
										unsigned int *_ThrdAddr);
  • 第1个参数 *_Security
    安全属性,默认NULL;
  • 第2个参数 _StackSize
    分配给这个线程的栈大小,默认0(1MB);
  • 第3个参数 *_StartAddress
    你要执行的函数地址(名字)
  • 第4个参数 *_ArgList
    函数入参列表
  • 第5个参数 _InitFlag
    新线程的初始状态,0表示立即执行,CREATE_SUSPENDED 表示挂起
  • 第6个参数 *_ThrdAddr
    用来传出线程的ID号
  • 返回值
    成功则返回新线程的句柄,失败则返回0
示例:运行1个线程
例1:使用简洁的 _beginthread()
typedef struct tag_paramStruct {
	int param1;
	int param2;
	float param3;
} PARAM;

void func(void *pParameter)
{
	PARAM *pMsg = (PARAM *)pParameter;
	
	while (1)
	{
		printf("Hello, I am thread. \r\n");
		Sleep(1000);
		printf("I get param 1 is %d\r\n", pMsg->param1);
		Sleep(500);
		printf("I get param 2 is %d\r\n", pMsg->param2);
		Sleep(500);
		printf("I get param 3 is %f\r\n", pMsg->param3);
		Sleep(500);
	}
}

int main(int argc, char **argv)
{
	PARAM pParameter;
	pParameter.param1 = 5;
	pParameter.param2 = 10;
	pParameter.param3 = 3.1415926;
	_beginthread(func, 0, &pParameter);

	while (1)
	{
		printf(">>>>>>>>>> I am main thread\r\n");
		Sleep(2000);
	}
	
	return 0;
}
例2:使用全面的 _beginthreadex()
// 定义标准的线程函数
unsigned int __stdcall threadFunc(LPVOID param)
{
	unsigned int *p = (unsigned int *)param;  // dereference
	while (1)
	{
		printf("Hello, I am thread %d!\r\n", *p);
		Sleep(1000);
	}

    return 0;
}
// 主线程
int main(int argc, char **argv)
{
	unsigned int threadId = 0;
	_beginthreadex(NULL, 0, threadFunc, &threadId, 0, &threadId);

	while (1)
	{
		/* code */
	}
	
	return 0;
}

2.1.2 运行线程的方法

2.1.3 挂起线程的方法

2.1.4 销毁线程的方法

2.2 售票员问题的线程级并行实现

2.2.1 相关头文件

#include <stdio.h>
#include "datatype.h"
#include <process.h>
#include <windows.h>
#include <stdlib.h>
#include <time.h>

2.2.2 创建所有线程共有资源(全局变量)

// 定义为全局变量,使得每个线程都能够访问!
#define numAgents 10 // 10个售票员
#define numTickets 150 // 150 张票

typedef struct tag_AgentData{
    unsigned int agentID;
    unsigned int numTicketsToSell;
}AgentData;

// 定义为全局变量,使得每个线程都能够访问!
// 每个售票员必须有自己独立的线程入参、线程id、线程句柄
AgentData pParam[numAgents]; // 每个线程的入参
unsigned int ThreadId[numAgents]; // 每个线程的ID号
HANDLE pHandler[numAgents]; // 每个线程的返回句柄

2.2.3 创建线程中要执行的函数

unsigned int __stdcall SellTickets(LPVOID param)
{
	AgentData *p = (AgentData *)param;

	while(p->numTicketsToSell > 0) {
		printf("Agent %d sells a ticket, %d tickets left\r\n", 
				p->agentID, p->numTicketsToSell);
		p->numTicketsToSell--;

		Sleep(rand() % ThreadId[p->agentID] % 3000); // 仿真给每个线程添加“随机的阻塞”!
	}
	printf("Agent %d: All Down\r\n", p->agentID);

	return 0;
}

2.2.4 主线程

int main(int argc, char **argv)
{
	uint8_t agent; // 用于循环控制
	uint8_t t = 0;
	
	srand(time(NULL));

	for(agent = 0; agent < numAgents; agent++) {
		pParam[agent].agentID = agent;
		pParam[agent].numTicketsToSell = numTickets / numAgents;

		pHandler[agent] = (HANDLE)_beginthreadex(NULL, 0, 
												SellTickets, &pParam[agent], 
												CREATE_SUSPENDED, 
												&ThreadId[agent]);
	}

	for(agent = 0; agent < numAgents; agent++) {
		ResumeThread(pHandler[agent]);
	}
	
	while (1)
	{
		printf(">>>>>>>> I am main thread >>>> Time is [%ds] >>>>\r\n", t);
		Sleep(1000);
		t++;
	}
	
	return 0;
}

2.3 售票员问题之“信号量”

我们不给每个售票员平均分配相等的票数了!改用每个售票员从1个共有资源地方取票的方法!

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

2.3.1 windows使用信号量的步骤

步骤函数说明
1CreateSemaphore()创建1个信号量
2OpenSemaphore()打开1个已经存在的信号量
3WaitForSingleObject()、WaitForMultipleObjects()等待信号量
4ReleaseSemaphore()释放信号量的占有权
5CloseHandle()关闭信号量

信号量是这样工作的:

  • 等待信号量:等到能用的时候,我就把信号量 -1 (上锁) —— 表示“正在占用”状态!
  • 释放信号量:把“信号量” +1 (解锁) —— 初始值必须>0,也就是释放状态!
信号量的值说明
0上锁(激活)
1解锁(释放)

在这里插入图片描述

信号量用于保护“Critical Region” (临界区域)!将其视为1个不可分割的“原子操作”!

2.3.1.1 创建信号量 CreateSemaphore()
WINBASEAPI HANDLE WINAPI CreateSemaphore (LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, 
										LONG lInitialCount, 
										LONG lMaximumCount, 
										LPCWSTR lpName);
  • 第1个参数:lpSemaphoreAttributes
    安全属性,如果为NULL则是默认安全属性
  • 第2个参数:lInitialCount
    信号量的初始值,要求 >= 0 且 <= lMaximumCount
  • 第3个参数:lMaximumCount
    信号量的最大值
  • 第4个参数:lpName
    信号量的名字,这是个字符串!可以写NULL,表示产生一个匿名信号量。任何线程(或进程)都可以根据这个“字符串”引用到这个信号量。
  • 返回值:HANDLE
    信号量的句柄
2.3.1.2 打开某个信号量 OpenSemaphore()
WINBASEAPI HANDLE WINAPI OpenSemaphore (DWORD dwDesiredAccess, 
										WINBOOL bInheritHandle, 
										LPCSTR lpName);
  • 第1个参数:dwDesiredAccess
    表示访问权限,一般传入SEMAPHORE_ALL_ACCESS
  • 第2个参数:bInheritHandle
    表示信号量句柄继承性,一般传入TRUE
  • 第3个参数:lpName
    信号量的名称,就是CreateSemaphore()时最后那个“字符串”型的名字!
2.3.1.3 等待信号量释放 WaitForSingleObject()
WINBASEAPI DWORD WINAPI WaitForSingleObject (HANDLE hHandle, 
											DWORD dwMilliseconds)

这个函数用于等待信号量的释放!

  • 第1个参数:hHandle
    被等待的信号量的句柄,HANDLE类型
  • 第2个参数:dwMilliseconds
    等待的时间,单位ms
2.3.1.4 释放信号量 ReleaseSemaphore()
WINBASEAPI WINBOOL WINAPI ReleaseSemaphore (HANDLE hSemaphore, 
											LONG lReleaseCount, 
											LPLONG lpPreviousCount)
  • 第1个参数:hSemaphore
    信号量的句柄,HANDLE类型,就是使用CreateSemaphore()的返回值
  • 第2个参数:lReleaseCount
    表示信号量值释放的个数,必须大于0且不超过最大资源数,一般为1
  • 第3个参数:lpPreviousCount
    这是1个指针,用于传出先前信号量的计数值!

2.3.2 售票员示例代码

#include <stdio.h>
#include "datatype.h"
#include <process.h>
#include <windows.h>
#include <stdlib.h>
#include <time.h>

// 定义为全局变量,使得每个线程都能够访问!
#define numAgents 10 // 10个售票员

typedef struct tag_AgentData{
    unsigned int agentID;
    unsigned int *pNumTickets; // 访问公共资源:票数 numTickets
	HANDLE hSemaphore;
}AgentData;

// 定义为全局变量,使得每个线程都能够访问!
// 每个售票员必须有自己独立的线程入参、线程id、线程句柄
AgentData pParam[numAgents]; // 每个线程的入参
unsigned int ThreadId[numAgents]; // 每个线程的ID号
HANDLE pHandler[numAgents]; // 每个线程的返回句柄

HANDLE gThreadSema;  //创建内核对象,用来初始化信号量

unsigned int __stdcall SellTickets(LPVOID param)
{
	AgentData *p = (AgentData *)param;

	printf("There are %d tikets\r\n", *p->pNumTickets);
	while(1) {
		Sleep(rand() % ThreadId[p->agentID] % 1000); // 仿真售票员在查票!

		WaitForSingleObject(gThreadSema, INFINITE); // 等待信号量
		if(*p->pNumTickets == 0) {
			printf("No tickets!\r\n");
			break;
		};
		// *(p->pNumTickets) --; // 这样写不对哦!
		(*p->pNumTickets) --;
		ReleaseSemaphore(p->hSemaphore, 1, NULL); // 信号量的值 + 1
		printf("Agent %d sells a ticket, %d tickets left\r\n",
				p->agentID, *p->pNumTickets);

		Sleep(rand() % ThreadId[p->agentID] % 1000); // 仿真售票员在结账!
	}
	ReleaseSemaphore(p->hSemaphore, 1, NULL); // 信号量的值 + 1

	return 0;
}

int main(int argc, char **argv)
{
	// 公共资源 150张票
	unsigned int numTickets = 150;

	uint8_t agent; // 用于循环控制
	uint8_t t = 0;
	
	srand(time(NULL));

	gThreadSema = CreateSemaphore(NULL, 1, 1, NULL);

	for(agent = 0; agent < numAgents; agent++) {
		pParam[agent].agentID = agent;
		pParam[agent].pNumTickets = &numTickets;
		pParam[agent].hSemaphore = gThreadSema;

		pHandler[agent] = (HANDLE)_beginthreadex(NULL, 0, 
												SellTickets, &pParam[agent], 
												CREATE_SUSPENDED, 
												&ThreadId[agent]);
	}

	for(agent = 0; agent < numAgents; agent++) {
		ResumeThread(pHandler[agent]);
	}
	
	while (1)
	{
		printf(">>>>>>>> I am main thread >>>> Time is [%ds] >>>>\r\n", t);
		Sleep(1000);
		t++;
	}
	
	return 0;
}

2.3.3 “死锁”现象

// 上述“售票员”程序中,创建这个信号量的初始值必须为 1 ,不然会造成死锁
gThreadSema = CreateSemaphore(NULL, 1, 1, NULL);

创建信号量的时候,假如给初始值为 0 (表示正在“锁着”),如果你后续没有跟着一次解锁ReleaseSemaphore()操作,那么就会导致“死锁”!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值