多线程(三)多线程同步_基本介绍及mutex互斥体
同步进制的引入为了解决以下三个主要问题:
1.控制多个线程之间对共享资源访问,保证共享资源的完整性
例如:线程A对共享资源进行写入,线程B读取共享资源
2.确保多个线程之间的动作以指定的次序发生
例如:线程B以线程A结束为条件进行触发运行
3.控制共享资源的最大访问数量
例如:有10个线程需要访问共享资源,同时只允许5个线程访问,那剩余线程就需要放入队列中进行等待
同步对象分类:
用户模式下的同步对象:例如关键段等
优点:速度快
缺点:不能跨进程,容易引起死锁
内核模式下的同步对象:例如互斥量,信号量,事件等
优点:跨进程,安全性高
缺点:速度慢(需要从当前用户模式下切换到内核模式,需要消耗时间)
等待函数(同步函数)
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
1.hHandle 对象句柄,如Event(事件)、Mutex(互斥锁)、Semaphore(信号)、Thread(线程)、process(进程)等等内核对象句柄
2.dwMilliseconds 等待时间间隔,毫秒
如果dwMilliseconds为非零值,函数会处于等待状态直到hHandle标志的对象成为触发状态,或者时间超时,函数才会返回
如果dwMilliseconds为0值,不论hHandle标志的对象是否是触发状态,都会立即返回
如果dwMilliseconds为INFINITE,函数会无限等待直到hHandle标志的对象成为触发状态后才会返回
返回值:
WAIT_OBJECT_0 对象是触发状态
WAIT_TIMEOUT 等待超时,等待时间内对象一直处于未触发状态
WAIT_ABANDONED 拥有mutex的线程在结束时没有释放对象
WAIT_FAILED 传递了一个无效句柄,出现错误,可通过GetLastError得到错误代码
注意:
1.调用等待函数时,会挂起当前代码所处的线程,直到函数返回时线程才会恢复执行.避免在窗口线程中调用此函数,因为一旦调用,窗口线程被挂起,函数没有返回前整个窗口都是处于假死状态。
2.当第一个参数是一个mutex对象时,有以下三种情况:
一.如果mutex对象存在拥有者并且不是当前线程,就会根据第二个参数进行等待.根据返回值如果为WAIT_OBJECT_0代表当前线程获得对象拥有权。如果为WAIT_TIMEOUT代表超时未获得对象拥有权。如果为WAIT_ABANDONED代表拥有mutex的线程在结束时没有释放对象,此时无法获知通过mutex所保护的共享资源是否被破坏.
二.如果mutex对象不存在拥有者,当前线程将会自动成为新的拥有者,返回WAIT_OBJECT_0
三.如果mutex对象拥有者是当前线程,也是直接返回WAIT_OBJECT_0
每次调用WaitForSingleObject,如果返回值为WAIT_OBJECT_0,mutex对象内部的递归计数器会+1,后面一定要使用ReleaseMutex进行释放使递归计数器-1.
演示:检测某个进程是否结束
1 //不进行等待,只获取进程是否有信号 2 //注意:进程启动后是无信号状态,只有当点击关闭按钮进程退出时才会变为有信号状态 3 DWORD dwCode = WaitForSingleObject(hProcess,0); 4 switch (dwCode) 5 { 6 case WAIT_OBJECT_0: //有信号状态,表示进程己退出 7 printf("Process is Exit!\r\n"); 8 break; 9 case WAIT_TIMEOUT: //无信号状态,表示进程还在运行中 10 printf("Process is Running\r\n"); 11 break; 12 case WAIT_FAILED: //错误 13 { 14 DWORD dwError = GetLastError(); 15 printf("Fail:%d\r\n",dwError); 16 } 17 default: 18 break; 19 } 20 CloseHandle(hProcess); 21 22 getchar(); 23 return 0;
mutex互斥对象:
它是系统内核中的一种数据结构,用于确保一个线程独占一个资源的访问.
包含三个部分:
使用计数器 统计有多少个线程在调用用该对象
线程ID 当前占用这个互斥对象的线程ID.线程ID为0,代表没有被任何线程占用,处于触发状态.反之线程ID不为0,代表被一个线程占用,处于未触发状态
递归计数器 统计当前占用这个互斥对象的线程占用了该互斥对象的次数
创建互斥体对象
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,BOOL bInitialOwner,LPCTSTR lpName)
1.lpMutexAttributes 指向安全属性指针,通常为NULL
2.bInitialOwner 初始化互斥对象所有者,通常为FASLE,也就是没有所有者
3.lpName 指定互斥体对象的名字
返回值:
成功返回新互斥体对象句柄,失败返回0
注意:如果己经有同名的互斥体对象存在,函数将返回己有的互体斥对象句柄,不会再创建新的互斥体对象
释放互斥体对象
BOOL ReleaseMutex(HANDLE hMutex)
1.hMutex 互斥体对象句柄
返回值:
成功返回真,失败返回假
当线程中调用ReleaseMutex时,该函数会判断当前线程ID与互斥体对象的线程ID是否相同?如果相同,互斥体对象的递归计数器会-1,如果不相同不进行任何操作,直接返回FASLE,此时调用GetLastErro,将返回ERROR_NOT_OWNER(试图释放不是调用者拥有的互斥对象)
每调用一次该函数会使对象的占用计数器-1.如果线程调用等待函数返回WAIT_OBJECT_0不止一次,那ReleaseMutex就得调用相同次数,才能使占用计数器变为0.当占用计数器变为0时,系统内部会把该互斥体对象的线程ID设为0,也就是失去拥有者,此时该互斥对象成为可触发状态.
注意:如果一个线程是互斥体对象的拥有者,但是没有释放互斥对象就结束了线程,此后系统在检则互斥体对象的线程ID时发现此线程己结束,系统将自动把互斥对象的ID复置为0,并将它的递归计数器复置为0.然后从等待线程中寻找一个线程做为它的新的拥有者,并将递归计数器设置为1,同时这个等待线程变为可调度状态
使用互斥体对象配合等待函数实现同步的流程:
假设有一个全局的共享资源,同时有多个线程进行读写。为了保证共享资源的完整性,同一时刻只允许一个线程读写操作
1.创建一个互斥体对象,第二个参数为0,代表该互斥体对象没有所有者,保存返回的对象句柄供多个线程访问
2.多个线程内部在访问共享资源前,先调用WaitForSingleObject传入保存的互斥体对象句柄,然后再访问共享资源,最后再ReleaseMutex.
这样当有一个线程A执行WaitForSingleObject时,会获得互斥体对象所有权,此时互斥体对象的线程ID将被修改为线程A的ID,然后递归计数器为1,使用计数器为1,然后线程A独占访问共享资源。在没有调用ReleaseMutex前,后面调用WaitForSingleObject的线程由于对象是未触发状态,最终都将进入等待状态.直到线程A调用ReleaseMutex使递归计数器-1后变为0,此时系统会把线程ID设为0,交出所有权,该对象又成为触发状态,下一个线程才能获得所有权,独占访问共享资源。
编写一个Demo用于演示mutex互斥体基本操作
功能介绍:
模拟售票系统,有三个售票窗口(三个线程),每隔1秒售出1张票,售票前提示剩余总票数,以及提示售出1张票。当剩余总票售为0时,提示售票窗口关闭(线程结束)
开始编写代码:
1. 创建个基于对话框的工程MutexDemo
2. 添加一个编辑框用于显示售票信息,修改ID为IDC_EDIT_SHOWINFO, 修改属性为不可读.
3.添加一个按钮用于启动线程,修改ID为IDC_BTN_SELLTICKET
4. 先定义相关全局变量和线程函数前置声明
1 int g_nTickNum = 10; //总票数 2 CEdit* g_editShowInfo; //编辑框控件指针 3 HANDLE g_hMutex; //互斥体对象句柄 4 5 //线程函数前置声明 6 DWORD WINAPI ThreadSellTicket_One(LPVOID lpParam); 7 DWORD WINAPI ThreadSellTicket_Two(LPVOID lpParam); 8 DWORD WINAPI ThreadSellTicket_Three(LPVOID lpParam);
5.OnInitDialog中添加相应代码
1 BOOL CMutexDemoDlg::OnInitDialog() 2 { 3 CDialogEx::OnInitDialog(); 4 5 // 设置此对话框的图标。当应用程序主窗口不是对话框时,框架将自动 6 // 执行此操作 7 SetIcon(m_hIcon, TRUE); // 设置大图标 8 SetIcon(m_hIcon, FALSE); // 设置小图标 9 10 //创建mutex互斥体对象 11 g_hMutex = CreateMutex(NULL,FALSE,0); 12 //获取EDIT控件指针,供线程内部访问 13 g_editShowInfo = (CEdit*)GetDlgItem(IDC_EDIT_SHOWINFO); 14 15 16 return TRUE; // 除非将焦点设置到控件,否则返回 TRUE 17 }
6.按钮_启动线程事件
1 //按钮_开始售票 2 void CMutexDemoDlg::OnBnClickedBtnSellticket() 3 { 4 //创建三个售票窗口线程,创建后直接关闭句柄 5 CloseHandle(CreateThread(NULL,0,ThreadSellTicket_One,NULL,0,NULL)); 6 CloseHandle(CreateThread(NULL,0,ThreadSellTicket_Two,NULL,0,NULL)); 7 CloseHandle(CreateThread(NULL,0,ThreadSellTicket_Three,NULL,0,NULL)); 8 }
7.三个售票窗口线程代码
1 DWORD WINAPI ThreadSellTicket_One(LPVOID lpParam) 2 { 3 CString strEdit; 4 CString strNew; 5 while (true) 6 { 7 Sleep(1000); 8 WaitForSingleObject(g_hMutex,INFINITE); 9 g_editShowInfo->GetWindowText(strEdit); 10 if(g_nTickNum > 0) 11 { 12 strNew.Format(_T("线程1:剩余票数:%d,售出1张票\r\n"),g_nTickNum--); 13 strEdit += strNew; 14 if(g_nTickNum == 0) 15 { 16 strEdit += _T("线程1:票己售空,关闭售票窗口\r\n"); 17 g_editShowInfo->SetWindowText(strEdit); 18 ReleaseMutex(g_hMutex); 19 break; 20 } 21 else 22 { 23 g_editShowInfo->SetWindowText(strEdit); 24 ReleaseMutex(g_hMutex); 25 } 26 } 27 else 28 { 29 strEdit += _T("线程1:票己售空,关闭售票窗口\r\n"); 30 g_editShowInfo->SetWindowTextW(strEdit); 31 ReleaseMutex(g_hMutex); 32 break; 33 } 34 } 35 return true; 36 }
1 DWORD WINAPI ThreadSellTicket_Two(LPVOID lpParam) 2 { 3 CString strEdit; 4 CString strNew; 5 while (true) 6 { 7 Sleep(1000); 8 WaitForSingleObject(g_hMutex,INFINITE); 9 g_editShowInfo->GetWindowText(strEdit); 10 if(g_nTickNum > 0) 11 { 12 strNew.Format(_T("线程2:剩余票数:%d,售出1张票\r\n"),g_nTickNum--); 13 strEdit += strNew; 14 if(g_nTickNum == 0) 15 { 16 strEdit += _T("线程2:票己售空,关闭售票窗口\r\n"); 17 g_editShowInfo->SetWindowText(strEdit); 18 ReleaseMutex(g_hMutex); 19 break; 20 } 21 else 22 { 23 g_editShowInfo->SetWindowText(strEdit); 24 ReleaseMutex(g_hMutex); 25 } 26 } 27 else 28 { 29 strEdit += _T("线程2:票己售空,关闭售票窗口\r\n"); 30 g_editShowInfo->SetWindowTextW(strEdit); 31 ReleaseMutex(g_hMutex); 32 break; 33 } 34 } 35 return true; 36 }
1 DWORD WINAPI ThreadSellTicket_Three(LPVOID lpParam) 2 { 3 CString strEdit; 4 CString strNew; 5 while (true) 6 { 7 Sleep(1000); 8 WaitForSingleObject(g_hMutex,INFINITE); 9 g_editShowInfo->GetWindowText(strEdit); 10 if(g_nTickNum > 0) 11 { 12 strNew.Format(_T("线程3:剩余票数:%d,售出1张票\r\n"),g_nTickNum--); 13 strEdit += strNew; 14 if(g_nTickNum == 0) 15 { 16 strEdit += _T("线程3:票己售空,关闭售票窗口\r\n"); 17 g_editShowInfo->SetWindowText(strEdit); 18 ReleaseMutex(g_hMutex); 19 break; 20 } 21 else 22 { 23 g_editShowInfo->SetWindowText(strEdit); 24 ReleaseMutex(g_hMutex); 25 } 26 } 27 else 28 { 29 strEdit += _T("线程3:票己售空,关闭售票窗口\r\n"); 30 g_editShowInfo->SetWindowTextW(strEdit); 31 ReleaseMutex(g_hMutex); 32 break; 33 } 34 } 35 return true; 36 }
8.DestroyWindow添加相应代码
1 BOOL CMutexDemoDlg::DestroyWindow() 2 { 3 //关闭互斥体对象句柄 4 CloseHandle(g_hMutex); 5 6 return CDialogEx::DestroyWindow(); 7 }