程序描述:
主线程启动10个子线程并将表示子线程序号的变量地址作为参数传递给子线程。子线程接收参数 -> sleep(50) -> 全局变量++ -> sleep(0) -> 输出参数和全局变量。
要求:
1.子线程输出的线程序号不能重复。
2.全局变量的输出必须递增。
//经典线程同步互斥问题
#include <stdio.h>
#include <process.h>
#include <Windows.h>
long g_nNum;//全局资源
unsigned int __stdcall Fun(void *pPM);//线程函数
const int THREAD_NUM = 10;//子线程个数
int main()
{
g_nNum = 0;
HANDLE handle[THREAD_NUM];
int i = 0;
while(i < THREAD_NUM)
{
handle[i] = (HANDLE)_beginthreadex(NULL,0,Fun,&i,0,NULL);
i++;//等子线程接收到参数时主线程可能改变了这个i值
}
//保证子线程已全部运行结束
WaitForMultipleObjects(THREAD_NUM,handle,TRUE,INFINITE);
return 0;
}
unsigned int _stdcall Fun(void *pPM)
{
//由于创建线程是要一定开销的,所以新线程并不能第一时间执行到这
int nThreadNum = *(int *)pPM;//子线程获取参数
Sleep(50);
g_nNum++;//处理全局资源
Sleep(0);
printf("线程编号为%d 全局资源值为%d\n",nThreadNum,g_nNum);
return 0;
}
执行结果:
//经典线程同步互斥问题(关键段=临界区=critical section)
#include <stdio.h>//C标准输入输出
#include <process.h>
#include <Windows.h>
long g_nNum;//全局资源
unsigned int __stdcall Fun(void *pPM);//线程函数
const int THREAD_NUM = 10;//子线程个数
//临界区变量声明
CRITICAL_SECTION g_csThreadParameter,g_csThreadCode;
int main()
{
printf("经典线程同步 关键段");
//临界区初始化
InitializeCriticalSection(&g_csThreadParameter);
InitializeCriticalSection(&g_csThreadCode);
g_nNum = 0;
HANDLE handle[THREAD_NUM];
int i = 0;
while(i < THREAD_NUM)
{
EnterCriticalSection(&g_csThreadParameter);//进入子线程序号临界区
handle[i] = (HANDLE)_beginthreadex(NULL,0,Fun,&i,0,NULL);
i++;//等子线程接收到参数时主线程可能改变了这个i值
}
//保证子线程已全部运行结束
WaitForMultipleObjects(THREAD_NUM,handle,TRUE,INFINITE);
DeleteCriticalSection(&g_csThreadCode);
DeleteCriticalSection(&g_csThreadParameter);
return 0;
}
unsigned int _stdcall Fun(void *pPM)
{
//由于创建线程是要一定开销的,所以新线程并不能第一时间执行到这
int nThreadNum = *(int *)pPM;//子线程获取参数
LeaveCriticalSection(&g_csThreadParameter);//离开子线程序号临界区
Sleep(50);
EnterCriticalSection(&g_csThreadCode);//进入各子线程互斥区域
g_nNum++;//处理全局资源
Sleep(0);
printf("线程编号为%d 全局资源值为%d\n",nThreadNum,g_nNum);
LeaveCriticalSection(&g_csThreadCode);//离开各子线程互斥区
return 0;
}
运行结果
各子线程已经可以互斥的访问与输出全局资源了,但主线程与子线程之间的同步还是有点问题。
EnterCriticalSection(&g_csThreadParameter);//进入子线程序号临界区
主线程和子线程没能同步:因为主线程能多次进入这个关键区域!
临界区会记录拥有该临界区的线程句柄(指针),临界区是有“线程所有权”概念的
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;//调试用
LONGLockCount;//n表示有n个线程在等待,初始化为-1
LONGRecursionCount;//该关键段拥有线程对此资源获取关键段的次数
HANDLEOwningThread; //即拥有该关键段的线程句柄 from the thread's ClientId->UniqueThread
HANDLELockSemaphore;//自复位事件
DWORDSpinCount;//旋转锁的设置,单CPU下忽略
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
因此可以将临界区比作旅馆的房卡,调用EnterCriticalSection()即申请房卡,得到房卡后自己当然是可以多次进出房间的,在你调用LeaveCriticalSection()交出房卡之前,别人自然是无法进入该房间。
回到这个经典线程同步问题上,主线程正是由于拥有“线程所有权”即房卡,所以它可以重复进入关键代码区域从而导致子线程在接收参数之前主线程就已经修改了这个参数。所以关键段可以用于线程间的互斥,但不可以用于同步。
在经典多线程问题中设置一个事件和一个临界区。用事件处理主线程与子线程的同步,用临界区来处理各子线程间的互斥。
//经典线程同步互斥问题
#include <stdio.h>
#include <process.h>
#include <Windows.h>
long g_nNum;//全局资源
unsigned int __stdcall Fun(void *pPM);//线程函数
const int THREAD_NUM = 10;//子线程个数
//关键段和事件声明
HANDLE g_hThreadEvent;
CRITICAL_SECTION g_csThreadCode;
int main()
{
printf("经典线程同步 事件关键段");
//事件和关键段初始化 自动置位,初始无触发的匿名事件
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++;//等子线程接收到参数时主线程可能改变了这个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);
EnterCriticalSection(&g_csThreadCode);//进入各子线程互斥区域
g_nNum++;//处理全局资源
Sleep(0);
printf("线程编号为%d 全局资源值为%d\n",nThreadNum,g_nNum);
LeaveCriticalSection(&g_csThreadCode);//离开各子线程互斥区
return 0;
}
说明主线程与子线程达到了同步。
最后总结下事件Event
1.事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。
2.事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。
3.事件可以解决线程间同步问题,因此也能解决互斥问题。
互斥量
//经典线程同步互斥问题
#include <stdio.h>
#include <process.h>
#include <Windows.h>
long g_nNum;//全局资源
unsigned int __stdcall Fun(void *pPM);//线程函数
const int THREAD_NUM = 10;//子线程个数
//关键段和互斥量
HANDLE g_hThreadParameter;
CRITICAL_SECTION g_csThreadCode;
int main()
{
printf("经典线程同步 事件关键段");
//互斥两和关键段初始化 第二个参数为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++;//等子线程接收到参数时主线程可能改变了这个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);
EnterCriticalSection(&g_csThreadCode);//进入各子线程互斥区域
g_nNum++;//处理全局资源
Sleep(0);
printf("线程编号为%d 全局资源值为%d\n",nThreadNum,g_nNum);
LeaveCriticalSection(&g_csThreadCode);//离开各子线程互斥区
return 0;
}
与关键段类似,互斥量也是不能解决线程间的同步问题。
最后总结下互斥量Mutex:
1.互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。
2.互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。
信号量Semaphore
由于信号量是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。