什么是同步:
“同步”不是指平常所说的两件事情同时进行。它的目的是使多个线程之间协调工作,而且常常是避免两个线程同时进行某些操作,比如同时访问同一个共享资源。一般来说,同步是通过暂时将会发生冲突操作的某个线程暂停执行(称为阻塞线程),然后等待不会冲突时再继续执行。
需要同步的情况:
1、多个线程同时访问同一对象时
MFC对象在对象级不是线程安全的,只有在类级才是。如:两个线程可以安全地使用两个不同的CString对象,但同时使用同一个CString对象就可能产生问题。如果必须使用同一个对象,那么应该采取适当的同步化措施。
2、多个线程之间需要协调运行
例如,如果第二个线程需要等待第一个线程完成到某一步时才能运行,那么该线程应该暂时挂起以减少对CPU的占用时间,提高程序的执行效率。当第一个线程完成了相应的步骤后,应该发出某种信号来激活第二个线程。
Windows中的4种线程同步对象:
1、Events(事件)——CEvent
作为标志在线程之间传递信号。简单地说,类似一个布尔型变量的开关作用。
2、Critical Sections(临界段)——CCriticalSection
在进程中作为关键字以获得对共享资源的访问
3、Mutexes(互斥量)——CMutex
与临界段的工作方式相似,只是该对象可以用于多进程中的线程同步,而不是用于单进程中
4、Semaphores(信号量)——CSemaphore
在给定的限制条件下,允许多个进程同时访问共享资源
注意:使用线程同步类,必须在文件中包含afxmt.h头文件。线程同步类的基类是CSyncObject。
事件:
CEvent类的构造函数,第一个参数确定事件对象的起始信号状态,TRUE代表活动(有信号)状态,FALSE代表沉寂(无信号)状态。默认情况下,是自动事件。
第二个参数为FALSE,是自动事件,自动事件在调用SetEvent函数被使用后能够自动恢复为无信号状态;第二个参数为TRUE,是手动事件,手动事件在调用SetEvent函数处于活动状态时,一直出于这个状态,直到程序显式地调用ResetEvent为止。所以,CEvent::ResetEvent函数只用于手动事件。
自动事件:
CEvent类的构造函数,第二个参数为FALSE,是自动事件,自动事件在调用SetEvent函数被使用后能够自动恢复为无信号状态。默认情况下,是自动事件。
1、声明事件为全局对象。
//包含使用线程同步对象所需要的头文件
#include "afxmt.h"
CEvent eventObj; //构造一个自动事件对象
2、在一个线程中使用CSyncObject::Lock函数来等待信号,如果事件无信号,该线程被挂起等待。
//锁住事件
eventObj.Lock(); //等待事件有信号才返回
3、在另外一个线程中使用CEvent::SetEvent来设置适当的事件为有信号状态,此时即可让等待的线程继续执行。
eventObj.SetEvent(); //设置事件变成有信号状态
注意:一般来说,在实际应用中不要轻易阻塞主线程,因为主线程一般是负责和用户进行交互的,阻塞主线程会影响应用程序的交互性。
手动事件:
CEvent类的构造函数,第二个参数为TRUE,是手动事件,手动事件在调用SetEvent函数处于活动状态时,一直出于这个状态,直到程序显式地调用ResetEvent为止。
为了简便起见,后面所有线程函数都通过弹出消息框的形式来告知用户线程的同步情况。同时约定,在视图区单击鼠标左键就会创建一个工作线程。单击鼠标右键则使事件对象处于有信号状态。
自动事件就好比地铁的自动检票口,一张票(SetEvent)只能使一个人(线程)通过;而手动事件则可以理解为路口的红绿灯,红灯表示无信号,所有的人(线程)都必须等待,当变为绿灯时(事件有信号),所有等待的人都可以通过,直到下次变为红灯(调用ResetEvent函数)为止。
临界段:
临界段(Critical Sections)是一种保证在某一时刻只有一个线程能够访问共享数据的简便方法,只能被使用在一个进程内的线程间进行通信。
1、声明临界段为全局对象。
//包含使用线程同步对象所需要的头文件
#include "afxmt.h"
CCriticalSection criticalSection; //构造一个自动事件对象
2、线程想访问共享资源,会调用临界段的成员函数,该函数会将资源关键字交给调用线程(假设其他线程没有拥有该临界段):
criticalSection.Lock(); //试图锁定临界段
如果其他线程已经锁定了该临界段,那么调用线程都会处于阻塞状态,直到该临界段被释放为止。否则,调用线程就会成为该临界段的拥有者,并且能够访问共享资源。
3、调用线程完成对资源的处理后,通过调用下面的函数来释放临界段对象:
criticalSection.Unlock(); //解除临界段
释放的临界段对象就可以被其他线程对象得到,并访问受保护的数据。
多个线程使用临界段访问共享资源就好比人们在火车上去洗手间。每个人代表一个线程,因此,洗手间的门锁就是临界段对象了。每个线程(人)使用共享资源时(洗手间),必须先使用临界段对象(门锁)锁住共享资源。如果共享资源已经被其他线程锁住了,则必须等待,直到临界段对象(门锁)被释放为止。
互斥量:
互斥量(Mutexes)的作用和临界段几乎相同,也用于保证在某一时刻只有一个线程能够访问共享数据,但可以用作不同进程内的线程间通信。不仅能够在同一个应用程序的线程之间实线资源的安全共享,而且可以在不同应用程序的线程之间实线安全的资源共享。
1、在应用程序中创建一个全局互斥量对象:
//包含使用线程同步对象所需要的头文件
#include "afxmt.h"
CMutex mutex(FALSE, "mutex1"); //声明一个名为mutex1的互斥量对象
注意:如果互斥量要被使用在不同应用程序的进程通信,则必须指定一个名字。第一个参数设定互斥量的开始状态是锁定(TRUE)还是未锁定(FALSE)。第二个参数指向互斥量的名称,该名称用于区别系统中不同进程定义的互斥量。
2、在程序即将访问共享资源士,调用对象的Lock成员函数锁定:
mutex.Lock(); //锁定互斥量
3、程序完成了对共享资源的使用后,调用互斥量的Unlock成员函数释放:
mutex.Unlock(); //释放互斥量
信号量:
信号量(Semaphores)的作用是允许一定数目的线程同时访问某个共享资源,也可以用作不同进程内的线程间通信。信号量维护着一个从0开始的计数,在计数值大于0时对象是有信号的,而在计数值为0时则是无信号的。每当有一个新的线程夺得了资源,计数值就减少;而当某线程释放了资源,计数值增加。当计数值为0时,除非有一个线程释放该资源,否则其他线程无法访问该资源。
1、创建一个全局的信号量对象,并设置初始资源数和最大资源数。如果要在进程间使用信号量,那么还要设置信号量名:
//包含使用线程同步对象所需要的头文件
#include "afxmt.h"
CSemaphore semaphore(2, 2, "semaphore1"); //声明一个名为semaphore1的信号量
2、在线程即将访问共享资源时,调用对象的Lock函数:
semaphore.Lock(); //信号量可用资源减1
3、在线程完成对共享资源的使用后,线程应调用信号量对象的Unlock成员函数:
semaphore.Unlock(); //信号量可用资源加1
信号量的例子,如汽车进加油站,每辆汽车就是一个线程,而加油站就是共享资源。但这个加油站只有4个加油点(相当于信号量的计数值),也就是说同时只能有4辆车在加油。如果还有更多的车想加油,就必须排队等候了。
总结:
事件用于线程间传递信号;临界段和互斥量控制在某一时刻只允许一个线程访问共享资源;信号量则控制访问共享资源线程的数目。其中互斥量和信号量可以用于不同进程间的线程同步。