管理线程
默认情况下,如果一个线程是前台线程,它将会一直执行下去,直到进入点函数结束,而不管它父类的生命期是多久;而在另一方面,后台线程则会在父类线程结束时自动结束。可通过设置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作为一个普通函数被调用,而不是作为创建的新线程的一部分。