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

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

时间: 2024-12-04 23:02:03

一家人的周末餐与多线程——起步的相关文章

多线程编程1

参考资料: http://blog.csdn.net/JXH_123/article/details/23450031                             秒杀多线程系列 http://www.baidu.com/index.php?tn=utf8kb_oem_dg&addresssearch=1#wd=C%2B%2B%E5%BE%AA%E7%8E%AF%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97&ie=utf-8&tn=utf8kb

Python多线程入门指南

一直懒得写Python相关的文章,恰好有天需要简单的给童鞋们讲点课,仓促之余就诞生了此文. 今天本来准备全面的聊聊有关高性能并发这个话题来着,但是周末马上要来了啊.所以我就取了其中的一点来介绍,关于其他的方面,有兴趣的小伙伴可以和我交流.谈高效并发,往往脱离不了以下三种方案: 进程:每个逻辑控制流都是一个进程,由内核来调度和维护.因为进程有独立的虚拟地址空间,想要和其他控制流通信必须依靠显示的进程间通信,即我们所说的IPC机制 线程:线程应该是我们最为熟知的.它本质是运行在一个单一进程上下文中的

【Linux】多进程与多线程之间的区别

http://blog.csdn.net/byrsongqq/article/details/6339240 网络编程中设计并发服务器,使用多进程与多线程 ,请问有什么区别?  答案一: 1,进程:子进程是父进程的复制品.子进程获得父进程数据空间.堆和栈的复制品. 2,线程:相对与进程而言,线程是一个更加接近与执行体的概念,它可以与同进程的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列. 两者都可以提高程序的并发度,提高程序运行效率和响应时间. 线程和进程在使用上各有优缺点:线程执行开

C++多线程那些事

线程之间的关系一般有两种,一种是互斥,一种是同步,互斥可以表现为两个线程同时争夺同一个资源,同步可以表现为两个线程按一定次序完成一个任务(如A 完成任务的前半部分,紧接着需要线程B 完成线程的后半部分) 在C++中处理上面两种关系的常用方法是: 关键段.事件.互斥量.信号量. 注意C++开启新的线程一定使用_beginthreadex函数而不要使用CreateThread函数,因为后者对系统中的全局变量没有保护,所以多个线程程环境下,容易出现系统的全局变量的值被覆盖的情况,而前者每个线程都有单独

这个周末我好累

0.硕士老婆周六又逃课了,马上要考试了都不去复习(顺便那啥一下考子们,你们懂的),唉...事实上从学校出来以后,你将面对更多的考试!1.但是不管哪里的考试,我都不参加!2.觉得<完美回忆>结局不好,纯粹是导演开后门儿...3.只要按我的想法继续<完美回忆>,一定比<黑客帝国>还成功!我一直坚信,好的电影没有第二部且不超过三小时(一个成年人酝酿一脬屎的最短时间,当然,不算拉肚子)! 4.Linux内核精髓?拉倒吧,爱探险的朵拉!5.话又说回来,曾经在高中属于风云级别人物的

Android 多线程编程初探

Android 中的多线程其实就是 JavaSE 中的多线程,只是为了方便使用,android 封装了一些类,如 AsyncTask.HandlerThread 等,在日常的开发过程中,我们往往需要去执行一些耗时的操作,例如发起网络请求,考虑到网速等其他外在的因素,服务器可能不会立刻响应我们的请求,如果不将这条操作放到子线程中去执行,就会造成主线程被阻塞,今天我们就从多线程的基础来一起探讨 一.线程的基本用法   对于 Andorid 多线程来说我们最新接触到的就是 Thread 和 Runna

Java多线程编程模式实战指南之Promise模式

Promise模式简介(转) Promise模式是一种异步编程模式 .它使得我们可以先开始一个任务的执行,并得到一个用于获取该任务执行结果的凭据对象,而不必等待该任务执行完毕就可以继续执行其他操作.等到我们需要该任务的执行结果时,再调用凭据对象的相关方法来获取.这样就避免了不必要的等待,增加了系统的并发性.这好比我们去小吃店,同时点了鸭血粉丝汤和生煎包.当我们点餐付完款后,我们拿到手的其实只是一张可借以换取相应食品的收银小票(凭据对象)而已,而不是对应的实物.由于鸭血粉丝汤可以较快制作好,故我们

ios多线程和进程的区别(转载)

本文转载至  http://daimajishu.iteye.com/blog/1557076 很想写点关于多进程和多线程的东西,我确实很爱他们.但是每每想动手写点关于他们的东西,却总是求全心理作祟,始终动不了手. 今天终于下了决心,写点东西,以后可以再修修补补也无妨. 一.为何需要多进程(或者多线程),为何需要并发? 这个问题或许本身都不是个问题.但是对于没有接触过多进程编程的朋友来说,他们确实无法感受到并发的魅力以及必要性. 我想,只要你不是整天都写那种int main()到底的代码的人,那

Apache Kafka系列(四) 多线程Consumer方案

Apache Kafka系列(一) 起步 Apache Kafka系列(二) 命令行工具(CLI) Apache Kafka系列(三) Java API使用 Apache Kafka系列(四) 多线程Consumer方案 本文的图片是通过PPT截图出的,读者如果修改意见请联系我 一.Consumer为何需要实现多线程 假设我们正在开发一个消息通知模块,该模块允许用户订阅其他用户发送的通知/消息.该消息通知模块采用Apache Kafka,那么整个架构应该是消息的发布者通过Producer调用AP