科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道基础软件实例解析C++/CLI线程之线程状态持久性

实例解析C++/CLI线程之线程状态持久性

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

我们可使用类Monitor与类Thread中的某些函数,直接控制线程的同步。

作者:谢启东编译 来源:天极开发 2007年11月14日

关键字:

  • 评论
  • 分享微博
  • 分享邮件
管理线程

  默认情况下,如果一个线程是前台线程,它将会一直执行下去,直到进入点函数结束,而不管它父类的生命期是多久;而在另一方面,后台线程则会在父类线程结束时自动结束。可通过设置Thread的IsBackground属性,把一个线程配置为后台线程,用同样的方法,也可把一个后台线程配置为前台线程。

  一旦线程被启动,它即为活跃线程,可通过检查Thread的IsAlive属性来判断一个线程是否为活跃线程;通过调用Wait函数,并传递给它一个零毫秒,可使一个线程放弃剩余的CPU时间片;另外,线程还可通过CurrentThread::Thread::CurrentThread属性得到其自己的Thread对象。

  每个线程都有与之相关的优先级,运行时环境(即操作系统)通过它来调度线程的执行,可通过Thread::Priority属性来设置或检测线程的优先级,它的范围从ThreadPriority::Lowest 到ThreadPriority::Highest,默认情况下,线程的优先级为ThreadPriority::Normal。另外,因为实现环境的不同,线程调度会有所不同,所以在控制线程方面,不应该过分依赖线程的优先级。

  易变字段(域)

  volatile这个限定类型告诉编译器,可能会有多个线程控制或访问它所指定的对象,尤其是,一个或多个线程可能将异步读写此变量。基本上,这个限定词是强制编译器在进行优化时不要那么"激进"。

  请看例2中的代码段,在缺少volatile时,标记1中的代码完全可以忽略,因为在标记2中立即就改写了i的值;然而,指定了volatile后,编译器则必须执行这两行代码。

  例2:

volatile int i = 0;
/*1*/ i = 10;
/*2*/ i = 20;
/*3*/ if (i < 5 || i > 10) {
// ...
}

int copy = i;
/*4*/ if (copy < 5 || copy > 10) {
// ...
}

  在标记3中,编译器必须生成取回值i的代码两次,但是,在两次取值过程中,数值都有可能改变。为确保我们测试的是同一个值,在此不得不以类似标记4的代码来代替。通过把值i的一个快照存储在一个非易变的变量中,我们就可以安全地多次使用这个值了--因为它的值不可能在"后台"改变。在此,使用volatile,可避免对特定类型变量的显式异步访问。

  线程局部存储

  当编写多线程应用程序时,只在特定的线程中使用特定的变量,这是一个非常好的习惯,请看例3的程序:

  例3:

using namespace System;
using namespace System::Threading;

public ref class ThreadX
{
 /*1*/ int m1;
 /*2*/ static int m2 = 20;
 /*3*/ [ThreadStatic] static int m3 = 30;

 public:
  ThreadX()
  {
   m1 = 10;
  }
 
  void TMain()
  {
   String^ threadName = Thread::CurrentThread->Name;

   /*4*/ Monitor::Enter(ThreadX::typeid);
   for (int i = 1; i <= 5; ++i)
   {
    ++m1;
    ++m2;
    ++m3;
   }
   Console::WriteLine("Thread {0}: m1 = {1}, m2 = {2}, m3 = {3}",
threadName, m1, m2, m3);
   Monitor::Exit(ThreadX::typeid);
 }
};

int main()
{
 /*5*/ Thread::CurrentThread->Name = "t0";

 ThreadX^ o1 = gcnew ThreadX;
 Thread^ t1 = gcnew Thread(gcnew ThreadStart(o1, &ThreadX::TMain));
 t1->Name = "t1";

 ThreadX^ o2 = gcnew ThreadX;
 Thread^ t2 = gcnew Thread(gcnew ThreadStart(o2, &ThreadX::TMain));
 t2->Name = "t2";

 t1->Start();
 /*6*/ (gcnew ThreadX)->TMain();
 t2->Start();
 t1->Join();
 t2->Join();
}

  m1是一个实例字段,所以每个ThreadX的实例都有一份各自的拷贝,且在父类对象的生命期中都会存在;而另一方面,m2是一个类字段,所以对类来说,不管有几个类的实例,它只有单独的一个,从理论上来说,它将会一直存在,直到程序结束。但这两个字段都不是特定于某个线程的,如果以适当的构造,这两种类型的字段都能被多个线程访问。

  简单来说,线程局部存储就是特定线程拥有的某段内存,这段内存在新线程创建时被分配,而在线程结束时被释放,它结合了局部变量的私有性和静态变量的持久性。通过指定ThreadStatic属性,可把一个字段标记为线程局部类型,如例中的标记3所示,在成为静态字段之后,m3甚至还能有一个初始化函数。

  函数TMain为新线程的入口点,这个函数只是简单地递增这三个变量:m1、m2和m3,每回5次,并打印出它们当前的值。标记4中的同步锁保证了在这些字段递增或打印时,另一个线程不会同时访问它们。

  在标记5中,主线程把它的名字设置为t0,接着创建并启动了两个线程,另外,它也把TMain当作了一个普通函数直接调用,而不是作为创建的新线程的一部分来调用。程序的输出请见插2。

  插2:

Thread t0: m1 = 15, m2 = 25, m3 = 35
Thread t1: m1 = 15, m2 = 30, m3 = 5
Thread t2: m1 = 15, m2 = 35, m3 = 5

  每个线程都有其自己的m1实例,它被初始化为10,所以在递增5次之后,每个线程中的值都为15。而m2则有所不同,所有的三个线程都共享同一变量,所以这一变量被递增了15次。

  线程t1与t2在经过线程创建过程之后,每个都有其自己的m3,然而,这些线程局部变量会被赋予默认的零值,而不是在源代码中初始化的30,注意了,在经过5次递增之后,各个值均为5,而线程t0则有所不同,正如我们所看到的,这个线程不是由创建其他两个线程同样的机制创建的,所以,它的m3会接受显式初始化的值30。同时也请注意标记6,TMain作为一个普通函数被调用,而不是作为创建的新线程的一部分。
    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章