1 基础知识
1.1 进程与线程
进程: 计算机中程序在相关数据集上的一次运行活动,是系统进行资源分配和调度的基本的、独立的运作单元;进程有独立的地址空间和供其调用的系统资源;进程是程序的实体。
线程: 线程是进程下的一个实体,它是CPU调度的基本单位。
**线程与进程的关系:**线程是进程中独立运作的任务单元,或者说是子任务;一个进程至少包含一个线程。
1.2 并行与并发
并行: 当计算机系统拥有两个及其以上的CPU内核时,就拥有了线程并行的能力。此时每个CPU内核单独的调度一个线程实例,这些线程实例之间不用抢夺CPU资源,只占有各自归属的CPU资源,这种多任务同时进行的方式称为并行。
并发: 当计算机系统只有一个CPU内核时,又要满足多个任务同时运行,这时系统就需要把CPU的运行时间划分成若干个较小的时间片段,并将CPU的资源在不同的时间片段中分配给不同的线程,一个时间片单元中只有一个线程独占CPU资源,到达时间点后资源被收回交予另一个线程,当前线程处于挂起等待的状态,这些线程实例之间需要抢夺CPU资源,这种多任务同时进行的方式称为并发。
一对双胞胎兄弟小明、小光,吃饭的时候,妈妈喂一口小明,再喂一口小光、再喂小明,循环往复进行,这个喂饭的过程就是并发;如果奶奶在家里,奶奶喂小明,妈妈喂小光,这个喂饭的过程就是并行。
1.3 线程的状态及生命周期
线程的状态:
- 新建状态(New):线程对象被创建出来后的状态就是新建状态。
- 就绪状态(Runnable):被创建后的线程,在被其他线程电泳其star()方法后,线程及被启动,此时线程任务已经进入CPU调度序列,随时都会被CPU调度,所以此时的状态又被称之为“可执行性状态”。
- 运行状态(Running Man):线程取得了CPU的资源,任务被执行。
- 阻塞状态(Blocked):正在运行的线程因为某些原因放弃了CPU的使用权限,暂时的停止了任务的执行,如主动执行线程的wait()方法、线程获取对象资源时遇到资源锁、线程中执行Sleep()或者join()方法、线程发起IO请求等、此时的状态则为堵塞状态。
- 死亡状态(Dead):线程执行完自身任务或者异常退出线程的Run(),线程的生命周期结束。
线程的生命周期:
线程运行过程中几种常用的方法的说明:
- sleep()方法是让当前线程放弃CPU对其的调度,让线程进入等待状态,此时线程不会放弃其所占有的资源,等到休眠结束后,状态切换为就绪状态,和同样处于此状态的其他线程竞争资源,等待CPU调度;
- yield()方法是让当前运行的线程放弃其获得的CPU执行片段,返回到可执行状态,此时线程将和其他线程共同竞争CPU资源。
- wait()也是让线程放弃其对CPU的占用并同时释放其对同步锁的占用,而进入堵塞状态。
2 线程的创建和使用
2.1 Win32 API
方法 | 描述 |
---|---|
CreateThread() | 创建线程 |
ExitThread() | 正常退出 |
TerminateThead() | 强制退出 |
ResumeThread() | 线程重启 |
SuspendThread() | 线程挂起 |
CloseHandle() | 关闭线程句柄 |
PostThreadMessage() | 发送消息给线程 |
GetCurrentThread() | 得到线程句柄 |
GetThreadId() | 获取线程Id |
WaitForSingleObject() | 等待一个对象 |
WaitForMultipleObjects() |
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
SIZE_T dwStackSize,//initialstacksize
LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction
LPVOID lpParameter,//threadargument
DWORD dwCreationFlags,//creationoption
LPDWORD lpThreadId//threadidentifier)
2.2 C++11 线程
C++11中有了标准的线程库 std::thread,其常用方法如下:
方法 | 描述 |
---|---|
thread() | 构造函数,参数支持值、引用(std::ref)以及移动(std::move)传递。 |
join() | 启动线程,会堵塞主线程(调用它的线程),被调线程的资源由主线程回收 |
detach() | 启动线程,不会堵塞主线程,被调线程的资源无法有主线程回收,由运行时库回收 |
joinable() | 检查是否可以被启动 |
swap() | 交换连个线程的句柄 |
get_id() | 获取线程id |
yield() | 放弃执行 |
sleep_until() | 休眠至指定的时刻 |
sleep_for() | 休眠指定的时间片段 |
3 多线程并发
3.1 多线程同步
3.1.1 问题的引入
多线程并发处理某个任务的时候,如果处理它的多个线程间需要访问相同的内存分区数据,并且需要对访问的数据进行写操作,出现这种情况时就可能造成内存操作异常或者数据混乱;同时有时候还需要基于特定的先后孙旭去访问资源,如不安预定的顺序访问可能导致程序运行的结果达不到的预期执行任务。
3.1.2 多线程同步
基于上述问题,就引入了线程之间通信的两个基本问题,线程互斥与线程同步。
资源的互斥访问: 多线程并发访问公共资源时,某一时间片段只允许一个线程访问资源,直到当前线程访问结束,竞争资源的其他线程才可以获得访问的权限,而且也只能随机的被其中一个线程访问,这种资源访问的方式就是互斥访问。
资源的同步访问: 多线程并发访问公共资源在互斥的基础上,还需要资源被访问的顺序按照我们预期设计的顺序进行有序的访问,这种访问的方式即为同步访问。
线程互斥是一种特殊的线程同步,互斥和同步对应着线程间通信发生的两种情况:
- 当有多个线程访问共享资源而不使资源被破坏时;
- 当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时。
3.1.3 同步机制
WIN32中实现同步机制的方法:
1、临界区 CRITICAL_SECTION:被保护的资源在在被访问的时候都需要需要放在同一个临界区中[EnterCriticalSection(&pSection); 操作被保护的资源;LeaveCriticalSection(&pSection)]。通过临界区保护资源时,资源并未被锁定,任何对象都可以去访问资源,因而想要确保资源被互斥访问就必须把每一次操作资源放在成对的临界区中。
2、互斥体Mutex:通过互斥体的锁操作来实现资源的互斥访问,资源的访问都放在一个互斥体的加锁解锁之间进行[pMutex.lock();操作被保护的资源;pMutex.unLock()],需要合理使用互斥体避免死锁。同时互斥体支持智能锁Lock_guard与unique_lock,这二者在互斥体的作用域中会自动控制锁的加锁和解锁过程。
3、信号量: 在信号量中有一个内置的计数值,用于对资源进行计数;同时它通过内置的互斥机制保证在有多个线程试图对计数值进行修改时,在任一时刻只有一个线程对计数值进行修改;信号量的两个核心操作是Up操作(提高计数值)和Down操作(降低计数值)。
4、事件:事件(Event)是WIN32提供的最灵活的线程间同步方式,事件可以处于激发状态(signaled or true)或未激发状态(unsignal or false)。根据状态变迁方式的不同,事件可分为两类。
linux中实现同步机制的方法:
1、互斥体锁:其本质就是一个特殊的全局变量,拥有lock和unlock两种状态,unlock的互斥锁可以由某个线程获得,一旦获得,这个互斥锁会锁上变成lock状态,此后只有该线程由权力打开该锁,其他线程想要获得互斥锁,必须得到互斥锁再次被打开之后。
2、读写锁:读写锁支持多线程同时读取,当不允许同时写。锁处于读模式的时候可以自愿共享,处于写模式的时候资源只能独占。读写锁有两种锁策略分为强度同步(读者更高的优先权,只要写者未操作,读者就可以访问)和强写同步(写着优先,所有写着结束读者才可读)。
3、条件变量:当线程在等待满足某些条件时使线程进入睡眠状态,一旦条件满足,就换线因等待满足特定条件而睡眠的线程,这种场景条件变量时实现同步的不错选择。
4、信号量: 在信号量中有一个内置的计数值,用于对资源进行计数;同时它通过内置的互斥机制保证在有多个线程试图对计数值进行修改时,在任一时刻只有一个线程对计数值进行修改;信号量的两个核心操作是Up操作(提高计数值)和Down操作(降低计数值)。
3.1.4 例子
- 有一个场景:我们需要在主线程中,通过两个线程交替的访问一个时间变量,同时每次访问的这个变量需要加1.每个线程只能访问这个变量10次,同时在子线程退出前,主线程不可退出。
代码
#include "stdafx.h"
#include <stdlib.h>
#include <ATLComTime.h>
#include <iostream>
#include <chrono>
#include <thread>
#define INIT_SECTION(s) InitializeCriticalSection(s)
#define ENTER_SECTION(s) EnterCriticalSection(s)
#define LEAVE_SECTION(s) LeaveCriticalSection(s)
#define DELT_SECTION(s) DeleteCriticalSection(s)
using namespace std;
int g_initTime = 63250; //定义全局变量
HANDLE g_hEvent; //事件句柄
bool g_bMainThreadOver = false; //定义全局变量
CRITICAL_SECTION g_critical; //临界区句柄
COleDateTime GetTime1(const int nTime)
{
int nHour = nTime / 3600;
int nMin = (nTime - nHour * 3600) / 60;
int nSec = nTime - nHour * 3600 - nMin * 60;
return COleDateTime(2021, 6, 11, nHour, nMin, nSec);
}
COleDateTime GetTime2(const int nTime)
{
int nHour = nTime / 3600;
int nMin = (nTime % 3600) / 60;
int nSec = nTime % 60;
return COleDateTime(2021, 6, 11, nHour, nMin, nSec);
}
void ThreadFun1()
{
for (int i = 0; i < 10; i++)
{
WaitForSingleObject(g_hEvent, INFINITE);
COleDateTime oleTime = GetTime1(g_initTime);
printf("Thread1-ThreadFun1 index = %d, oleTime = %d:%d:%d \n",i, oleTime.GetHour(),
oleTime.GetMinute(), oleTime.GetSecond());
g_initTime++;
SetEvent(g_hEvent);
}
}
void ThreadFun2()
{
for (int i = 0; i < 10; i++)
{
WaitForSingleObject(g_hEvent, INFINITE);
COleDateTime oleTime = GetTime2(g_initTime);
printf("Thread2-ThreadFun2 index = %d, oleTime = %d:%d:%d \n", i, oleTime.GetHour(),
oleTime.GetMinute(), oleTime.GetSecond());
g_initTime++;
SetEvent(g_hEvent);
}
ENTER_SECTION(&g_critical);
g_bMainThreadOver = true;
LEAVE_SECTION(&g_critical);
}
void ThreadCaller(const int n)
{
if (n == 1)
{
std::thread pThread(ThreadFun1);
pThread.detach();
}
else
{
std::thread pThread(ThreadFun2);
pThread.detach();
}
}
int main()
{
INIT_SECTION(&g_critical);
g_hEvent = CreateEvent(NULL, FALSE, TRUE, L"event1");
ThreadCaller(1);
ThreadCaller(2);
bool bOk = true;
while (bOk)
{
ENTER_SECTION(&g_critical);
bool bOver = g_bMainThreadOver;
LEAVE_SECTION(&g_critical);
if (g_bMainThreadOver)
{
bOk = false;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
printf("主线程将要退出 \n");
if (g_hEvent)
{
::CloseHandle(g_hEvent);
}
system("pause");
return 0;
}
运行结果
本例中我们使用了两种同步机制来实现线程互斥和线程同步。
通过一个Event事件实现了两个线程对变量g_init Time的互斥访问;通过一个临界区实现了对变量g_bMainThreadOver的互斥访问,并基于此实现了子线程2和主线程的同步问题。
- 在上面的基础上,场景变为需要两个线程交替访问时间变量g_initTime。
代码:
#include "stdafx.h"
#include <stdlib.h>
#include <ATLComTime.h>
#include <iostream>
#include <chrono>
#include <thread>
#define INIT_SECTION(s) InitializeCriticalSection(s)
#define ENTER_SECTION(s) EnterCriticalSection(s)
#define LEAVE_SECTION(s) LeaveCriticalSection(s)
#define DELT_SECTION(s) DeleteCriticalSection(s)
using namespace std;
int g_initTime = 63250; //定义全局变量
HANDLE g_hEvent1; //事件句柄
HANDLE g_hEvent2; //事件句柄
bool g_bMainThreadOver = false; //定义全局变量
CRITICAL_SECTION g_critical; //临界区句柄
COleDateTime GetTime1(const int nTime)
{
int nHour = nTime / 3600;
int nMin = (nTime - nHour * 3600) / 60;
int nSec = nTime - nHour * 3600 - nMin * 60;
return COleDateTime(2021, 6, 11, nHour, nMin, nSec);
}
COleDateTime GetTime2(const int nTime)
{
int nHour = nTime / 3600;
int nMin = (nTime % 3600) / 60;
int nSec = nTime % 60;
return COleDateTime(2021, 6, 11, nHour, nMin, nSec);
}
void ThreadFun1()
{
for (int i = 0; i < 10; i++)
{
WaitForSingleObject(g_hEvent1, INFINITE);
COleDateTime oleTime = GetTime1(g_initTime);
printf("Thread1-ThreadFun1 index = %d, oleTime = %d:%d:%d \n",i, oleTime.GetHour(),
oleTime.GetMinute(), oleTime.GetSecond());
g_initTime++;
SetEvent(g_hEvent2);
}
}
void ThreadFun2()
{
for (int i = 0; i < 10; i++)
{
WaitForSingleObject(g_hEvent2, INFINITE);
COleDateTime oleTime = GetTime2(g_initTime);
printf("Thread2-ThreadFun2 index = %d, oleTime = %d:%d:%d \n", i, oleTime.GetHour(),
oleTime.GetMinute(), oleTime.GetSecond());
g_initTime++;
SetEvent(g_hEvent1);
}
ENTER_SECTION(&g_critical);
g_bMainThreadOver = true;
LEAVE_SECTION(&g_critical);
}
void ThreadCaller(const int n)
{
if (n == 1)
{
std::thread pThread(ThreadFun1);
pThread.detach();
}
else
{
std::thread pThread(ThreadFun2);
pThread.detach();
}
}
int main()
{
INIT_SECTION(&g_critical);
g_hEvent1 = CreateEvent(NULL, FALSE, TRUE, L"event1");
g_hEvent2 = CreateEvent(NULL, FALSE, TRUE, L"event2");
ThreadCaller(1);
ThreadCaller(2);
bool bOk = true;
while (bOk)
{
ENTER_SECTION(&g_critical);
bool bOver = g_bMainThreadOver;
LEAVE_SECTION(&g_critical);
if (g_bMainThreadOver)
{
bOk = false;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
printf("主线程将要退出 \n");
if (g_hEvent1)
{
::CloseHandle(g_hEvent1);
}
if (g_hEvent2)
{
::CloseHandle(g_hEvent2);
}
system("pause");
return 0;
}
运行结果:
这当中,通过使用两个Event时间对象来做两个子线程间的同步问题,做到了多个线程按照我们制定的顺序去访问资源的目的。
3.2 线程间通信
1、使用全局变量:通过全局变量进行通信,要对该变量加关键字volatile。
2、自定义消息:略。
2、事件通信:如win32中CreateEvent()来定义事件;通过SetEvent(event)给事件赋予信号,通过WaitForSingleObject()来接受事件;
4、使用户信号量:如Win32函数 CreateSemaphore()用来产生信号量;WaitForSingleObject()来添加锁定;ReleaseSemaphore()用来解除锁定。
5、临界区或者互斥体:略。
4 线程池
4.1 线程池概念
线程池是一种使用线程的方式。在执行量大而耗时短的多线程业务中,线程的创建和销毁是一个非常耗系统资源的过程,这种开销会带来局部的内存压力和整体性能的波动。线程池就是用来规避因线程对象频繁创建而带来的资源消耗问题,除此外还可以防止过度调度和减小线程切换开销。
线程池中的线程一定要适量创建,创建太多线程而未被充分的使用反而会浪费资源。
4.2适用场景
1、任务体量大,需要并发处理,且任务的频次频繁。如web服务器的网页请求等。
2、对任务的响应速度要求极高。
3、应对任务体谅波动较大的情况。