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>
序号 | 函数名 | 功能 |
---|---|---|
1 | ResumeThread() | 恢复线程的运行 |
2 | SuspendThread() | 挂起线程 |
3 | GetExiCodeThread() | 得到1个线程的退出码 |
4 | WaitForSingleObject() | 等待1个对象 |
5 | WaitForMultipleObjects() | 等待多个对象 |
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使用信号量的步骤
步骤 | 函数 | 说明 |
---|---|---|
1 | CreateSemaphore() | 创建1个信号量 |
2 | OpenSemaphore() | 打开1个已经存在的信号量 |
3 | WaitForSingleObject()、WaitForMultipleObjects() | 等待信号量 |
4 | ReleaseSemaphore() | 释放信号量的占有权 |
5 | CloseHandle() | 关闭信号量 |
信号量是这样工作的:
- 等待信号量:等到能用的时候,我就把信号量 -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()操作,那么就会导致“死锁”!