进程/线程
一、程序运行
1.每个程序运行的时候,操作系统会给这个程序分配一个进程,以32位操作系统为例,就会分配4GB虚拟内存空间(代码段、数据段、堆、栈),将程序的代码加载到代码段,并运行程序,执行程序指令。
二、什么是线程
1.线程是基于进程的轻量级的调度单元,也是操作系统可以调用独立的最小单元,线程是在一个进程中创建出来的,当一个进程分配出来时,他本身就是一个线程。
2.一个进程可创建多个线程,这些线程共享进程的代码段、数据段、堆,唯一不共用栈,每个线程都有自己独立的栈,这样每个线程的函数调用与执行都是独立互不影响的。
3.线程在执行过程中,随时有可能挂起调度出去,所以当两个线程在访问共同资源时要注意数据的同步。
4.代码段、数据段、堆上的数据每个线程都是共享访问。
线程挂起/休眠
一、线程事件(线程条件)
1.线程可以通过系统api挂在事件对象上,挂起后线程不会往下执行(不会被调度),只有到事件被触发了,唤醒了挂在这个事件上的所有线程。
二、休眠
1.线程可以调用休眠函数来挂起,休眠时间结束后唤醒。
三、线程安全
1.当有多个线程访问同一个共享数据时(全局变量、堆),如果要把这个共享数据设计成线程安全的,则需要使用临界区或互斥量(暂不介绍)。
2.线程先申请进入临界区,如果临界区已被占用,则挂起线程、等待直到其他线程离开临界区。
3.进入临界区后可操作该共享数据。
4.操作完共享数据后,离开临界区,此时能唤醒等待进入临界区的其他线程,其他线程进入临界区后同样操作共享数据,再离开临界区。
创建线程
一、创建一个线程
HANDLE WINAPI CreateThread(
LPSECURITY_ATTRIBUTESlpThreadAttributes,//线程内核对象安全属性,一般传NULL表示使用默认SIZE_TdwStackSize,//线程栈控件大小,一般传0表示使用默认(1MB)
LPTHREAD_START_ROUTINElpStartAddress,//新线程锁执行的函数地址
LPVOIDlpParameter,//传给线程函数的参数
DWORDdwCreationFlags,//指定标志来控制线程创建,0表示创建后可立即调度,CREATE_SUSPENDED表示线程创建后暂停运行,这样它将无法调度,直到调用ResumeThread()
LPDWORDlpThreadId//输出线程ID,传NULL表示不需返回线程ID
);
等待线程结束
一、等待内核对象触发 WaitForSingleObject/WaitForMultipleObjects
DWORDWINAPIWaitForSingleObject(
HANDLE hHandle,//
DWORDdwMilliseconds//
);
二、获取线程ID函数
GetCurrentThreadId();
事件操作
一、创建事件
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTESlpEventAttributes,//表示安全控制,一般传NULL
BOOL bManualReset,//手动还是自动置位,传FALSE表示自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态
BOOL bInitialState,//表示事件初始状态,true表示已触发
LPCTSTRlpName//表示事件的名称,传NULL表示匿名事件
);
二、打开事件
HANDLE OpenEvent(
DWORDdwDesiredAccess,//表示访问的权限,一般传EVENT_ALL_ACCESS,详细解释可查看相关文档
BOOLbInheritHandle,//表示事件句柄继承性,一般传TRUE
LPCTSTRlpName //名称,不同进程的各线程可以通过名称来确保它们访问的是用一事件
);
三、触发事件
BOOL SetEvent(HANDLE hEvent);//每次调用后,必有一个或多个处于等待状态下的线程变成可调度状态
四、重置事件
BOOL SetEvent(HANDLE hEvent); //每次调用后,必有一个或多个处于等待状态下的线程变成可调度状态
临界区
一、 临界区:CRITICAL_SECTION 临界区对象;
InitializeCriticalSection() // 初始化临界区;
EnterCriticalSection(); // 请求进入临界区
LeaveCriticalSection();// 离开临界区;
程序代码
#include <stdlib.h>
#include <string.h>
#include <windows.h>
//全局变量(数据段)
static int global_value = 0;
//
//共用代码段函数
void fun(char* printStr)
{
printf("%s\n", printStr);
}
//
/*
事件通知/等待:
*线程A,等待线程B完成达到某条件后,才能继续;
*案例:多媒体解码线程,等待输入线程输入数据,有数据后,通知解码线程解码;
*步骤:
1.创建一个事件,线程都可以访问;
2.等待的线程,调用函数来等待时间;
3.触发的线程,当条件满足后触发;
*/
static HANDLE wait_condition = INVALID_HANDLE_VALUE;
/*
线程安全机制:
*代码段、数据段、堆是共享的,所以会存在一个问题,多个线程同时访问一个资源时,由于线程之间随时切换,所以就会导致访问的同一资源可能产生冲突;
*为了防止线程之间资源访问冲突,那么可以加一个临界区的机制;
*临界区保证了共享资源同时只有一个线程在处理;
*另一种术语叫:线程同步/线程同步锁
*步骤:
1.需要请求一个资源时,先进入临界区,如果这个临界区已被占用了,则等待其他线程离开临界区;
2.如果请求成功,则逻辑处理,处理完后离开这个临界区;
*线程死锁
线程A: 进入区1、进入区2 ······ 离开临界区1,离开临界区2
线程B: 进入区2、进入区1 ······ 离开临界区2,离开临界区1
*避免死锁: 用同样的顺序来获得我们的多个锁
线程A: 进入区1、进入区2 ······ 离开临界区2,离开临界区1
线程B: 进入区1、进入区2 ······ 离开临界区2,离开临界区1
*/
CRITICAL_SECTION section;
DWORD WINAPI thread_entry(LPVOID lpThreadParameter)
{
//线程启动后3秒后触发事件
Sleep(3000);
SetEvent(wait_condition);
while (1)
{
fun("sub thread");
//进入临界区
EnterCriticalSection(§ion);
//访问共享资源
global_value = 100;
//离开临界区
LeaveCriticalSection(§ion);
printf("global_value:%d\n", global_value);
Sleep(3000); //3秒
}
return 0;
}
int main(int argc, char** argv)
{
/*
1.线程句柄: HANDLE;
2.创建线程API: CreateThread;
3.属性: NULL;
4.栈大小: 0;
5.执行函数地址: thread_entry;
6.执行函数参数指针: NULL;
7.标记: 0;
8.线程id: thread_id;
*/
DWORD thread_id;
HANDLE h = CreateThread(NULL, 0, thread_entry, NULL, 0, &thread_id);
//事件创建
/*
1.事件句柄: HANDLE;
2.创建线程API: CreateEvent;
3.属性: NULL;
4.手动重置: FALSE;
5.初始触发: FALSE;
6.名称:NULL;
*/
wait_condition = CreateEvent(NULL,FALSE,FALSE,NULL);
//等待事件触发
printf("waiting...\n");
WaitForSingleObject(wait_condition, INFINITE);
//初始化 临界区
InitializeCriticalSection(§ion);
//主线程
while (1)
{
fun("main thread");
//进入临界区
EnterCriticalSection(§ion);
//访问共享资源
global_value = 200;
//离开临界区
LeaveCriticalSection(§ion);
printf("global_value:%d\n", global_value);
Sleep(1700); //1.7秒
}
system("pause");
return 0;
}