Thursday, March 24, 2005

 

Some notes on Thread - 3

MFC支持的七个多线程的同步类可以分成两大类:同步对象(CsyncObject、Csemaphore、Cmutex、CcriticalSection和Cevent)和同步访问对象(CmultiLock和CsingleLock)。下面的文摘主要论证各自的异同和trick。

I.
4 种进程或线程同步互斥的控制方法
By zhangSichu
From http://www.ASPCool.com

很想整理一下自己对进程线程同步互斥的理解。正巧周六一个刚刚回到学校的同学请客吃饭。在吃饭的过程中,有两个同学,为了一个问题争论的面红耳赤。一个认为.Net下的进程线程控制模型更加合理。一个认为Java下的线程池策略比.Net的好。大家的话题一下转到了进程线程同步互斥的控制问题上。回到家,想了想就写了这个东东。
  现在流行的进程线程同步互斥的控制机制,其实是由最原始最基本的4种方法实现的。由这4种方法组合优化就有了.Net和Java下灵活多变的,编程简便的线程进程控制手段。
  这4种方法具体定义如下 在《操作系统教程》ISBN 7-5053-6193-7 一书中可以找到更加详细的解释
   1临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
   2互斥量:为协调共同对一个共享资源的单独访问而设计的。
   3信号量:为控制一个具有有限数量用户资源而设计。
   4事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

  临界区(Critical Section)
  保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
临界区包含两个操作原语:
  EnterCriticalSection() 进入临界区
  LeaveCriticalSection() 离开临界区
  EnterCriticalSection()语句执行后代码将进入临界区以后无论发生什么,必须确保与之匹配的LeaveCriticalSection()都能够被执行到。否则临界区保护的共享资源将永远不会被释放。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
  MFC提供了很多功能完备的类,我用MFC实现了临界区。MFC为临界区提供有一个CCriticalSection类,使用该类进行线程同步处理是非常简单的。只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护代码片段即可。Lock()后代码用到的资源自动被视为临界区内的资源被保护。UnLock后别的线程才能访问这些资源。

//CriticalSection
CCriticalSection global_CriticalSection;

// 共享资源
char global_Array[256];

//初始化共享资源
void InitializeArray()
{
for(int i = 0;i<256;i++)
{
global_Array[i]=I;
}
}

//写线程
UINT Global_ThreadWrite(LPVOID pParam)
{
CEdit *ptr=(CEdit *)pParam;
ptr->SetWindowText("");
//进入临界区
global_CriticalSection.Lock();
for(int i = 0;i<256;i++)
{
global_Array[i]=W;
ptr->SetWindowText(global_Array);
Sleep(10);
}
//离开临界区
global_CriticalSection.Unlock();
return 0;
}

//删除线程
UINT Global_ThreadDelete(LPVOID pParam)
{
CEdit *ptr=(CEdit *)pParam;
ptr->SetWindowText("");
//进入临界区
global_CriticalSection.Lock();
for(int i = 0;i<256;i++)
{
global_Array[i]=D;
ptr->SetWindowText(global_Array);
Sleep(10);
}
//离开临界区
global_CriticalSection.Unlock();
return 0;
}

//创建线程并启动线程
void CCriticalSectionsDlg::OnBnClickedButtonLock()
{
//Start the first Thread
CWinThread *ptrWrite = AfxBeginThread(Global_ThreadWrite,
&m_Write,
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
ptrWrite->ResumeThread();

//Start the second Thread
CWinThread *ptrDelete = AfxBeginThread(Global_ThreadDelete,
&m_Delete,
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
ptrDelete->ResumeThread();
}

  在测试程序中,Lock UnLock两个按钮分别实现,在有临界区保护共享资源的执行状态,和没有临界区保护共享资源的执行状态。
  程序运行结果
  
  互斥量(Mutex)

  互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。

  互斥量包含的几个操作原语:
  CreateMutex() 创建一个互斥量
  OpenMutex() 打开一个互斥量
  ReleaseMutex() 释放互斥量
  WaitForMultipleObjects() 等待互斥量对象

  同样MFC为互斥量提供有一个CMutex类。使用CMutex类实现互斥量操作非常简单,但是要特别注意对CMutex的构造函数的调用
  CMutex( BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL)
  不用的参数不能乱填,乱填会出现一些意想不到的运行结果。

//创建互斥量
CMutex global_Mutex(0,0,0);

// 共享资源
char global_Array[256];

void InitializeArray()
{
for(int i = 0;i<256;i++)
{
global_Array[i]=I;
}
}
UINT Global_ThreadWrite(LPVOID pParam)
{
CEdit *ptr=(CEdit *)pParam;
ptr->SetWindowText("");
global_Mutex.Lock();
for(int i = 0;i<256;i++)
{
global_Array[i]=W;
ptr->SetWindowText(global_Array);
Sleep(10);
}
global_Mutex.Unlock();
return 0;
}

UINT Global_ThreadDelete(LPVOID pParam)
{
CEdit *ptr=(CEdit *)pParam;
ptr->SetWindowText("");
global_Mutex.Lock();
for(int i = 0;i<256;i++)
{
global_Array[i]=D;
ptr->SetWindowText(global_Array);
Sleep(10);
}
global_Mutex.Unlock();
return 0;
}
  同样在测试程序中,Lock UnLock两个按钮分别实现,在有互斥量保护共享资源的执行状态,和没有互斥量保护共享资源的执行状态。
  程序运行结果
  

  信号量(Semaphores)
  信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。
PV操作及信号量的概念都是由荷兰科学家E.W.Dijkstra提出的。信号量S是一个整数,S大于等于零时代表可供并发进程使用的资源实体数,但S小于零时则表示正在等待使用共享资源的进程数。
P操作 申请资源:
  (1)S减1;
  (2)若S减1后仍大于等于零,则进程继续执行;
  (3)若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。
V操作 释放资源:
  (1)S加1;
  (2)若相加结果大于零,则进程继续执行;
  (3)若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。

  信号量包含的几个操作原语:
  CreateSemaphore() 创建一个信号量
  OpenSemaphore() 打开一个信号量
  ReleaseSemaphore() 释放信号量
  WaitForSingleObject() 等待信号量

//信号量句柄
HANDLE global_Semephore;

// 共享资源
char global_Array[256];
void InitializeArray()
{
for(int i = 0;i<256;i++)
{
global_Array[i]=I;
}
}
//线程1
UINT Global_ThreadOne(LPVOID pParam)
{
CEdit *ptr=(CEdit *)pParam;
ptr->SetWindowText("");
//等待对共享资源请求被通过 等于 P操作
WaitForSingleObject(global_Semephore, INFINITE);
for(int i = 0;i<256;i++)
{
global_Array[i]=O;
ptr->SetWindowText(global_Array);
Sleep(10);
}
//释放共享资源 等于 V操作
ReleaseSemaphore(global_Semephore, 1, NULL);
return 0;
}

UINT Global_ThreadTwo(LPVOID pParam)
{
CEdit *ptr=(CEdit *)pParam;
ptr->SetWindowText("");
WaitForSingleObject(global_Semephore, INFINITE);
for(int i = 0;i<256;i++)
{
global_Array[i]=T;
ptr->SetWindowText(global_Array);
Sleep(10);
}
ReleaseSemaphore(global_Semephore, 1, NULL);
return 0;
}

UINT Global_ThreadThree(LPVOID pParam)
{
CEdit *ptr=(CEdit *)pParam;
ptr->SetWindowText("");
WaitForSingleObject(global_Semephore, INFINITE);
for(int i = 0;i<256;i++)
{
global_Array[i]=H;
ptr->SetWindowText(global_Array);
Sleep(10);
}
ReleaseSemaphore(global_Semephore, 1, NULL);
return 0;
}

void CSemaphoreDlg::OnBnClickedButtonOne()
{
//设置信号量 1 个资源 1同时只可以有一个线程访问
global_Semephore= CreateSemaphore(NULL, 1, 1, NULL);
this->StartThread();
// TODO: Add your control notification handler code here
}

void CSemaphoreDlg::OnBnClickedButtonTwo()
{
//设置信号量 2 个资源 2 同时只可以有两个线程访问
global_Semephore= CreateSemaphore(NULL, 2, 2, NULL);
this->StartThread();
// TODO: Add your control notification handler code here
}

void CSemaphoreDlg::OnBnClickedButtonThree()
{
//设置信号量 3 个资源 3 同时只可以有三个线程访问
global_Semephore= CreateSemaphore(NULL, 3, 3, NULL);
this->StartThread();
// TODO: Add your control notification handler code here
}
  信号量的使用特点使其更适用于对Socket(套接字)程序中线程的同步。例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这时可以为每一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。
  程序运行结果
  

  事件(Event)

  事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。
  信号量包含的几个操作原语:
  CreateEvent() 创建一个信号量
  OpenEvent() 打开一个事件
  SetEvent() 回置事件
  WaitForSingleObject() 等待一个事件
  WaitForMultipleObjects()         等待多个事件
    WaitForMultipleObjects 函数原型:
     WaitForMultipleObjects(
     IN DWORD nCount, // 等待句柄数
     IN CONST HANDLE *lpHandles, //指向句柄数组
     IN BOOL bWaitAll, //是否完全等待标志
     IN DWORD dwMilliseconds //等待时间
     )
  参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回WAIT_TIMEOUT。

//事件数组
HANDLE global_Events[2];

// 共享资源
char global_Array[256];

void InitializeArray()
{
for(int i = 0;i<256;i++)
{
global_Array[i]=I;
}
}

UINT Global_ThreadOne(LPVOID pParam)
{
CEdit *ptr=(CEdit *)pParam;
ptr->SetWindowText("");
for(int i = 0;i<256;i++)
{
global_Array[i]=O;
ptr->SetWindowText(global_Array);
Sleep(10);
}
//回置事件
SetEvent(global_Events[0]);
return 0;
}

UINT Global_ThreadTwo(LPVOID pParam)
{
CEdit *ptr=(CEdit *)pParam;
ptr->SetWindowText("");
for(int i = 0;i<256;i++)
{
global_Array[i]=T;
ptr->SetWindowText(global_Array);
Sleep(10);
}
//回置事件
SetEvent(global_Events[1]);
return 0;
}

UINT Global_ThreadThree(LPVOID pParam)
{
CEdit *ptr=(CEdit *)pParam;
ptr->SetWindowText("");
//等待两个事件都被回置
WaitForMultipleObjects(2, global_Events, true, INFINITE);
for(int i = 0;i<256;i++)
{
global_Array[i]=H;
ptr->SetWindowText(global_Array);
Sleep(10);
}
return 0;
}
void CEventDlg::OnBnClickedButtonStart()
{
for (int i = 0; i < 2; i++)
{
//实例化事件
global_Events[i]=CreateEvent(NULL,false,false,NULL);
}
CWinThread *ptrOne = AfxBeginThread(Global_ThreadOne,
&m_One,
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
ptrOne->ResumeThread();

//Start the second Thread
CWinThread *ptrTwo = AfxBeginThread(Global_ThreadTwo,
&m_Two,
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
ptrTwo->ResumeThread();

//Start the Third Thread
CWinThread *ptrThree = AfxBeginThread(Global_ThreadThree,
&m_Three,
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
ptrThree->ResumeThread();
// TODO: Add your control notification handler code here
}
  事件可以实现不同进程中的线程同步操作,并且可以方便的实现多个线程的优先比较等待操作,例如写多个WaitForSingleObject来代替WaitForMultipleObjects从而使编程更加灵活。
  程序运行结果
  
  总结:
  1. 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。
  2. 互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。
  3. 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。
  疑问:
  在 Linux 上,有两类信号量。第一类是由 semget/semop/semctl API 定义的信号量的 SVR4(System V Release 4)版本。第二类是由 sem_init/sem_wait/sem_post/interfaces 定义的 POSIX 接口。 它们具有相同的功能,但接口不同。 在2.4.x内核中,信号量数据结构定义为(include/asm/semaphore.h)。
但是在Linux中没有对互斥量的具体提法,只是看到说互斥量是信号量的一种特殊情况,当信号量的最大资源数=1同时可以访问共享资源的线程数=1 就是互斥量了。临界区的定义也比较模糊。没有找到用事件处理线程/进程同步互斥的操作的相关资料。在Linux下用GCC/G++编译标准C++代码,信号量的操作几乎和Windows下VC7的编程一样,不用改多少就顺利移植了,可是互斥量,事件,临界区的Linux移植没有成功。

II.
有关多线程的一些技术问题 (mutex,semaphore,event,critical section)

By Robin

1、 何时使用多线程?
2、 线程如何同步?
3、 线程之间如何通讯?
4、 进程之间如何通讯?

先来回答第一个问题,线程实际主要应用于四个主要领域,当然各个领域之间不是绝对孤立的,他们有可能是重叠的,但是每个程序应该都可以归于某个领域:

1、 offloading time-consuming task。由辅助线程来执行耗时计算,而使GUI有更好的反应。我想这应该是我们考虑使用线程最多的一种情况吧。

2、 Scalability。服务器软件最常考虑的问题,在程序中产生多个线程,每个线程做一份小的工作,使每个CPU都忙碌,使CPU(一般是多个)有最佳的使用率,达到负载的均衡,这比较复杂,我想以后再讨论这个问题。

3、 Fair-share resource allocation。当你向一个负荷沉重的服务器发出请求,多少时间才能获得服务。一个服务器不能同时为太多的请求服务,必须有一个请求的最大个数,而且有时候对某些请求要优先处理,这是线程优先级干的活了。

4、 Simulations。线程用于仿真测试。

我把主要的目光放在第一个领域,因为它正是我想要的。第二和第三个领域比较有意思,但是目前不在我的研究时间表中。

线程的同步机制:

1、 Event
用事件(Event)来同步线程是最具弹性的了。一个事件有两种状态:激发状态和未激发状态。也称有信号状态和无信号状态。事件又分两种类型:手动重置事件和自动重置事件。手动重置事件被设置为激发状态后,会唤醒所有等待的线程,而且一直保持为激发状态,直到程序重新把它设置为未激发状态。自动重置事件被设置为激发状态后,会唤醒“一个”等待中的线程,然后自动恢复为未激发状态。所以用自动重置事件来同步两个线程比较理想。MFC中对应的类为CEvent.。CEvent的构造函数默认创建一个自动重置的事件,而且处于未激发状态。共有三个函数来改变事件的状态:SetEvent,ResetEvent和PulseEvent。用事件来同步线程是一种比较理想的做法,但在实际的使用过程中要注意的是,对自动重置事件调用SetEvent和PulseEvent有可能会引起死锁,必须小心。

2、 Critical Section
使用临界区域的第一个忠告就是不要长时间锁住一份资源。这里的长时间是相对的,视不同程序而定。对一些控制软件来说,可能是数毫秒,但是对另外一些程序来说,可以长达数分钟。但进入临界区后必须尽快地离开,释放资源。如果不释放的话,会如何?答案是不会怎样。如果是主线程(GUI线程)要进入一个没有被释放的临界区,呵呵,程序就会挂了!临界区域的一个缺点就是:Critical Section不是一个核心对象,无法获知进入临界区的线程是生是死,如果进入临界区的线程挂了,没有释放临界资源,系统无法获知,而且没有办法释放该临界资源。这个缺点在互斥器(Mutex)中得到了弥补。Critical Section在MFC中的相应实现类是CcriticalSection。CcriticalSection::Lock()进入临界区,CcriticalSection::UnLock()离开临界区。

3、 Mutex
互斥器的功能和临界区域很相似。区别是:Mutex所花费的时间比Critical Section多的多,但是Mutex是核心对象(Event、Semaphore也是),可以跨进程使用,而且等待一个被锁住的Mutex可以设定TIMEOUT,不会像Critical Section那样无法得知临界区域的情况,而一直死等。MFC中的对应类为CMutex。Win32函数有:创建互斥体CreateMutex() ,打开互斥体OpenMutex(),释放互斥体ReleaseMutex()。Mutex的拥有权并非属于那个产生它的线程,而是最后那个对此Mutex进行等待操作(WaitForSingleObject等等)并且尚未进行ReleaseMutex()操作的线程。线程拥有Mutex就好像进入Critical Section一样,一次只能有一个线程拥有该Mutex。如果一个拥有Mutex的线程在返回之前没有调用ReleaseMutex(),那么这个Mutex就被舍弃了,但是当其他线程等待(WaitForSingleObject等)这个Mutex时,仍能返回,并得到一个WAIT_ABANDONED_0返回值。能够知道一个Mutex被舍弃是Mutex特有的。

4、 Semaphore
信号量是最具历史的同步机制。信号量是解决producer/consumer问题的关键要素。对应的MFC类是Csemaphore。Win32函数CreateSemaphore()用来产生信号量。ReleaseSemaphore()用来解除锁定。Semaphore的现值代表的意义是目前可用的资源数,如果Semaphore的现值为1,表示还有一个锁定动作可以成功。如果现值为5,就表示还有五个锁定动作可以成功。当调用Wait…等函数要求锁定,如果Semaphore现值不为0,Wait…马上返回,资源数减1。当调用ReleaseSemaphore()资源数加1,当时不会超过初始设定的资源总数。

线程之间的通讯:
线程常常要将数据传递给另外一个线程。Worker线程可能需要告诉别人说它的工作完成了,GUI线程则可能需要交给Worker线程一件新的工作。
通过PostThreadMessage(),可以将消息传递给目标线程,当然目标线程必须有消息队列。以消息当作通讯方式,比起标准技术如使用全局变量等,有很大的好处。如果对象是同一进程中的线程,可以发送自定义消息,传递数据给目标线程,如果是线程在不同的进程中,就涉及进程之间的通讯了。下面将会讲到。

进程之间的通讯:

当线程分属于不同进程,也就是分驻在不同的地址空间时,它们之间的通讯需要跨越地址空间的边界,便得采取一些与同一进程中不同线程间通讯不同的方法。

1、 Windows专门定义了一个消息:WM_COPYDATA,用来在线程之间搬移数据,――不管两个线程是否同属于一个进程。同时接受这个消息的线程必须有一个窗口,即必须是UI线程。WM_COPYDATA必须由SendMessage()来发送,不能由PostMessage()等来发送,这是由待发送数据缓冲区的生命期决定的,出于安全的需要。
2、 WM_COPYDATA效率上面不是太高,如果要求高效率,可以考虑使用共享内存(Shared Memory)。使用共享内存要做的是:设定一块内存共享区域;使用共享内存;同步处理共享内存。
第一步:设定一块内存共享区域。首先,CreateFileMapping()产生一个file-mapping核心对象,并指定共享区域的大小。MapViewOfFile()获得一个指针指向可用的内存。如果是C/S模式,由Server端来产生file-mapping,那么Client端使用OpenFileMapping(),然后调用MapViewOfFile()。
第二步:使用共享内存。共享内存指针的使用是一件比较麻烦的事,我们需要借助_based属性,允许指针被定义为从某一点开始起算的32位偏移值。
第三步:清理。UnmapViewOfFile()交出由MapViewOfFile()获得的指针,CloseHandle()交出file-mapping核心对象的handle。
第四步:同步处理。可以借助Mutex来进行同步处理。
3、 IPC
1)Anonymous Pipes。Anonymous Pipes只被使用于点对点通讯。当一个进程产生另一个进程时,这是最有用的一种通讯方式。
2)Named Pipes。Named Pipes可以是单向,也可以是双向,并且可以跨越网络,不局限于单机。
3)Mailslots。Mailslots为广播式通讯。Server进程可以产生Mailslots,任何Client进程可以写数据进去,但是只有Server进程可以取数据。
4)OLE Automation。OLE Automation和UDP都是更高阶的机制,允许通讯发生于不同进程间,甚至不同机器间。
5)DDE。DDE动态数据交换,使用于16位Windows,目前这一方式应尽量避免使用。

III.
多线程程序设计的相关问题 (节选)

作者:softhead
出处:vchelp.com

发表于 2004-1-8 16:11:56 [50分]
--------------------------------------------------------------------------------

多线程程序设计的相关问题
一、 什么是进程?什么是线程?
进程是一大堆系统对象拥有权的集合。如进程拥有内存上下文,文件句柄,可以派生出很多线程,也可以拥有很多DLL模块。在windows系统中,进程并不完成实质的工作,只是提供一个相对独立的运行环境,线程才是完成实际工作的载体。线程从属于进程,共享进程所拥有的系统对象。线程是操作系统调度的单位。实质上,线程就是一段可执行代码。
采用多进程的优点和缺点:
优点:运行环境相对独立,某一进程的崩溃一般不会影响到其它进程的执行。
缺点:
耗时耗资源:启动一个进程需要申请大量的系统资源,其中包括虚拟内存、文件句柄以及加载各种必要的动态链接库;线程则不需要以上动作,因为它共享进程中的所有资源。
“系统准备一个进程环境可能需要好几M的空间”
通信复杂:进程的地址空间独立,进程A的地址X,在进程B中可能是无意义的,这样,当进程间需要共享数据时,就需要特殊的机制来完成这些工作。线程则在同一地址空间,数据共享方便快捷。“线程是一个物美价廉的选择,在一个Windows上拥有500个线程是一件很轻易的事情,但是500个进程将是难以想象的”。

二、 为什么需要多线程(解释何时考虑使用线程)
从用户的角度考虑,就是为了得到更好的系统服务;从程序自身的角度考虑,就是使目标任务能够尽可能快的完成,更有效的利用系统资源。综合考虑,一般以下场合需要使用多线程:
1、 程序包含复杂的计算任务时
主要是利用多线程获取更多的CPU时间(资源)。
2、 处理速度较慢的外围设备
比如:打印时。再比如网络程序,涉及数据包的收发,时间因素不定。使用独立的线程处理这些任务,可使程序无需专门等待结果。
3、 程序设计自身的需要
WINDOWS系统是基于消息循环的抢占式多任务系统,为使消息循环系统不至于阻塞,程序需要多个线程的来共同完成某些任务。
三、 使用多线程可能出现的问题(列举问题)
事实上,单纯的使用线程不会产生任何问题,其启动、运行和结束都是非常简单的事情。在Win32环境下,启动:CreateThread,运行就是函数执行的过程,中止就是函数返回的过程或者调用ExitThread。但是由于下列原因可能会使在使用线程的过程中带来一系列问题:
1、 版本问题
多任务的概念是随着实际需求的提出而产生,最初的程序设计者并没有考虑到代码需要在多线程环境下运行,在多线程环境下使用这些代码无疑将产生访问冲突。最典型的例子就是C runtime library。最早的C runtime library产生于20世纪70年代,当时连多任务都是一个新奇的概念,更别说什么多线程了,该版本的库中使用了大量全局变量和静态变量(产生竞争条件的根源,对局部变量无此要求,因为局部变量都使用栈,每个线程都有自己的栈空间,另外在启动线程时,给线程函数的参数应该是尽量使用值,而非指针或引用,这样可以避免因此带来的冲突问题),如在该库中统一使用一个errno变量来表明程序的错误码,如果在多线程中使用该库,并且都需要设置错误码时,此时即产生了一个冲突。
VC为防止以上问题,提供了另外一个线程安全的C runtime library,因此在写多线程程序时,需要注意所连接库的版本是否正确(该过程一般由应用程序向导完成,因此平时编程并无此问题)。与此有关的还有一些其它版本:单线程版、多线程版调试版和多线程发行版。
2、 线程间共享资源时形成竞争条件(race condition)
一般而言,线程并不是单独行动,通常是多个线程分工协作,完成一个大任务中的不同小任务,此时,这些线程之间就需要共同操作一些资源,比较典型的例子是多个线程进行文件操作或屏幕打印的情况:线程A在写文件进行了一半时,发生了context switch,另外一个线程B继续进行写文件操作,此时文件的内容将会凌乱不堪。甚至造成异常错误。典型的例子是,三个线程,线程A在堆中申请了一块内存并填入了一个值,线程B读取了该值后将该内存释放,如果线程C还要对该内存操作时,将导致异常。
3、 线程间的通信问题
线程协作完成某一任务时,有时还需要通信以控制任务的执行步骤,典型的例子就是读写者线程:写线程在对某内存区域写完数据后,需要通知读线程来取,读完之后又需要通知写线程可以继续往里写入数据。更为广泛的例子是:某线程需要等待某一事件发生,以决定是否继续工作。此时,如果没有正确控制线程的执行过程,将导致不可预料的错误发生。
4、 由于不规范的使用线程导致系统效率下降
进程中包含了一个以上的线程,这些线程可能会动态的申请某些资源,如某些数据库线程可能会动态加载数据库方面的动态链接库,但是在该线程结束时,并没有及时释放该动态链接库即被其他线程强行终止,于是该进程中的该动态链接库引用计数不为0,从而导致该动态链接库在该进程中存有一个副本。当这种情况频繁时,将对系统效率产生很大的影响。
四、 线程的类型(解释UI线程和WORKER线程的区别和联系)
严格说来,线程并没有什么本质区别,但是Win32编程文档中却反复强调UI线程和Worker线程的区别。并给出了它们的定义:
UI线程就是:拥有消息队列和窗口的线程,并且它的主要职责是处理窗口消息。Worker线程则没有消息队列,但是当Worker线程产生一个用户界面(消息框和模式对话框除外)时,则该线程则摇身一变,成为UI线程。
问题:
1、 线程的消息队列和窗口的消息队列
在Win32中,每个线程都有它自己专属的消息队列,而窗口并不总是有消息队列,因为一个UI线程可以创建很多个窗口。
2、 UI线程到底跟Worker线程存在什么差别?
职责不一样:UI线程负责处理与用户界面有关的消息,一般而言,用户界面消息来自用户输入(如鼠标键盘消息)、系统消息(如WM_PAINT)以及程序产生的用户自定义消息。因此,在该线程下一般不能存在等待(wait…)函数,这样该线程就会挂起,从而影响消息队列的处理。Worker线程不用处理用户界面消息,而是完成一般性的计算任务,该线程等待计算过程中必要的资源时,不会影响到界面的刷新动作。
操作系统的管理不一样:对UI线程来说,产生一个UI线程实际上产生了两个线程,一个是其自身,另一个是操作系统为响应其GDI调用而产生的影子线程。
3、 Worker线程变成UI线程有什么不好?
Worker线程一般用于计算,此时如果它转换为UI线程的话,将无暇顾及用户界面的消息响应。
4、 Worker线程可否拥有自己的消息队列?
Worker线程同样可以拥有自己的消息队列,该队列一般通过PeekMessage()调用建立,通过GetMessage调用来解析。(具体实现看源码)
5、 用以下规则来管理win32中线程、消息和窗口的互动
所有传送给某一窗口的消息,将由产生该窗口的线程负责处理。
五、 线程的启动和中止(解释启动线程的不同方式及其它们的区别和实用场合)
随C Runtime Library库的更新和编程环境的不同,线程的启动方式也有所不同,以下介绍几种典型的线程启动方式。
1、_beginthread和_endthread
该函数是C Runtime Library中的函数,它负责初始化函数库;其原型如下unsigned long _beginthread( void( __cdecl *start_address )( void * ), unsigned stack_size, void *arglist );“该函数被认为是头脑简单的函数”,使用该函数导致无法有效的控制被创建线程,如不能在启动时将该线程挂起,无法为该线程设置优先权等。另外,该函数为隐藏Win32的实现细节,启动线程的第一件事情即将自己的Handle关闭,因此也就无法利用这个Handle来等待该线程结束等操作。该函数是早期的C Runtime Library的产物,不提倡使用,后期的改良版本为_beginthreadex。
通过_beginthread启动的线程在应当通过调用_endthread结束,以保证清除与线程相关的资源。
2、_beginthreadex和_endthreadex
该函数是C Runtime Library中的一个函数,用标准C实现,相比_beginthread,_beginthreadex对线程控制更为有力(比前者多三个参数),是_beginthread的加强版。其原型为unsigned long _beginthreadex( void *security, unsigned stack_size, unsigned ( __stdcall *start_address )( void * ), void *arglist, unsigned initflag, unsigned *thrdaddr );该函数返回新线程的句柄,通过该句柄可实现对线程的控制。虽然,该函数是用标准C写的(即可不加修改就可以移植到其他系统执行),但是由于它与Windows系统有着紧密的联系(需要手动关闭该线程产生的Handle),因此实现时,往往需要包含windows.h。
通过_beginthreadex启动的线程通过调用_endthreadex做相关清理。
3、CreateThread和ExitThread
CreateThread是Win32 API函数集中的一个函数,其原型为HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,DWORD dwStackSize,LPTHREAD_START_ROUTINE lpStartAddress,LPVOID lpParameter,DWORD wCreationFlags,LPDWORD lpThreadId);该函数使用Win32编程环境中的类型约定,只适用于Windows系统。参数形式与_beginthreadex一致,对线程控制能力也与之一致,只是该函数与C Runtime Library没有任何关系,它不负责初始化该库,因此在多线程环境中,如果使用该函数启动线程,则不应使用C Runtime Library中的多线程版本的函数。取而代之的应该是功能相对应的 Win32 API函数;另外,应当自己手工提供线程同步的代码。
通过CreateThread创建的线程则通过ExitThread做清理工作。
4、AfxBeginThread和AfxEndThread
AfxBeginThread是MFC提供的线程启动方式,它是个重载函数,有两种调用形式:Worker线程版和UI线程版。MFC对Win32线程做了小心的很好的封装(CWinThread),虽然其总是调用了_beginthreadex来启动一个线程,但是其额外做的工作使得在MFC环境下,操作线程变得简单明了,并且不需要太多的关注细节问题。MFC在线程的封装方面主要做了下列事情:
1、 自动清除CWinThread对象
2、 关闭线程handle,线程对象自动释放
3、 存储了线程相关的重要参数,即线程handle和线程ID
4、 辅之以其它MFC同步对象,方便的实现线程同步
5、 使用了严格的断言调试语句,使线程调试变得相对简单

“(C Runtime Library是用标准C开发的实用函数集)如果多线程程序中使用了标准C库函数,并用CreateThread()和ExitThread(),则会导致内存泄漏。解决这个问题的方法是用C运行库(run-time library)函数来启动和终止线程,而不用WIN32 API定义的CreateThread()和ExitThread()。在C运行库函数中,它们的替代函数分别是_beginthreadex()和_exitthreadex(),需要的头文件是_process.h。在VC6.0下,还需在Project->Settings->C/C++->Code Generation中选择Multithreaded Runtime Library。当然,也可以通过避免使用C标准库函数的方法来解决上述问题,WIN32提供了一些C标准库函数的替代函数,例如,可用wsprintf()和lstrlen()来代替sprintf()和strlen()。这样,使用CreateThread()和ExitThread()不会出现问题。”
六、 线程的同步问题(介绍Windows的同步机制)
1、 怎样等待一个线程结束(忙等(busy loop)和高效的等(WaitForSingleObject))
1) 忙等(busy loop)
hThrd = CreateThread(NULL,0,ThreadFunc,(LPVOID)1,0,&threadId );
for (;;)
{
GetExitCodeThread(hThrd, &exitCode);
if ( exitCode != STILL_ACTIVE )
break;
}
CloseHandle(hThrd);
缺点:耗费CPU资源,且如果在UI线程中这样等待将导致窗口无法刷新。不推荐使用。
2) 高效的等待
(1)WaitForSingleObject;
关于WaitForSingleObject的参数,前者为等待的对象,后者为等待的时间,对某些执行时间较长的线程,可以设置一个合适的值,等待完这个时间后,更新界面,然后继续等待,或者强行终止线程。
将以上的等待部分的代码改为:
WaitForSingleObject(hThrd,INFINITE);
该函数相当于Sleep函数,当需要等待的对象(句柄)没有被触发时,等待的线程将被自动挂起。该方法解决了耗费CPU时间的问题,但是在UI线程中,仍不能使用该方法来等待某一线程结束。
解决方法之一:创建一个Worker管理者线程,在该线程中等待,工作者线程完成,然后由管理者线程发消息通知UI线程更新窗口。
(2)WaitForMultipleObject
该函数允许在同一时间等待多个对象,函数的原型如下:
DWORD WaitForMultipleObject(DWORD nCount,CONST HANDE *lpHandles,BOOL bWaitAll,dwMilliseconds);
第一个参数表示句柄数组的大小;等待的对象不能超过64
第二个参数为句柄数组;
第三个参数表明是否等待所有对象激发。True表示是。
第四个参数为等待时间。
关于WaitForMultipleObject的返回值:
当bWaitAll为True时,返回值为WAIT_OBJECT_0;
当bWaitAll为false时,返回值减去WAIT_OBJECT_0,就是激发对象所在的下标。
应用:
A) 解决多个工人n完成多个任务m(n解决的思路如下:先从m个任务中取出n个任务,对应地用n个工人去完成,然后利用该函数等待其中任意一个工人结束任务,一旦结束则让其做另外一个任务
B) 解决等待多个资源的问题(bWaitAll设置为true)
哲学家就餐问题:5个哲学家在圆桌旁,每个哲学家左手边放着1只筷子,哲学家做两件事情,吃饭和思考,吃饭时同时需要其左右的两只筷子。
解决思路:将哲学家模拟为线程,筷子为资源,只有哲学家线程同时获得两个资源时,方可进一步动作(吃饭)。即:
WaitForMultipleObjects(2, myChopsticks, TRUE, INFINITE);
MyChopsticks是一个大小为5的核心对象数组。
(3)MsgWaitForMultipleObjects
原型:
DWORD MsgWaitForMultipleObjects( DWORD nCount,CONST HANDLE pHandles,BOOL fWaitAll,DWORD dwMilliseconds,DWORD dwWakeMask);
前几个参数含义同WaitForMultipleObject,最后一个是消息屏蔽标识,指示接收消息的类型。此外返回值也有额外的意义:当消息到达时,该函数返回WAIT_OBJECT_0+nCount。以下是常见的使用MsgWaitForMultipleObjects的架构:
while (!quit)
{ // Wait for next message or object being signaled
DWORD dwWake;
dwWake = MsgWaitForMultipleObjects(
gNumPrinting,
gPrintJobs,
FALSE,
INFINITE,
QS_ALLEVENTS);

if (dwWake >= WAIT_OBJECT_0 && dwWake < WAIT_OBJECT_0 + gNumPrinting)
{
//对象被触发
} // end if
else if (dwWake == WAIT_OBJECT_0 + gNumPrinting)
{
//有消息到达
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{ // Get Next message in queue
if (msg.message == WM_QUIT)
{
quit = TRUE;
exitCode = msg.wParam;
break;
} // end if
TranslateMessage(&msg);
DispatchMessage(&msg);
} // end while
}
} // end while
2、 怎样有效的控制一个线程
在任何情况下,切记线程的核心属性为:线程的句柄,线程的ID号。因此控制一个线程也需从这两方面着手。
1) 使用能返回线程Handle的启动函数来启动线程(除_beginthread外)
2) 尽量不要使一个工作量较大的线程成为“闷葫芦”,从而使该线程能够接收外界通知消息;如下列代码:


MSG msg;
while(1)
{
PeekMessage(&msg,NULL,0,0,PM_REMOVE);
if(msg.message==WM_MY)
break;
Sleep(100);
}
注:GetMessage也是用来得到消息队列中一条消息的函数,它们的区别在于GetMessage是同步的,即如果消息队列中没有消息的话,该线程将自动挂起。使用GetMessage可以使Worker线程成为一个一步一动的线程!
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
if(msg.message==WM_MY)
{
//Do something here
}
}
以上的过程也可以通过事件对象予以实现。
悬而未决的问题:怎么控制一个正在等待其他事件的线程。如:一个TCP监听线程,在某一Socket上listen,此时该线程处于挂起状态!但是现在主线程又需要关闭该线程,应该怎么操作!

3、 怎样互斥访问一个资源(CMutex和Critical Section)
何时需要一个互斥对象?
常见的情形:多个线程需要不定时的操作同一链表(锁链表的头指针);多个线程需要不定时的进行写文件或是进行屏幕输出(锁文件句柄或屏幕句柄);多个线程需要不定时对某个计数器进行操作(锁这个变量);在多线程环境吓,凡是涉及到对全局变量、静态变量、堆中的内存进行访问时,都应该考虑,是否可能出现一个race condition(竞争条件)。
1) 互斥器
Win32提供了对互斥资源访问的一整套机制,其中之一就是互斥器,MFC将这些API函数加以封装,形成了CMutex互斥类,使用这两种方法都能够实现对资源的互斥访问。
Win32中的API:
CreateMutex:
原型:
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName );
第一个参数为安全属性;
第二个参数用来指示互斥器的拥有者是否为当前线程;
第三个参数为互斥器的名称;
当不再需要互斥器时,应当调用CloseHandle关闭。
约定:互斥器产生之后,由某一线程完成锁定工作(即调用Wait…函数),此时系统将该mutex的拥有权交于该线程,然后短暂地将该对象设置为激发态,于是Wait…函数返回,做完相应的工作之后(如:修改链表指针、修改计数器、写文件等),调用ReleaseMutex释放拥有权。周而复始。
MFC中的互斥器CMutex对象:
A、 利用其构造函数产生一个互斥器对象
HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes,BOOL bInitialOwner, LPCTSTR lpName);
B、 配合CSingleLock或者CmutipleLock产生一个临时对象,对产生的互斥器进行加锁和释放的动作;
2) 临界区
另一个提供互斥访问的机制是Critical Section,该机制较前一种方法廉价,因为它不属于不是系统的核心对象;临界区可以反复进入,这一点与Mutex有所区别,这需要我们在使用临界区时,保证进入的次数要等于离开的次数。
相关函数为InitializeCriticalSection、DeleteCriticalSection、EnterCriticalSection、LeaveCriticalSection。
4、 怎样等待多个不同(或者相同)资源(WaitForMultiObject)
等待多个不同资源在多线程程序设计中时常遇到,如:等待某一线程结束和某一个资源被释放,等待缓冲区和设备准备好两个资源;这种现实情况,可以分别为不同的资源设置系统对象,然后利用WaitForMultiObject进行等待。
5、 怎样等待多个资源中的一个(使用CSemaphore)
现实中还可能出现如下情形:客人租相机的问题:有若干客人需要,租相机,总相机数为n,相机租完后,客人必须等待,只要有一个相机,则某客人就可以等到租借。还有许多问题可以用这种Producer/consumer模型加以概括。
这种情形即是等待多个资源中的一个的情况,在Win32程序设计中则经常使用信号量(Semaphore)来解决此问题。
Win32系统中,信号量具有以下特性:
一个信号量可以被锁定N次,N一般代表可用资源的个数,上例中即可代表相机的个数,信号量初始化后,在Win32环境下调用一次Wait…操作即表示对其的一次锁定,信号量的值相应加1,操作完后,调用ReleaseSemaphore操作,即代表资源释放(上述例子中就是归还相机)。MFC对Win32信号量的相关API函数进行了封装(CSemaphore),配合CMultiLock 或者 CSingleLock即可实现锁定和资源释放的动作。

IV.
Interlocked operations don't solve everything

By Raymond Chen
from http://weblogs.asp.net/oldnewthing/archive/2004/09/15/229915.aspx

Interlocked operations are a high-performance way of updating DWORD-sized or pointer-sized values in an atomic manner. Note, however, that this doesn't mean that you can avoid the critical section.

http://msdn.microsoft.com/library/en-us/dllproc/base/interlocked_variable_access.asp

For example, suppose you have a critical section that protects a variable, and in some other part of the code, you want to update the variable atomically. "Well," you say, "this is a simple imcrement, so I can skip the critical section and just do a direct InterlockedIncrement. Woo-hoo, I avoided the critical section bottleneck."

Well, except that the purpose of that critical section was to ensure that nobody changed the value of the variable while the protected section of code was running. You just ran in and changed the value behind that code's back.

Conversely, some people suggested emulating complex interlocked operations by having a critical section whose job it was to protect the variable. For example, you might have an InterlockedMultiply that goes like this:

// Wrong!
LONG InterlockedMultiply(volatile LONG *plMultiplicand, LONG lMultiplier)
{
EnterCriticalSection(&SomeCriticalSection);
LONG lResult = *plMultiplicand *= lMultiplier;
LeaveCriticalSection(&SomeCriticalSection);
return lResult;
}

While this code does protect against two threads performing an InterlockedMultiply against the same variable simultaneously, it fails to protect against other code performing a simple atomic write to the variable. Consider the following:

int x = 2;
Thread1()
{
InterlockedIncrement(&x);
}

Thread2()
{
InterlockedMultiply(&x, 5);
}

If the InterlockedMultiply were truly interlocked, the only valid results would be x=15 (if the interlocked increment beat the interlocked multiply) or x=11 (if the interlocked multiply beat the interlocked increment). But since it isn't truly interlocked, you can get other weird values:

Thread 1 Thread 2
x = 2 at start
InterlockedMultiply(&x, 5)
EnterCriticalSection
load x (loads 2)
InterlockedIncrement(&x);
x is now 3
multiply by 5 (result: 10)
store x (stores 10)
LeaveCriticalSection
x = 10 at end

Oh no, our interlocked multiply isn't very interlocked after all! How can we fix it?

If the operation you want to perform is a function solely of the starting numerical value and the other function parameters (with no dependencies on any other memory locations), you can write your own interlocked-style operation with the help of InterlockedCompareExchange.

http://msdn.microsoft.com/library/en-us/dllproc/base/interlockedcompareexchange.asp

LONG InterlockedMultiply(volatile LONG *plMultiplicand, LONG lMultiplier)
{
LONG lOriginal, lResult;
do {
lOriginal = *plMultiplicand;
lResult = lOriginal * lMultiplier;
} while (InterlockedCompareExchange(plMultiplicand,
lResult, lOriginal) != lOriginal);
return lResult;
}

[Typo in algorithm fixed 9:00am.]

To perform a complicated function on the multiplicand, we perform three steps.

First, capture the value from memory: lOriginal = *plMultiplicand;

Second, compute the desired result from the captured value: lResult = lOriginal * lMultiplier;

Third, store the result provided the value in memory has not changed: InterlockedCompareExchange(plMultiplicand, lResult, lOriginal)

If the value did change, then this means that the interlocked operation was unsucessful because somebody else changed the value while we were busy doing our computation. In that case, loop back and try again.

If you walk through the scenario above with this new InterlockedMultiply function, you will see that after the interloping InterlockedIncrement, the loop will detect that the value of "x" has changed and restart. Since the final update of "x" is performed by an InterlockedCompareExchange operation, the result of the computation is trusted only if "x" did not change value.

Note that this technique works only if the operation being performed is a pure function of the memory value and the function parameters. If you have to access other memory as part of the computation, then this technique will not work! That's because those other memory locations might have changed during the computation and you would have no way of knowing, since InterlockedCompareExchange checks only the memory value being updated.

Failure to heed the above note results in problems such as the so-called "ABA Problem". I'll leave you to google on that term and read about it. Fortunately, everybody who talks about it also talks about how to solve the ABA Problem, so I'll leave you to read that, too.

Once you've read about the ABA Problem and its solution, you should be aware that the solution has already been implemented for you, via the Interlocked SList functions.

http://msdn.microsoft.com/library/en-us/dllproc/base/interlocked_singly_linked_lists.asp

后面的一个问题
by MikeF

Possibly silly question: What is the "critical section bottleneck", and why is looping over a cmpxchg better?

My understanding is that a pure critical section approach (a) lets the OS sleep the blocked thread, and (b) wastes less time than the cmpxchg loop anyway.

I'm almost certainly missing something here but I can't see what. Is the overhead on critical sections particularly big?

回答
by Trent Glascock

MikeF: Waiting on a critical section is an expensive operation. So expensive that on multi-processor systems you can elect to "spin" before waiting on the critical section. The code in the original post could loop through a hundred iterations and still come out ahead of waiting on a busy critical section.

See InitializeCriticalSectionAndSpinCount

http://msdn.microsoft.com/library/en-us/dllproc/base/initializecriticalsectionandspincount.asp



<< Home

This page is powered by Blogger. Isn't yours?