首页 > 代码库 > 一家人的周末餐与多线程——起步

一家人的周末餐与多线程——起步

     多线程编程在学习编程初级阶段的时候一直是一个既富有神秘感而吸引人,又充满了难以学习感而经常看不懂,特别是当时还没有学习操作系统的时候。正好最近的 工作做了很多与多线程编程相关的事情,并且在坐班车的时候突发灵感,迸发出如何和现实结合阐述多线程的思路,希望这系列文章能给那些想要学习多线程编程的 童鞋能有个小小的帮助。另外,别忘了猛戳我的博客哟:http://www.richinmemory.com

     首先得介绍一下要出场的人物,不多,只有一家子四口人,妈妈,熊孩子哥哥,熊孩子弟弟,爸爸,所有的故事都是围绕一个主题,熊孩子帮大人做饭。

     吃一直是爸爸心目中最重要的事情,所以这天,爸 爸让妈妈去做饭。妈妈这一天比较困,又是周末,实在暂时不想起床。回头一想,熊孩子哥哥已经长大了,所以决定让他帮点忙做饭,主要就是洗洗蔬菜,剥剥豆子 之类的活,这样自己也能稍微再睡一会儿。一想到这里,妈妈甚是开心,于是叫来了哥哥,告诉他该做什么,然后给爸爸说,等哥哥弄好这些菜之后叫自己起来做 菜,自己再睡一下。哥哥听了之后,就去干活了,结果由于第一次干,实在太生疏,哥哥弄了好久才弄完,待爸爸去叫妈妈起来做菜时已经差不多一点了,到吃饭 时,爸爸和熊孩子们由于忍饥挨饿太久,情绪低落,最终大家在不热烈的气氛下吃了一顿饭。妈妈心想,下次我可不能这样了,这样太浪费时间了,这个简单的顺序 过程翻译成代码应该是这样的。妈妈就是主程序,而哥哥洗菜剥豆子和妈妈做菜就是主程序调用的两个函数,主程序只有当函数返回之后才能依次往下执行。利用 GetTickCount来统计下整个过程运行的时间。    

#include "stdafx.h"
#include <iostream>
#include <windows.h>  using namespace std;

unsigned long Child1Func();
void  CookSomething();
bool  IsAllDishesReady(int dishes[],int count);
bool  IsAllDishesCooked(int dishes[],int count);

int arrDishes[20];

int _tmain(int argc, _TCHAR* argv[])
{
    bool   bEat      = false;

    for( int i=0; i<20; i++ ) arrDishes[i] = 0;
    DWORD dwStart = GetTickCount();
    while(!bEat)
    {
        Child1Func();
                CookSomething();
        if(IsAllDishesCooked(arrDishes,20))
           bEat = true;
         
    }
        cout<<"********** Total time:" << GetTickCount() - dwStart <<endl;
    
    system("pause");
    return 0;
}

unsigned long Child1Func()
{
    for( int i=0; i<20; i++ )
    {    
    arrDishes[i] = 1;
    cout<<"dishes "<<i<<" is ready \r\n";
    Sleep(100);
    }
    return 0;
}

void  CookSomething()
{
    for( int i=0; i<20; i++ )
    {    
    if ( arrDishes[i] == 1 )
    {
        arrDishes[i] = 2;
        cout<<"dishes "<<i<<" is cooked \r\n";
        Sleep(100);
     } 
     }
}

bool IsAllDishesCooked(int dishes[],int count)
{
      for( int i=0; i<20; i++ )
      if(dishes[i] < 2 ) return false;
      return true;
}

      正好结合这段代码来说明下故事里出现的一些假设,后面的内容都基于这段假设,假设要洗剥的菜 有20个,用一个数组来表述,如果洗好了,则将相应的元素设置为1,如果做好了则设置为2,没有准备好的为0。当熊孩子洗完了所有的菜之后,妈妈就可以开 始做菜了。假设熊孩子准备一个菜的时间是100ms,而妈妈做一个菜的时间也是100ms,当然这有点不切合实际,但是谁也不想一个程序演示一下需要 30s。运行一下,如果按这样的流程,耗费的时间是:

       

     而且从输出看,只有当所有菜ready之后才开始cook的,这样着实浪费了大部分时间。

   又到了一个周末,好吃的爸爸又开始叫唤,这次妈妈没有睡懒觉,但是想到如果边做菜边让熊孩子哥哥来帮忙洗菜和剥豆子,双管齐下,岂不是更能节省时间?省得 爸爸到时候又在那叫来叫去的,于是叫来了哥哥帮忙,吩咐完哥哥该干嘛之后,妈妈就开始做菜了。看起来简直天衣无缝,结果熊孩子的特点就是你完全猜不透他的 想法,哥哥洗着洗着就开始玩水,结果妈妈一道菜做完了,熊孩子哥哥的菜还没有洗完,所以妈妈只能停下手中的工作等熊孩子哥哥的菜。还有就是有时候熊孩子累 了,熊孩子就休息一下,有时候妈妈累了,妈妈就休息一下,谁在干活其实没有个章法,效率极其低下。这还不是最糟糕的,最糟糕的是,有时候弟弟也要来参与, 弟弟也是个热心的熊孩子,经常从塑料袋里给哥哥拿菜洗,哥哥呢,明明洗好了一些菜,结果由于弟弟不停的加菜,越洗越多,并且分不清哪些菜已经洗过了,直接 导致哥哥情绪崩溃,妈妈也分不清篮子里的菜是不是都洗过了。于是,一家人又吃了一顿不愉快的午饭,“下次一定要改进”,妈妈在心里想到。 这个过程就是一个典型的多线程的过程,windows通过函数CreateThread来创建一个线程,其中大部分参数都用不到,比较重要的是返回值,线 程ID和回调函数。这个例子哥哥和弟弟的行为就是两个子线程的回调函数,句柄和线程Id都能标识线程是哥哥还是弟弟,妈妈就是主线程。一旦创建好一个线 程,线程就随时可以被执行,主线程和其他线程在执行过程中其实没什么规律,而且由于缺乏基本的管理,效率低下,问题很多,写成代码大约是这个样子滴,通过 执行的代码也能看到上面所说的问题。  

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include "stdafx.h"
#include <iostream>
#include <windows.h>
using namespace std;
  
DWORD WINAPI Child1Func(LPVOID);
DWORD WINAPI Child2Func(LPVOID);
void  CookSomething();
bool  IsAllDishesReady(int dishes[],int count);
bool  IsAllDishesCooked(int dishes[],int count);
  
int arrDishes[20];
 
int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE hChild1,hChild2;
    HANDLE hEvent;
    DWORD  threadId1,threadId2;
    DWORD  ExitCode1 = 0;
    int    rtnValue1 = 0;
    bool   bDone     = false;
    bool   bEat      = false;
 
    for( int i=0; i<20; i++ ) arrDishes[i] = 0;
    DWORD dwStart = GetTickCount();
    while(!bEat)
    {
    hChild1  = CreateThread(NULL,0,Child1Func,0,0,&threadId1);
    hChild2  = CreateThread(NULL,0,Child2Func,0,0,&threadId2);
    CookSomething();
    if(IsAllDishesCooked(arrDishes,20))
           bEat = true;
     }
     cout<<"********** Total time:" << GetTickCount() - dwStart <<endl;
         
     system("pause");
     return 0;
}
 
DWORD WINAPI Child1Func(LPVOID p)
{
     for( int i=0; i<20; i++ )
     
     arrDishes[i] = 1;
     cout<<"dishes "<<i<<" is ready \r\n";
     Sleep(100);
     
     return 0;
}
 
DWORD WINAPI Child2Func(LPVOID p)
{
     for( int i=0; i<20; i++ )
     
    arrDishes[i] = 0;
    cout<<"dishes "<<i<<" is not ready \r\n";
    Sleep(50);
      }
      return 0;
}
 
void  CookSomething()
{
     for( int i=0; i<20; i++ )
     
      if ( arrDishes[i] == 1 )
      {
        arrDishes[i] = 2;
        cout<<"dishes "<<i<<" is cooked \r\n";
        Sleep(100);
      }
     }
}
 
bool IsAllDishesCooked(int dishes[],int count)
{
    for( int i=0; i<20; i++ )
    {
    if(dishes[i] < 2 ) return false;
    }
    return true;
}

   执行一下,会发现,这样的过程甚至无法执行完,因为太混乱了,一下子执行线程1(哥哥),一下子是线程2(弟弟),一下子又是主线程(妈妈)。又是对于同一个数组(菜)进行操作,所以,如果不经规范和认真规划的多线程程序绝对是灾难。

   经过了上次那个周末,妈妈决定这次自己来,但是哥哥居然自己跑来说要帮忙,妈妈本来怕重蹈覆辙,准备拒绝,但是看到哥哥那渴望的眼神,没忍住,就答应他 了。但是,这次妈妈经过前两次的教训,决定在哥哥洗菜的时候看着他,就不停的问哥哥菜洗完了吗?一旦得到哥哥洗完的消息就开始做饭,如果出现了其他问题, 也好相应的采取措施。但是这样的问题就在于,妈妈放下了手中所有的事情去询问哥哥的状态,虽然效率是低下了点,但是也算不会出错。这个状态在多线程编程里 称为线程的状态量,主程序可以通过GetExitCodeThread获得,写成代码大约是这样的。

   

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include "stdafx.h"
#include <iostream>
#include <windows.h>
using namespace std;
 
DWORD WINAPI Child1Func(LPVOID);
DWORD WINAPI Child2Func(LPVOID);
void  CookSomething();
bool  IsAllDishesReady(int dishes[],int count);
bool  IsAllDishesCooked(int dishes[],int count);
 
int nBeansAmount = 100;
DWORD dwStartTime = 0;
int arrDishes[20];
 
int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE hChild1;
    HANDLE hEvent;
    DWORD  threadId1;
        DWORD  ExitCode1 = 0;
        int    rtnValue1 = 0;
        bool   bDone     = false;
        bool   bEat      = false;
 
    for( int i=0; i<20; i++ ) arrDishes[i] = 0;
    DWORD dwStart = GetTickCount();
    while(!bEat)
    {
        hChild1  = CreateThread(NULL,0,Child1Func,0,0,&threadId1);
        do
        {
             rtnValue1 = GetExitCodeThread(hChild1,&ExitCode1);
        }while( ExitCode1==STILL_ACTIVE && rtnValue1 != 0 );
            CookSomething();
        if(IsAllDishesCooked(arrDishes,20))
           bEat = true;
       }      
    
    cout<<"********** Total time:" << GetTickCount() - dwStart <<endl;
      
    system("pause");
    return 0;
}
 
DWORD WINAPI Child1Func(LPVOID p)
{
    for( int i=0; i<20; i++ )
    {  
        arrDishes[i] = 1;
        cout<<"dishes "<<i<<" is ready \r\n";
        Sleep(100);
    }
         
    return 0;
}
 
void  CookSomething()
{
    for( int i=0; i<20; i++ )
    {  
        if ( arrDishes[i] == 1 )
        {
            arrDishes[i] = 2;
            cout<<"dishes "<<i<<" is cooked \r\n";
            Sleep(100);
        }
    }
}
 
bool  IsAllDishesReady(int dishes[],int count)
{
    for( int i=0; i<20; i++ )
    {
        if(dishes[i] == 0) return false;
    }
    return true;
}
 
bool IsAllDishesCooked(int dishes[],int count)
{
    for( int i=0; i<20; i++ )
    {
            if(dishes[i] < 2 ) return false;
    }
    return true;
}

     执行一下上述代码,会发现,运行的结果和第一种情况可以说基本完全一样,运行的时间也差不多,所以这种方法只有多线程编程的形,却没有多线程编程的心,主线程一直停在那里检查线程的状态,术语称之为busy waiting,这种等待会使得多线程的优点尽失。

       上次的方法实在一个得不偿失的方法,所以妈妈这个周末决定换一种方式,每 隔一段时间询问下熊孩子哥哥是否洗好,如果有洗好的菜,就拿来做菜,如果没有,就接着做现在正在做的菜。这样在某种程度上就实现了“同时”干活的目标,大 大缩短了时间,并且由于有询问状态,所以也没有出错。在多线程中可以使用WaitForSingleObject来等待一个线程的状态。这个函数除了能等待指定等待一个线程的状态,还可以设置一个time
out的时间,可以理解成为,过多少时间去询问一下当前线程的状态(代码里我设置的是1000ms)。如果线程已经执行完毕退出,则为WAIT_OBJECT_0,如果过了这么多时间线程没有反应,则为WAIT_TIMEOUT,用这个实现上面的逻辑就是这样的(只展示修改的main中 的代码了喔~)。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hChild1  = CreateThread(NULL,0,Child1Func,0,0,&threadId1);
 
while(!bDone)
{
    DWORD dwStatus = WaitForSingleObject(hChild1,1000);
    switch(dwStatus)
    {
    case WAIT_OBJECT_0:
     if( IsAllDishesReady(arrDishes,20) )
        bDone = true;
     break;
    case WAIT_TIMEOUT:
    CookSomething();
    break;
    }          
}
 
CookSomething();
if(IsAllDishesCooked(arrDishes,20))
    bEat = true;

    替换上面main部分多线程的代码,你会发现,运行速度提高了大约1s,相对于最开始的速度,已经提升了20%多,所以可以看到多线程的第一个优点就是能提高程序的相应速度。 

    上一次的成功让妈妈大为振奋,这个周末她想到了个更绝的办法,她想到,为什么要我去询问孩子的状态,浪费这个时间,我完全可以让孩子自己汇报当前的状态 啊。比如他洗完了一个菜,就告诉我,什么什么菜洗完啦,于是我就可以取这个菜做饭了,如果没有洗完我还可以做其他事情。想到这里,妈妈十分兴奋,于是叫来 了哥哥,告诉了他这个点子,双方开始愉快的干活。果然,合理的规划加上统筹,这种方法大大提高了效率。在多线程编程中,如何让子线程通知主线程某种事物的 状态呢?一种最常用的方法就是Event,当某种事件发生之后,子线程可以将主线程创建的Event激活,主线程只要通过上面说过的 WaitForSingleObject来等待Event的状态就可以实现接收子线程对某种事件的激活。上述过程写成代码大约是这样的(同样,只展示不同 的部分)。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// main part
hEvent   = CreateEvent(NULL,TRUE,FALSE,_T("ChildEvent1"));
hChild1  = CreateThread(NULL,0,Child1Func,0,0,&threadId1);
         
while(!bDone)
{
    DWORD dwStatus = WaitForSingleObject(hEvent,INFINITE);
    switch(dwStatus)
    {
         case WAIT_OBJECT_0:
                   CookSomething();
           ResetEvent(hEvent);
           break;
         case WAIT_TIMEOUT:
           break;
     }
     if( IsAllDishesReady(arrDishes,20) )
         bDone = true;
}
if(IsAllDishesCooked(arrDishes,20))
    bEat = true;
 
// ThreadFunc1
HANDLE hEvent;
hEvent = OpenEvent(EVENT_ALL_ACCESS ,FALSE,_T("ChildEvent1"));
for( int i=0; i<20; i++ )
{
    if( arrDishes[i] == 0 )
    {
        arrDishes[i] = 1;
        SetEvent(hEvent);
        cout<<"dishes "<<i<<" is ready \r\n";
        Sleep(100);
    }
}

     SetEvent 和 ResetEvent 分别是将事件置为有信号状态和无信号状态,当事件是有信号状态时,WaitForSingleObject返回的状态WAIT_OBJECT_0。另外在 CreateEvent创建一个事件的时候,最后一个字符串是该事件的名字,这样就可以在其余的地方通过这个名字来找到相应的事件了,等于是一个标识的作 用。运行下这段代码,会发现,基本上ready和cooked的是交替出现,并且只需要2s多,相比前面的,过程的运行速度提高了50%。

    这一段的故事弟弟都没怎么参加,但是这怎么可能呢?小孩子看到大一点的孩子在做什么都要去捣乱的,就和多线程编程一样,在绝大多数情况下,都不可能只有一 个子线程的,所以,这一部分仅仅是最基本的概念,如果有很多线程的情况下,还会产生很多问题,比如弟弟把哥哥洗好的菜替换成了还没有洗的菜,这些问题请看 下一篇,一家人的周末餐与多线程之熊孩子越多越麻烦(个人博客会先更新喔,http://www.richinmemory.com)。

    另外,这里就没有具体介绍所有和多线程相关的函数的具体解释(譬如,函数的参数,返回值等等),因为我觉得这事如果说太细,会影响对宏观的认识,就显得太琐碎,所以,如果对这方面有疑问的话,请参照MSDN吧。