当前位置:天才代写 > tutorial > C语言/C++ 教程 > 实例理会C++/CLI线程之多任务

实例理会C++/CLI线程之多任务

2017-11-07 08:00 星期二 所属: C语言/C++ 教程 浏览:1483

副标题#e#

简介

从处理惩罚器的角度来看,线程是一个单独的执行流程,每个线程都有各自的寄存器及仓库上下文。凡是来说,在系统中只有一个处理惩罚器或处理惩罚器只有一个焦点时,运行时情况在一个时间片内只能执行一个线程,当线程未能获取所需的资源时,线程的执行就会被间断,且会一直比及相关操纵的完成,如I/O;可能在线程用完它的处理惩罚器时间片时,也会被间断下来期待。而处理惩罚器把执行流程从一个线程切换到另一个线程时,这称为"上下文切换";当某个线程变为"阻塞"状态,从而执行另一个线程时,系统有效地淘汰了处理惩罚器空闲时间,这称为"多任务"。

当措施执行时,系统知道可以从磁盘上某处获取相关的指令及静态数据,措施会被分派到一组包括虚拟内存在内的地点空间,这个运行时上下文被称为"历程"。然而,在一个历程可以运行之前,它必需拥有至少一个线程,也就是说,当一个历程被建设时,它自动被赋予了一个线程,这称为"主线程"。可是话说返来,这个线程与之后这个历程所建设的线程对比,没有任何差异之处,它只不外刚好是这个历程的第一个线程罢了。一般来说,在措施的节制之下,历程内的线程数在运行时会有所变革,任何线程都可以建设其他的线程,但不管奈何,线程不拥有它所建设的线程,所有历程内的线程都是作为一个整体属于这个历程。

可把历程要完成的事情分成差异的"子任务",每一部门都由差异的线程来执行,这称为"多线程"。历程内的每个线程共享同样的地点空间与历程资源,当最后一个历程内的线程竣事时,父历程就竣事了。

为何历程内要有多个线程呢?假如历程只有一个线程,那么它的执行流程是自上而下顺序执行的;当线程阻塞,而又没有其他的勾当线程处于期待状态时,系统就会进入空闲状态;假如此时历程的子任务必需被顺序地执行,那么这种环境就不行制止,将耗费大量的时间来期待。然而,绝大大都的历程都不是这样的,试想有这样一种环境,某个历程有多个选项,用户可以选择个中一些选项,由此发生的计较会利用内存或文件中的数据,并生成功效,假如能从中分出一些新的线程,那么历程不必期待前一个计较的功效,就可以继承接管新的计较请求。另外,通过指定线程的优先级,历程可只在更要害的线程阻塞时,才运行次要害的线程。

在有多个线程的环境下,某些线程可认真措施的主要事情,而另一个线程可用于处理惩罚键盘和鼠标的输入。譬喻,用户大概会以为前一次请求并不是期望的行动,从而但愿打消由前一次请求发生的那一个线程,这时就可在某个下拉菜单中举办选择,由一个线程去终止另一个线程。

另一个例子就是打印假脱机措施,它的任务是保持打印机尽大概地满载事情,并处理惩罚用户的打印请求;假如这个措施必需要比及前一项打印事情完成,才气接管新请求的话,用户大概会感想很是的不满。虽然,措施也可周期性地停下打印事情,来查察是否有新的未处理惩罚请求(这称为"轮询"),可是,假如没有新请求,这将会很是挥霍时间。别的,假如轮询的隔断时间太长,对处理惩罚新请求,还会造成延时;假如隔断太短,那么线程在轮询上耗费的时间又太多。那么,为什么不让假脱机措施有两个线程呢?一个用于将打印事情通报到打印机,而另一个用于处于用户的请求,它们之间都彼此独立运行;而当一个线程事情完成时,它要么竣事自身,要么进入休眠状态。

当处理惩罚并发的执行线程时,必需要首先相识两个重要的观念:原子性和重入性。一个原子变量或工具是作为一个整体被会见的,甚至于在异步操纵的环境下也是如此–会见的是同一个变量或工具。举例来讲,假如一个线程正在更新一个原子变量或工具,而另一个线程在读取其内容,此时来讲,内容逻辑上的完整性是不行能被粉碎的,所以,要么读取到旧值,要么读取到新值,而不会旧值新值各读一部门。凡是来说,能被原子性会见的变量或工具,只是那些在硬件上能被原子性支持的范例,如字节(Byte)和字(Word)。C++/CLI中大大都的根基范例都确保具有原子性,剩下的范例也可被某种特定的实现支持原子性,但不能百分百担保。显而易见,一个实现了x与y坐标对的Point工具,不具有原子性,对Point值的写入,大概会被对其值的读取间断,功效就是,读取到了一个新的x值和一个旧的y值,反之亦然;同样地,数组也不行能被原子性地会见。正是因为大大都的工具不能被原子性地会见,所以必需利用一些同步形式来担保在某一时间,只有一个线程可哄骗某个特定的工具。也正是因为此,C++/CLI分派给每一个工具、数据和类一个同步锁。


#p#副标题#e#

#p#分页标题#e#

一个重入的函数可由多个线程安详地并行执行。当线程开始执行一个函数时,在函数中分派的所有数据都来自栈或堆,但无论如何,对此挪用来说,都是独一的。假如在另一个线程仍处于事情状态时,本线程开始执行同一个函数,那么,每个线程中的数据都是彼此独立的。然而,假如函数会见线程间共享的变量或文件时,则必需利用某些同步要领。

建设线程

在例1中,主线程建设了两个其他的线程,这三个线程并行运行,而且未举办同步。在线程间并未共享数据,且当最后一个线程竣事时,主历程也竣事了。

例1:

using namespace System;
using namespace System::Threading;
public ref class ThreadX
{
  int loopStart;
  int loopEnd;
  int dispFrequency;
  public:
   ThreadX(int startValue, int endValue, int frequency)
   {
    loopStart = startValue;
    loopEnd = endValue;
    dispFrequency = frequency;
   }
   /*1*/ void ThreadEntryPoint()
   {
    /*2*/ String^ threadName = Thread::CurrentThread->Name;
    for (int i = loopStart; i <= loopEnd; ++i)
    {
     if (i % dispFrequency == 0)
     {
      Console::WriteLine("{0}: i = {1,10}", threadName, i);
     }
    }
    Console::WriteLine("{0} thread terminating", threadName);
   }
};
int main()
{
  /*3a*/ ThreadX^ o1 = gcnew ThreadX(0, 1000000, 200000);
  /*3b*/ Thread^ t1 = gcnew Thread(gcnew ThreadStart(o1, &ThreadX::ThreadEntryPoint));
  /*3c*/ t1->Name = "t1";
  /*4a*/ ThreadX^ o2 = gcnew ThreadX(-1000000, 0, 200000);
  /*4b*/ Thread^ t2 = gcnew Thread(gcnew ThreadStart(o2, &ThreadX::ThreadEntryPoint));
  /*4c*/ t2->Name = "t2";
  /*5*/ t1->Start();
  /*6*/ t2->Start();
  Console::WriteLine("Primary thread terminating");
}

#p#副标题#e#

请看标志3a中第一条可执行语句,此处我们建设了一个用户自界说ThreadX范例的工具,这个类有一个结构函数、一个实例函数及三个字段。我们挪用结构函数时,通报进一个开始、一个竣事计数,及一个牢靠增量,其用于轮回节制。

在标志3b中,建设了一个库范例System::Thread的工具,它源自定名空间System::Threading,可用此工具来建设一个新的线程,可是,在线程可以事情之前,它必需要知道从哪开始执行,所以通报给Thread结构函数一个System::ThreadStart署理范例,其可支持不接管参数的任意函数,且没有返回值(作为一个署理,它可封装进多个函数,在本例中,只指定了一个)。在上面的代码中,指定了线程由执行工具o1的ThreadEntryPoint实例函数开始,一旦开始之后,这个线程将会执行下去直到函数竣事。最后,在标志3c中,随意利用了一个名称,以配置它的Name属性。

请看标志4a、4b及4c,第二个线程也一样,只不外配置了差异的轮回节制及名称。

眼下,已结构了两个线程工具,但并未建设新的线程,也就是说,这些线程处于未激活状态。为激活一个线程,必需挪用Thread中的Start函数,见标志5与6。通过挪用进入点函数,这个函数启动了一个新的执行线程(对一个已经激活的函数挪用Start将导致一个ThreadStateException范例异常)。两个新的线程都各自显示出它们的名称,并在轮回中按时地显示它们的进度,因为每个线程都执行其自身的实例函数,所以每个线程都有其本身的实例数据成员集。

所有三个线程均写至尺度输出,见插1,可看出线程中的输出是缠绕在一起的(虽然,在后续的执行中,输出也大概有差异的顺序)。可见,主线程在其他两个线程启动之前就竣事了,这证明白尽量主线程是其他线程的父类,但线程的生命期是无关的。固然,例中利用的进入点函数无关紧急,但其可挪用它可会见的任意其他函数。 插1:三个线程的缠绕输出

#p#副标题#e#

Primary thread terminating
t1: i = 0
t1: i = 200000
t1: i = 400000
t1: i = 600000
t2: i = -1000000
t2: i = -800000
t2: i = -600000
t2: i = -400000
t2: i = -200000
t2: i = 0
t2 thread terminating
t1: i = 800000
t1: i = 1000000
t1 thread terminating

假如想让差异的线程由差异的进入点函数开始,只需简朴地在同一或差异的类中,界说这些函数就行了(或作为非成员函数)。

同步语句

#p#分页标题#e#

例2中的主措施有两个线程会见同一Point,个中一个不绝地把Point的x与y坐标配置为一些新值,而另一个取回并显示这些值。纵然两个线程由同一进入点函数开始执行,通过通报一个值给它们的结构函数,可使每个线程的行为都有所差异。

例2:

using namespace System;
using namespace System::Threading;
public ref class Point
{
  int x;
  int y;
  public:
   //界说读写会见器
   property int X
   {
    int get() { return x; }
    void set(int val) { x = val; }
   }
   property int Y
   {
    int get() { return y; }
    void set(int val) { y = val; }
   }
   // ...
   void Move(int xor, int yor)
   {
    /*1a*/ Monitor::Enter(this);
    X = xor;
    Y = yor;
    /*1b*/ Monitor::Exit(this);
   }
   virtual bool Equals(Object^ obj) override
   {
    // ...
    if (GetType() == obj->GetType())
    {
     int xCopy1, xCopy2, yCopy1, yCopy2;
     Point^ p = static_cast<Point^>(obj);
     /*2a*/ Monitor::Enter(this);
     xCopy1 = X;
     xCopy2 = p->X;
     yCopy1 = Y;
     yCopy2 = p->Y;
     /*2b*/ Monitor::Exit(this);
     return (xCopy1 == xCopy2) && (yCopy1 == yCopy2);
    }
    return false;
   }
   virtual int GetHashCode() override
   {
    int xCopy;
    int yCopy;
    /*3a*/ Monitor::Enter(this);
    xCopy = X;
    yCopy = Y;
    /*3b*/ Monitor::Exit(this);
    return xCopy ^ (yCopy << 1);
   }
   virtual String^ ToString() override
   {
    int xCopy;
    int yCopy;
    /*4a*/ Monitor::Enter(this);
    xCopy = X;
    yCopy = Y;
    /*4b*/ Monitor::Exit(this);
    return String::Concat("(", xCopy, ",", yCopy, ")");
   }
};
public ref class ThreadY
{
  Point^ pnt;
  bool mover;
  public:
   ThreadY(bool isMover, Point^ p)
   {
    mover = isMover;
    pnt = p;
   }
   void StartUp()
   {
    if (mover)
    {
     for (int i = 1; i <= 10000000; ++i)
     {
      /*1*/ pnt->Move(i, i);
     }
    }
    else
    {
     for (int i = 1; i <= 10; ++i)
     {
      /*2*/ Console::WriteLine(pnt); // calls ToString
      Thread::Sleep(10);
     }
    }
   }
};
int main()
{
  Point^ p = gcnew Point;
 
  /*1*/ ThreadY^ o1 = gcnew ThreadY(true, p);
  /*2*/ Thread^ t1 = gcnew Thread(gcnew ThreadStart(o1, &ThreadY::StartUp));
 
  /*3*/ ThreadY^ o2 = gcnew ThreadY(false, p);
  /*4*/ Thread^ t2 = gcnew Thread(gcnew ThreadStart(o2, &ThreadY::StartUp));
  t1->Start();
  t2->Start();
  Thread::Sleep(100);
  /*5*/ Console::WriteLine("x: {0}", p->X);
  /*6*/ Console::WriteLine("y: {0}", p->Y);
  /*7*/ t1->Join();
  t2->Join();
}

#p#副标题#e#

挪用Sleep休眠100毫秒的目标是为了在可以会见x与y坐标之前,让两个线程开始执行,这就是说,我们想要主线程与其他两个线程竞争坐标值的独有会见。

对Thread::Join的挪用将会挂起挪用线程,直到Join挪用的线程竣事。

请看例2中的ThreadY类,当一个线程挪用标志1中的Move,而另一个线程隐式地挪用标志2中的ToString时,潜在的斗嘴就产生了。因为两个函数没有用同步法子来会见同一个Point,Move大概会先更新x坐标,但在它更新相应的y坐标之前,ToString却显示了一对错误的坐标值,这时,输出大概会如插2a所示。然而,当相关的语句被同步之后,ToString显示的坐标对老是正确匹配的,同步执行之后的输出如插2b所示。再看一下例2中的Point范例,在此可看到这些会见x与y坐标的函数是如何被同步的。

插2:a线程输生发生了不匹配的坐标对;b同步执行中匹配的坐标对

(a)

#p#分页标题#e#

(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。

#p#副标题#e#

因为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);
  }
};

#p#副标题#e#

一个同步锁可包括同一工具的另一个同步锁,在这种环境下,锁计数相应地增长了;但假如想被另一个线程中的同步语句操纵,必需先递减到零。一个同步锁还可包括差异工具的同步锁,在此环境下,它将会一直阻塞,直到第二个工具可会见,函数CopyArrays就是一个例子。

一般来说,利用同步锁的目标,是为了利用父类函数的实例工具,然而,我们在不需要这些工具实际包括任何信息的环境下,也能"缔造"出锁工具和同步机制。请看例4,类C有一个名为Lock的同步锁,其并未包括任何数据,且除了一个同步锁外,从未举办初始化或利用在任何上下文中。但在函数F3与F4中,则别离包括了一些语句,各自在运行时必需阻塞对方的运行。

例4:

#p#分页标题#e#

using namespace System::Threading;
public ref class C
{
  /*1*/ static Object^ Lock = gcnew Object;
  public:
   static void F1()
   {
    /*2*/ Monitor::Enter(C::typeid);
    /*3*/ try {
     //执行一些操纵
    }
    finally {
     Monitor::Exit(C::typeid);
    }
   }
   static void F2()
   {
    Monitor::Enter(C::typeid);
    // ...
    Monitor::Exit(C::typeid);
   }
   static void F3()
   {
    /*4*/ Monitor::Enter(Lock);
    // ...
    Monitor::Exit(Lock);
   }
   static void F4()
   {
    Monitor::Enter(Lock);
    // ...
    Monitor::Exit(Lock);
   }
};

假如一个类函数(而不是一个实例函数)需要同步,可利用typeid操纵符来包括一个锁工具,如标志2中所示。对每个CLI范例而言,都有一个锁工具,同样,对范例的每个实例而言,也有一个锁工具。类上的同步锁意味着在同一时刻,只能执行一个类函数。

留意标志3中的try/finally,一般而言,假如同步锁中的执行正常完成,将如前面的例子一样,正常地挪用Monitor::Exit;可是,假如在同步锁中抛出了一个异常,将不会挪用到Exit,因为正常的执行流程已经被间断了。那么我们要做的就是,假如同步锁中大概存在一丝时机产生异常–不管是同步锁中直接或是间接挪用的任何函数,我们都必需加上try/finally语句块,这样的话,不管是同步锁的正常或非正常退出,城市挪用到Exit了。

 

    关键字:

天才代写-代写联系方式