多线程基础

主要讲关键段,事件,互斥量,信号量。

学习CSDN上MoreWindows博客《秒杀多线程》系列。

CreateThread与_beginthreadex

_beginthreadex()函数在创建新线程时会分配并初始化一个_tiddata块。这个_tiddata块自然是用来存放一些需要线程独享的数据。事实上新线程运行时会首先将_tiddata块与自己进一步关联起来。然后新线程调用标准C运行库函数如strtok()时就会先取得_tiddata块的地址再将需要保护的数据存入_tiddata块中。这样每个线程就只会访问和修改自己的数据而不会去篡改其它线程的数据了。因此,如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread()。

Interlocked

增减操作

//返回变量执行增减操作之后的值。
LONG __cdecl InterlockedIncrement(LONG volatile* Addend);
LONG __cdecl InterlockedDecrement(LONG volatile* Addend);
//返回运算后的值,注意!加个负数就是减。
LONG __cdecl InterlockedExchangeAdd(LONG volatile* Addend, LONGValue);

赋值操作

//Value是新值,函数会返回原先的值
LONG __cdecl InterlockedExchange(LONG volatile* Target, LONGValue);

关键段

CRITICAL_SECTION

初始化

函数功能:初始化

函数原型:

void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

函数说明:定义关键段变量后必须先初始化。

销毁

函数功能:销毁

函数原型:

void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

函数说明:用完之后记得销毁。

进入关键段

函数原型:

void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

函数说明:系统保证各线程互斥的进入关键区域。

离开关键段

函数原型:

void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

实例

【代码】

#include 
#include 
#include 
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//关键段变量声明
CRITICAL_SECTION  g_csThreadParameter, g_csThreadCode;
int main()
{
    //关键段初始化
    InitializeCriticalSection(&g_csThreadParameter);
    InitializeCriticalSection(&g_csThreadCode);
    HANDLE  handle[THREAD_NUM];
    g_nNum = 0;
    int i = 0;
    while (i < THREAD_NUM)
    {
    
        handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
        ++i;
    }
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
    DeleteCriticalSection(&g_csThreadCode);
    DeleteCriticalSection(&g_csThreadParameter);
    return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
    EnterCriticalSection(&g_csThreadParameter);//进入子线程序号关键区域
    int nThreadNum = *(int *)pPM;
    LeaveCriticalSection(&g_csThreadParameter);//离开子线程序号关键区域
    Sleep(50);//some work should to do
    EnterCriticalSection(&g_csThreadCode);//进入各子线程互斥区域
    g_nNum++;
    Sleep(0);//some work should to do
    printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
    LeaveCriticalSection(&g_csThreadCode);//离开各子线程互斥区域
    return 0;
}

【运行结果】

线程编号为10  全局资源值为1
线程编号为10  全局资源值为2
线程编号为10  全局资源值为3
线程编号为10  全局资源值为4
线程编号为10  全局资源值为5
线程编号为10  全局资源值为6
线程编号为10  全局资源值为7
线程编号为10  全局资源值为8
线程编号为10  全局资源值为9
线程编号为10  全局资源值为10
请按任意键继续. . .

小结

  1. 关键段共初始化、销毁、进入和离开关键区域四个函数。
  2. 关键段可以解决线程的互斥问题,但因为具有“线程所有权”,所以无法解决同步问题。
  3. 推荐关键段与旋转锁配合使用。

事件

CreateEvent

函数功能:创建事件

函数原型:

HANDLE CreateEvent(
    LPSECURITY_ATTRIBUTES lpEventAttributes,
    BOOL bManualReset,
    BOOL bInitialState,
    LPCTSTR lpName
);

函数说明:

第一个参数表示安全控制,一般直接传入NULL。

第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。打个小小比方,手动置位事件相当于教室门,教室门一旦打开(被触发),所有人都可以进入直到老师去关上教室门(事件变成未触发)。自动置位事件就相当于医院里拍X光的房间门,门打开后只能进入一个人,这个人进去后会将门关上,其它人不能进入除非门重新被打开(事件重新被触发)。

第三个参数表示事件的初始状态,传入TRUR表示已触发。

第四个参数表示事件的名称,传入NULL表示匿名事件。

OpenEvent

函数功能:根据名称获得一个事件句柄。

函数原型:

HANDLE OpenEvent(
    DWORD dwDesiredAccess,
    BOOL bInheritHandle,
    LPCTSTR lpName
);

函数说明:

第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示事件句柄继承性,一般传入TRUE即可。

第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。

SetEvent

函数功能:触发事件

函数原型:

BOOL SetEvent(HANDLE hEvent);

函数说明:每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。

ResetEvent

函数功能:将事件设为末触发

函数原型:

BOOL ResetEvent(HANDLE hEvent);

事件的清理与销毁

由于事件是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。

PulseEvent

函数功能:将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲。

函数原型:

BOOL PulseEvent(HANDLE hEvent);

函数说明:这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();此时情况可以分为两种:

1.对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。

2.对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。

此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态。

实例

【代码】

#include 
#include 
#include 
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//事件与关键段
HANDLE  g_hThreadEvent;
CRITICAL_SECTION g_csThreadCode;
int main()
{
    //初始化事件和关键段 自动置位,初始无触发的匿名事件
    g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    InitializeCriticalSection(&g_csThreadCode);
    HANDLE  handle[THREAD_NUM];
    g_nNum = 0;
    int i = 0;
    while (i < THREAD_NUM)
    {
        handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
        WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被触发
        i++;
    }
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
    //销毁事件和关键段
    CloseHandle(g_hThreadEvent);
    DeleteCriticalSection(&g_csThreadCode);
    return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
    int nThreadNum = *(int *)pPM;
    SetEvent(g_hThreadEvent); //触发事件
    Sleep(50);//some work should to do
    EnterCriticalSection(&g_csThreadCode);
    g_nNum++;
    Sleep(0);//some work should to do
    printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
    LeaveCriticalSection(&g_csThreadCode);
    return 0;
}

【运行结果】

线程编号为4  全局资源值为1
线程编号为0  全局资源值为2
线程编号为5  全局资源值为3
线程编号为1  全局资源值为4
线程编号为3  全局资源值为5
线程编号为2  全局资源值为6
线程编号为7  全局资源值为7
线程编号为8  全局资源值为8
线程编号为6  全局资源值为9
线程编号为9  全局资源值为10
请按任意键继续. . .

小结

  1. 事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。

  2. 事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。

  3. 事件可以解决线程间同步问题,因此也能解决互斥问题。

互斥量

CreateMutex

函数功能:创建互斥量(注意与事件Event的创建函数对比)

函数原型:

HANDLE CreateMutex(
    LPSECURITY_ATTRIBUTES lpMutexAttributes,
    BOOL bInitialOwner,     
    LPCTSTR lpName
);

函数说明:

第一个参数表示安全控制,一般直接传入NULL。

第二个参数用来确定互斥量的初始拥有者。如果传入TRUE表示互斥量对象内部会记录创建它的线程的线程ID号并将递归计数设置为1,由于该线程ID非零,所以互斥量处于未触发状态。如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态。

第三个参数用来设置互斥量的名称,在多个进程中的线程就是通过名称来确保它们访问的是同一个互斥量。

函数访问值:

成功返回一个表示互斥量的句柄,失败返回NULL。

打开互斥量

函数原型:

HANDLE OpenMutex(
    DWORD dwDesiredAccess,
    BOOL bInheritHandle,
    LPCTSTR lpName 
);

函数说明:

第一个参数表示访问权限,对互斥量一般传入MUTEX_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示互斥量句柄继承性,一般传入TRUE即可。

第三个参数表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。

函数访问值:

成功返回一个表示互斥量的句柄,失败返回NULL。

触发互斥量

函数原型:

BOOL ReleaseMutex (HANDLE hMutex)

函数说明:

访问互斥资源前应该要调用等待函数,结束访问时就要调用ReleaseMutex()来表示自己已经结束访问,其它线程可以开始访问了。

清理互斥量

由于互斥量是内核对象,因此使用CloseHandle()就可以(这一点所有内核对象都一样)。

实例

【代码】

//经典线程同步问题 互斥量Mutex
#include 
#include 
#include 

long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//互斥量与关键段
HANDLE  g_hThreadParameter;
CRITICAL_SECTION g_csThreadCode;

int main()
{
    //初始化互斥量与关键段 第二个参数为TRUE表示互斥量为创建线程所有
    g_hThreadParameter = CreateMutex(NULL, FALSE, NULL);
    InitializeCriticalSection(&g_csThreadCode);
    HANDLE  handle[THREAD_NUM];
    g_nNum = 0;
    int i = 0;
    while (i < THREAD_NUM)
    {
        handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
        WaitForSingleObject(g_hThreadParameter, INFINITE); //等待互斥量被触发
        i++;
    }
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
    //销毁互斥量和关键段
    CloseHandle(g_hThreadParameter);
    DeleteCriticalSection(&g_csThreadCode);
    for (i = 0; i < THREAD_NUM; i++)
        CloseHandle(handle[i]);
    return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
    int nThreadNum = *(int *)pPM;
    ReleaseMutex(g_hThreadParameter);//触发互斥量
    Sleep(50);//some work should to do
    EnterCriticalSection(&g_csThreadCode);
    g_nNum++;
    Sleep(0);//some work should to do
    printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
    LeaveCriticalSection(&g_csThreadCode);
    return 0;
}

【运行结果】

线程编号为10  全局资源值为1
线程编号为10  全局资源值为2
线程编号为10  全局资源值为3
线程编号为10  全局资源值为4
线程编号为10  全局资源值为5
线程编号为10  全局资源值为6
线程编号为10  全局资源值为7
线程编号为10  全局资源值为8
线程编号为10  全局资源值为9
线程编号为10  全局资源值为10
请按任意键继续. . .

小结

  1. 互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。

  2. 互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。

信号量

CreateSemaphore

函数功能:创建信号量

函数原型:

HANDLE CreateSemaphore(
    LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
    LONG lInitialCount,
    LONG lMaximumCount,
    LPCTSTR lpName
);

函数说明:

第一个参数表示安全控制,一般直接传入NULL。

第二个参数表示初始资源数量。

第三个参数表示最大并发数量。

第四个参数表示信号量的名称,传入NULL表示匿名信号量。

OpenSemaphore

函数功能:打开信号量

函数原型:

HANDLE OpenSemaphore(
    DWORD dwDesiredAccess,
    BOOL bInheritHandle,
    LPCTSTR lpName
);

函数说明:

第一个参数表示访问权限,对一般传入SEMAPHORE_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示信号量句柄继承性,一般传入TRUE即可。

第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个信号量。

ReleaseSemaphore

函数功能:递增信号量的当前资源计数

函数原型:

BOOL ReleaseSemaphore(
    HANDLE hSemaphore,
    LONG lReleaseCount,  
    LPLONG lpPreviousCount 
);

函数说明:

第一个参数是信号量的句柄。

第二个参数表示增加个数,必须大于0且不超过最大资源数量。

第三个参数可以用来传出先前的资源计数,设为NULL表示不需要传出。

注意:当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发。在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。

信号量的清理与销毁

由于信号量是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。

实例

【代码】

#include 
#include 
#include 
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//信号量与关键段
HANDLE            g_hThreadParameter;
CRITICAL_SECTION  g_csThreadCode;
int main()
{
    //初始化信号量和关键段
    g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);//当前0个资源,最大允许1个同时访问
    InitializeCriticalSection(&g_csThreadCode);
    HANDLE  handle[THREAD_NUM];
    g_nNum = 0;
    int i = 0;
    while (i < THREAD_NUM)
    {
        handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
        WaitForSingleObject(g_hThreadParameter, INFINITE);//等待信号量>0
        ++i;
    }
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
    //销毁信号量和关键段
    DeleteCriticalSection(&g_csThreadCode);
    CloseHandle(g_hThreadParameter);
    for (i = 0; i < THREAD_NUM; i++)
        CloseHandle(handle[i]);
    return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
    int nThreadNum = *(int *)pPM;
    ReleaseSemaphore(g_hThreadParameter, 1, NULL);//信号量++
    Sleep(50);//some work should to do
    EnterCriticalSection(&g_csThreadCode);
    ++g_nNum;
    Sleep(0);//some work should to do
    printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
    LeaveCriticalSection(&g_csThreadCode);
    return 0;
}

【运行结果】

线程编号为10  全局资源值为1
线程编号为10  全局资源值为2
线程编号为10  全局资源值为3
线程编号为10  全局资源值为4
线程编号为10  全局资源值为5
线程编号为10  全局资源值为6
线程编号为10  全局资源值为7
线程编号为10  全局资源值为8
线程编号为10  全局资源值为9
线程编号为10  全局资源值为10
请按任意键继续. . .

小结

  1. 信号量可以解决线程之间的同步问题。
  2. 由于信号量可以计算资源当前剩余量并根据当前剩余量与零比较来决定信号量是处于触发状态或是未触发状态,因此信号量的应用范围相当广泛。

关键段 事件 互斥量 信号量 总结

  • 线程(进程)同步的主要任务

在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。

  • 线程(进程)之间的制约关系?

当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。

(1).间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享CPU,共享I/O设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程A在使用打印机时,其它线程都要等待。

(2).直接相互制约。这种制约主要是因为线程之间的合作,如有线程A将计算结果提供给线程B作进一步处理,那么线程B在线程A将数据送达之前都将处于阻塞状态。

间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程A和线程B互斥访问某个资源则它们之间就会产个顺序问题——要么线程A等待线程B操作完毕,要么线程B等待线程操作完毕,这其实就是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步。

  • 临界资源和临界区

在一段时间内只允许一个线程访问的资源就称为临界资源或独占资源,计算机中大多数物理设备,进程中的共享变量等待都是临界资源,它们要求被互斥的访问。每个进程中访问临界资源的代码称为临界区。

总结

关键段(CS)与互斥量(Mutex):都有“线程所有权”

-- 创建或初始化 销毁 进入互斥区域 离开互斥区域
关键段 InitializeCriticalSection DeleteCriticalSection EnterCriticalSection LeaveCriticalSection
互斥量 CreateMutex CloseHandle WaitForSingleObject ReleaseMutex

事件(Event)

-- 创建 销毁 使事件触发 使事件未触发
事件 CreateEvent CloseHandle SetEvent ResetEvent

信号量(Semaphore)

-- 创建 销毁 递减计数 递增计数
信号量 CreateSemaphore CloseHandle WaitForSingleObject ReleaseSemaphore

信号量在计数大于0时表示触发状态,调用WaitForSingleObject不会阻塞,等于0表示未触发状态,调用WaitForSingleObject会阻塞直到有其它线程递增了计数。

互斥量,事件,信号量都是内核对象,可以跨进程使用(通过OpenMutex,OpenEvent,OpenSemaphore)。只有互斥量能解决“遗弃”情况。

你可能感兴趣的:(多线程基础)