CSDN首页 空间 新闻 论坛 Blog 下载 读书 网摘 搜索 .NET Java 视频 接项目 求职 在线学习 买书 程序员 通知
【经验总结】不能实施并行处理的情况 浅谈并行编程中的任务分解模式
CSDN社区
搜索 收藏 打印 关闭
CSDN社区 >  专题开发/技术/项目 >  英特尔多核计算技术

实例解析C++/CLI线程之多任务

楼主celineshi()2006-09-26 16:07:09 在 专题开发/技术/项目 / 英特尔多核计算技术 提问

简介  
   
    从处理器的角度来看,线程是一个单独的执行流程,每个线程都有各自的寄存器及堆栈上下文。通常来说,在系统中只有一个处理器或处理器只有一个核心时,运行时环境在一个时间片内只能执行一个线程,当线程未能获取所需的资源时,线程的执行就会被中断,且会一直等到相关操作的完成,如I/O;或者在线程用完它的处理器时间片时,也会被中断下来等待。而处理器把执行流程从一个线程切换到另一个线程时,这称为"上下文切换";当某个线程变为"阻塞"状态,从而执行另一个线程时,系统有效地减少了处理器空闲时间,这称为"多任务"。  
   
    当程序执行时,系统知道可以从磁盘上某处获取相关的指令及静态数据,程序会被分配到一组包含虚拟内存在内的地址空间,这个运行时上下文被称为"进程"。然而,在一个进程可以运行之前,它必须拥有至少一个线程,也就是说,当一个进程被创建时,它自动被赋予了一个线程,这称为"主线程"。但是话说回来,这个线程与之后这个进程所创建的线程相比,没有任何不同之处,它只不过恰好是这个进程的第一个线程而已。一般来说,在程序的控制之下,进程内的线程数在运行时会有所变化,任何线程都可以创建其他的线程,但不管怎样,线程不拥有它所创建的线程,所有进程内的线程都是作为一个整体属于这个进程。  
   
    可把进程要完成的工作分成不同的"子任务",每一部分都由不同的线程来执行,这称为"多线程"。进程内的每个线程共享同样的地址空间与进程资源,当最后一个进程内的线程结束时,父进程就结束了。  
   
    为何进程内要有多个线程呢?如果进程只有一个线程,那么它的执行流程是自上而下顺序执行的;当线程阻塞,而又没有其他的活动线程处于等待状态时,系统就会进入空闲状态;如果此时进程的子任务必须被顺序地执行,那么这种情况就不可避免,将花费大量的时间来等待。然而,绝大多数的进程都不是这样的,试想有这样一种情况,某个进程有多个选项,用户可以选择其中一些选项,由此产生的计算会使用内存或文件中的数据,并生成结果,如果能从中分出一些新的线程,那么进程不必等待前一个计算的结果,就可以继续接受新的计算请求。此外,通过指定线程的优先级,进程可只在更关键的线程阻塞时,才运行次关键的线程。  
   
    在有多个线程的情况下,某些线程可负责程序的主要工作,而另一个线程可用于处理键盘和鼠标的输入。例如,用户可能会觉得前一次请求并不是期望的动作,从而希望取消由前一次请求产生的那一个线程,这时就可在某个下拉菜单中进行选择,由一个线程去终止另一个线程。  
   
    另一个例子就是打印假脱机程序,它的任务是保持打印机尽可能地满载工作,并处理用户的打印请求;如果这个程序必须要等到前一项打印工作完成,才能接受新请求的话,用户可能会感到非常的不满。当然,程序也可周期性地停下打印工作,来查看是否有新的未处理请求(这称为"轮询"),但是,如果没有新请求,这将会非常浪费时间。另外,如果轮询的间隔时间太长,对处理新请求,还会造成延时;如果间隔太短,那么线程在轮询上花费的时间又太多。那么,为什么不让假脱机程序有两个线程呢?一个用于将打印工作传递到打印机,而另一个用于处于用户的请求,它们之间都相互独立运行;而当一个线程工作完成时,它要么结束自身,要么进入休眠状态。  
   
    当处理并发的执行线程时,必须要首先了解两个重要的概念:原子性和重入性。一个原子变量或对象是作为一个整体被访问的,甚至于在异步操作的情况下也是如此--访问的是同一个变量或对象。举例来讲,如果一个线程正在更新一个原子变量或对象,而另一个线程在读取其内容,此时来讲,内容逻辑上的完整性是不可能被破坏的,所以,要么读取到旧值,要么读取到新值,而不会旧值新值各读一部分。通常来说,能被原子性访问的变量或对象,只是那些在硬件上能被原子性支持的类型,如字节(Byte)和字(Word)。C++/CLI中大多数的基本类型都确保具有原子性,剩下的类型也可被某种特定的实现支持原子性,但不能百分百保证。显而易见,一个实现了x与y坐标对的Point对象,不具有原子性,对Point值的写入,可能会被对其值的读取中断,结果就是,读取到了一个新的x值和一个旧的y值,反之亦然;同样地,数组也不可能被原子性地访问。正是因为大多数的对象不能被原子性地访问,所以必须使用一些同步形式来保证在某一时间,只有一个线程可操纵某个特定的对象。也正是因为此,C++/CLI分配给每一个对象、数据和类一个同步锁。  
   
    一个重入的函数可由多个线程安全地并行执行。当线程开始执行一个函数时,在函数中分配的所有数据都来自栈或堆,但无论如何,对此调用来说,都是唯一的。如果在另一个线程仍处于工作状态时,本线程开始执行同一个函数,那么,每个线程中的数据都是相互独立的。然而,如果函数访问线程间共享的变量或文件时,则必须使用某些同步方法。  
   
    简介  
   
    从处理器的角度来看,线程是一个单独的执行流程,每个线程都有各自的寄存器及堆栈上下文。通常来说,在系统中只有一个处理器或处理器只有一个核心时,运行时环境在一个时间片内只能执行一个线程,当线程未能获取所需的资源时,线程的执行就会被中断,且会一直等到相关操作的完成,如I/O;或者在线程用完它的处理器时间片时,也会被中断下来等待。而处理器把执行流程从一个线程切换到另一个线程时,这称为"上下文切换";当某个线程变为"阻塞"状态,从而执行另一个线程时,系统有效地减少了处理器空闲时间,这称为"多任务"。  
   
    当程序执行时,系统知道可以从磁盘上某处获取相关的指令及静态数据,程序会被分配到一组包含虚拟内存在内的地址空间,这个运行时上下文被称为"进程"。然而,在一个进程可以运行之前,它必须拥有至少一个线程,也就是说,当一个进程被创建时,它自动被赋予了一个线程,这称为"主线程"。但是话说回来,这个线程与之后这个进程所创建的线程相比,没有任何不同之处,它只不过恰好是这个进程的第一个线程而已。一般来说,在程序的控制之下,进程内的线程数在运行时会有所变化,任何线程都可以创建其他的线程,但不管怎样,线程不拥有它所创建的线程,所有进程内的线程都是作为一个整体属于这个进程。  
   
    可把进程要完成的工作分成不同的"子任务",每一部分都由不同的线程来执行,这称为"多线程"。进程内的每个线程共享同样的地址空间与进程资源,当最后一个进程内的线程结束时,父进程就结束了。  
   
    为何进程内要有多个线程呢?如果进程只有一个线程,那么它的执行流程是自上而下顺序执行的;当线程阻塞,而又没有其他的活动线程处于等待状态时,系统就会进入空闲状态;如果此时进程的子任务必须被顺序地执行,那么这种情况就不可避免,将花费大量的时间来等待。然而,绝大多数的进程都不是这样的,试想有这样一种情况,某个进程有多个选项,用户可以选择其中一些选项,由此产生的计算会使用内存或文件中的数据,并生成结果,如果能从中分出一些新的线程,那么进程不必等待前一个计算的结果,就可以继续接受新的计算请求。此外,通过指定线程的优先级,进程可只在更关键的线程阻塞时,才运行次关键的线程。  
   
    在有多个线程的情况下,某些线程可负责程序的主要工作,而另一个线程可用于处理键盘和鼠标的输入。例如,用户可能会觉得前一次请求并不是期望的动作,从而希望取消由前一次请求产生的那一个线程,这时就可在某个下拉菜单中进行选择,由一个线程去终止另一个线程。  
   
    另一个例子就是打印假脱机程序,它的任务是保持打印机尽可能地满载工作,并处理用户的打印请求;如果这个程序必须要等到前一项打印工作完成,才能接受新请求的话,用户可能会感到非常的不满。当然,程序也可周期性地停下打印工作,来查看是否有新的未处理请求(这称为"轮询"),但是,如果没有新请求,这将会非常浪费时间。另外,如果轮询的间隔时间太长,对处理新请求,还会造成延时;如果间隔太短,那么线程在轮询上花费的时间又太多。那么,为什么不让假脱机程序有两个线程呢?一个用于将打印工作传递到打印机,而另一个用于处于用户的请求,它们之间都相互独立运行;而当一个线程工作完成时,它要么结束自身,要么进入休眠状态。  
   
    当处理并发的执行线程时,必须要首先了解两个重要的概念:原子性和重入性。一个原子变量或对象是作为一个整体被访问的,甚至于在异步操作的情况下也是如此--访问的是同一个变量或对象。举例来讲,如果一个线程正在更新一个原子变量或对象,而另一个线程在读取其内容,此时来讲,内容逻辑上的完整性是不可能被破坏的,所以,要么读取到旧值,要么读取到新值,而不会旧值新值各读一部分。通常来说,能被原子性访问的变量或对象,只是那些在硬件上能被原子性支持的类型,如字节(Byte)和字(Word)。C++/CLI中大多数的基本类型都确保具有原子性,剩下的类型也可被某种特定的实现支持原子性,但不能百分百保证。显而易见,一个实现了x与y坐标对的Point对象,不具有原子性,对Point值的写入,可能会被对其值的读取中断,结果就是,读取到了一个新的x值和一个旧的y值,反之亦然;同样地,数组也不可能被原子性地访问。正是因为大多数的对象不能被原子性地访问,所以必须使用一些同步形式来保证在某一时间,只有一个线程可操纵某个特定的对象。也正是因为此,C++/CLI分配给每一个对象、数据和类一个同步锁。  
   
    一个重入的函数可由多个线程安全地并行执行。当线程开始执行一个函数时,在函数中分配的所有数据都来自栈或堆,但无论如何,对此调用来说,都是唯一的。如果在另一个线程仍处于工作状态时,本线程开始执行同一个函数,那么,每个线程中的数据都是相互独立的。然而,如果函数访问线程间共享的变量或文件时,则必须使用某些同步方法。 问题点数:100、回复次数:6Top

1 楼celineshi()回复于 2006-09-26 16:07:28 得分 0

请看例2中的ThreadY类,当一个线程调用标记1中的Move,而另一个线程隐式地调用标记2中的ToString时,潜在的冲突就发生了。因为两个函数没有用同步措施来访问同一个Point,Move可能会先更新x坐标,但在它更新相应的y坐标之前,ToString却显示了一对错误的坐标值,这时,输出可能会如插2a所示。然而,当相关的语句被同步之后,ToString显示的坐标对总是正确匹配的,同步执行之后的输出如插2b所示。再看一下例2中的Point类型,在此可看到这些访问x与y坐标的函数是如何被同步的。  
   
    插2:a线程输出产生了不匹配的坐标对;b同步执行中匹配的坐标对  
   
    (a)  
   
  (1878406,1878406)  
  (2110533,2110533)  
  (2439367,2439367)  
  (2790112,2790112)  
  x:   3137912  
  y:   3137911   //   y与x不同  
  (3137912,3137911)   //   y与x不同  
  (3466456,3466456)  
  (3798720,3798720)  
  (5571903,5571902)   //   y与x不同  
  (5785646,5785646)  
  (5785646,5785646)    
   
    (b)  
   
  (333731,333731)  
  (397574,397574)  
  (509857,509857)  
  (967553,967553)  
  x:   853896  
  y:   967553   //   y仍与x不同  
  (1619521,1619521)  
  (1720752,1720752)  
  (1833313,1833313)  
  (2973291,2973291)  
  (3083198,3083198)  
  (3640996,3640996)    
   
    在此,可把一段语句放在一个称作"同步锁"--即Thread::Monitor的Enter与Exit语句当中,来进行对某些资源的独占式访问,如标记1a与1b、2a与2b、3a与3b、4a与4b。  
   
    因为Move与ToString都是实例函数,当它们在同一Point上被调用时,它们共享Point的同步锁,为独占访问一个对象,就必须传递一个指向对象的句柄给Enter。如果在ToString访问时,Move也被调用操作同一Point,Move将会一直处于阻塞状态,直至ToString完成,反之亦然。结果就是,函数花费时间在相互等待,反之没有同步,它们都会尽可能快地同时运行。  
   
    一旦同步锁控制了对象,它将保证在同一时刻,只有一个此类的实例函数可以在对象上执行它的关键代码。当然,类中没有使用同步锁的其他实例函数,可不会理会它的同步"兄弟"在做些什么,所以,必须小心适当地使用同步锁(注意,X与Y的访问器未被同步)。同步锁对于那些操作不同对象的实例函数,将不起任何作用,这些函数不会互相等待。  
   
    通常地,当调用Exit时,同步锁就被释放了,因此,同步锁的作用范围就是Enter与Exit中间的那些代码,程序员必须有责任避免死锁问题的发生--防止线程A一直等待线程B,或反之。  
   
    假设有一个包含25条语句的函数,其中只有3条连贯的语句需要同步,如果我们把全部的25条语句都包括在一个同步锁中,那么,将把资源比实际所需锁住了更长的时间。正如前述代码所示,每个同步锁保持的时间都要尽可能地短。  
   
    请看例3中的ArrayManip结构,当同步锁执行到标记2时,锁中的array正处于忙碌状态,因此将会阻塞其他所有在array上需要同步的代码。  
   
    例3:  
   
  using   namespace   System;  
  using   namespace   System::Threading;  
   
  public   ref   struct   ArrayManip  
  {  
   static   int   TotalValues(array<int>^   array)  
   {  
    /*1*/   int   sum   =   0;  
    /*2*/   Monitor::Enter(array);  
    {  
     for   (int   i   =   0;   i   <   array->Length;   ++i)  
     {  
      sum   +=   array[i];  
     }  
    }  
    Monitor::Exit(array);  
    return   sum;  
   }  
   
   static   void   SetAllValues(array<int>^   array,   int   newValue)  
   {  
    /*3*/   Monitor::Enter(array);  
    {  
     for   (int   i   =   0;   i   <   array->Length;   ++i)  
     {  
      array[i]   =   newValue;  
     }  
    }  
    Monitor::Exit(array);  
   }  
   
   static   void   CopyArrays(array<int>^   array1,   array<int>^   array2)  
   {  
    /*4*/   Monitor::Enter(array1);  
    {  
     /*5*/   Monitor::Enter(array2);  
     {  
      Array::Copy(array1,   array2,    
       array1->Length   <   array2->Length   ?   array1->Length  
       :   array2->Length);  
     }  
     Monitor::Exit(array2);  
    }  
    Monitor::Exit(array1);  
   }  
  };    
   
    一个同步锁可包含同一对象的另一个同步锁,在这种情况下,锁计数相应地增长了;但如果想被另一个线程中的同步语句操作,必须先递减到零。一个同步锁还可包含不同对象的同步锁,在此情况下,它将会一直阻塞,直到第二个对象可访问,函数CopyArrays就是一个例子。Top

2 楼celineshi()回复于 2006-09-26 16:07:43 得分 0

请看例2中的ThreadY类,当一个线程调用标记1中的Move,而另一个线程隐式地调用标记2中的ToString时,潜在的冲突就发生了。因为两个函数没有用同步措施来访问同一个Point,Move可能会先更新x坐标,但在它更新相应的y坐标之前,ToString却显示了一对错误的坐标值,这时,输出可能会如插2a所示。然而,当相关的语句被同步之后,ToString显示的坐标对总是正确匹配的,同步执行之后的输出如插2b所示。再看一下例2中的Point类型,在此可看到这些访问x与y坐标的函数是如何被同步的。  
   
    插2:a线程输出产生了不匹配的坐标对;b同步执行中匹配的坐标对  
   
    (a)  
   
  (1878406,1878406)  
  (2110533,2110533)  
  (2439367,2439367)  
  (2790112,2790112)  
  x:   3137912  
  y:   3137911   //   y与x不同  
  (3137912,3137911)   //   y与x不同  
  (3466456,3466456)  
  (3798720,3798720)  
  (5571903,5571902)   //   y与x不同  
  (5785646,5785646)  
  (5785646,5785646)    
   
    (b)  
   
  (333731,333731)  
  (397574,397574)  
  (509857,509857)  
  (967553,967553)  
  x:   853896  
  y:   967553   //   y仍与x不同  
  (1619521,1619521)  
  (1720752,1720752)  
  (1833313,1833313)  
  (2973291,2973291)  
  (3083198,3083198)  
  (3640996,3640996)    
   
    在此,可把一段语句放在一个称作"同步锁"--即Thread::Monitor的Enter与Exit语句当中,来进行对某些资源的独占式访问,如标记1a与1b、2a与2b、3a与3b、4a与4b。  
   
    因为Move与ToString都是实例函数,当它们在同一Point上被调用时,它们共享Point的同步锁,为独占访问一个对象,就必须传递一个指向对象的句柄给Enter。如果在ToString访问时,Move也被调用操作同一Point,Move将会一直处于阻塞状态,直至ToString完成,反之亦然。结果就是,函数花费时间在相互等待,反之没有同步,它们都会尽可能快地同时运行。  
   
    一旦同步锁控制了对象,它将保证在同一时刻,只有一个此类的实例函数可以在对象上执行它的关键代码。当然,类中没有使用同步锁的其他实例函数,可不会理会它的同步"兄弟"在做些什么,所以,必须小心适当地使用同步锁(注意,X与Y的访问器未被同步)。同步锁对于那些操作不同对象的实例函数,将不起任何作用,这些函数不会互相等待。  
   
    通常地,当调用Exit时,同步锁就被释放了,因此,同步锁的作用范围就是Enter与Exit中间的那些代码,程序员必须有责任避免死锁问题的发生--防止线程A一直等待线程B,或反之。  
   
    假设有一个包含25条语句的函数,其中只有3条连贯的语句需要同步,如果我们把全部的25条语句都包括在一个同步锁中,那么,将把资源比实际所需锁住了更长的时间。正如前述代码所示,每个同步锁保持的时间都要尽可能地短。  
   
    请看例3中的ArrayManip结构,当同步锁执行到标记2时,锁中的array正处于忙碌状态,因此将会阻塞其他所有在array上需要同步的代码。  
   
    例3:  
   
  using   namespace   System;  
  using   namespace   System::Threading;  
   
  public   ref   struct   ArrayManip  
  {  
   static   int   TotalValues(array<int>^   array)  
   {  
    /*1*/   int   sum   =   0;  
    /*2*/   Monitor::Enter(array);  
    {  
     for   (int   i   =   0;   i   <   array->Length;   ++i)  
     {  
      sum   +=   array[i];  
     }  
    }  
    Monitor::Exit(array);  
    return   sum;  
   }  
   
   static   void   SetAllValues(array<int>^   array,   int   newValue)  
   {  
    /*3*/   Monitor::Enter(array);  
    {  
     for   (int   i   =   0;   i   <   array->Length;   ++i)  
     {  
      array[i]   =   newValue;  
     }  
    }  
    Monitor::Exit(array);  
   }  
   
   static   void   CopyArrays(array<int>^   array1,   array<int>^   array2)  
   {  
    /*4*/   Monitor::Enter(array1);  
    {  
     /*5*/   Monitor::Enter(array2);  
     {  
      Array::Copy(array1,   array2,    
       array1->Length   <   array2->Length   ?   array1->Length  
       :   array2->Length);  
     }  
     Monitor::Exit(array2);  
    }  
    Monitor::Exit(array1);  
   }  
  };    
   
    一个同步锁可包含同一对象的另一个同步锁,在这种情况下,锁计数相应地增长了;但如果想被另一个线程中的同步语句操作,必须先递减到零。一个同步锁还可包含不同对象的同步锁,在此情况下,它将会一直阻塞,直到第二个对象可访问,函数CopyArrays就是一个例子。Top

3 楼Eddie005(♂) №.零零伍 (♂)回复于 2006-09-27 10:56:40 得分 15

顶~~Top

4 楼nethaoke(豪客)回复于 2006-09-27 19:19:57 得分 15

牛Top

5 楼sstower(幽幽)回复于 2006-09-27 22:17:56 得分 20

不错不错Top

6 楼DentistryDoctor(不在无聊中无奈,就在沉默中变态)回复于 2006-10-04 18:11:59 得分 50

多线程,还得因应用的场景而议。Top

相关问题

关键词

得分解答快速导航

  • 帖主:celineshi
  • Eddie005
  • nethaoke
  • sstower
  • DentistryDoctor

相关链接

  • CSDN Blog
  • 技术文档
  • 代码下载
  • 第二书店
  • 读书频道

广告也精彩

反馈

请通过下述方式给我们反馈
反馈
提问
惹火投票。。火热进行中...
网站简介|广告服务|VIP资费标准|银行汇款帐号|网站地图|帮助|联系方式|诚聘英才|English|问题报告
北京创新乐知广告有限公司 版权所有, 京 ICP 证 070598 号
世纪乐知(北京)网络技术有限公司 提供技术支持
CSDN网站24小时值班电话:13552009689
Copyright © 2000-2009, CSDN.NET, All Rights Reserved
GongshangLogo