副标题#e#
其他形式的同步
我们可利用类Monitor与类Thread中的某些函数,直接节制线程的同步,请看例1。
例1:
using namespace System;
using namespace System::Threading;
int main()
{
/*1*/ MessageBuffer^ m = gcnew MessageBuffer;
/*2a*/ ProcessMessages^ pm = gcnew ProcessMessages(m);
/*2b*/ Thread^ pmt = gcnew Thread(gcnew ThreadStart(pm,&ProcessMessages::ProcessMessagesEntryPoint));
/*2c*/ pmt->Start();
/*3a*/ CreateMessages^ cm = gcnew CreateMessages(m);
/*3b*/ Thread^ cmt = gcnew Thread(gcnew ThreadStart(cm, &CreateMessages::CreateMessagesEntryPoint));
/*3c*/ cmt->Start();
/*4*/ cmt->Join();
/*5*/ pmt->Interrupt();
/*6*/ pmt->Join();
Console::WriteLine("Primary thread terminating");
}
public ref class MessageBuffer
{
String^ messageText;
public:
void SetMessage(String^ s)
{
/*7*/ Monitor::Enter(this);
messageText = s;
/*8*/ Monitor::Pulse(this);
Console::WriteLine("Set new message {0}", messageText);
Monitor::Exit(this);
}
void ProcessMessages()
{
/*9*/ Monitor::Enter(this);
while (true)
{
try
{
/*10*/ Monitor::Wait(this);
}
catch (ThreadInterruptedException^ e)
{
Console::WriteLine("ProcessMessage interrupted");
return;
}
Console::WriteLine("Processed new message {0}", messageText);
}
Monitor::Exit(this);
}
};
public ref class CreateMessages
{
MessageBuffer^ msg;
public:
CreateMessages(MessageBuffer^ m)
{
msg = m;
}
void CreateMessagesEntryPoint()
{
for (int i = 1; i <= 5; ++i)
{
msg->SetMessage(String::Concat("M-", i.ToString()));
Thread::Sleep(2000);
}
Console::WriteLine("CreateMessages thread terminating");
}
};
public ref class ProcessMessages
{
MessageBuffer^ msg;
public:
ProcessMessages(MessageBuffer^ m)
{
msg = m;
}
void ProcessMessagesEntryPoint()
{
msg->ProcessMessages();
Console::WriteLine("ProcessMessages thread terminating");
}
};
#p#副标题#e#
在标志1中,建设一个MessageBuffer范例的共享缓冲区;接着在标志2a、2b、2c中,建设了一个线程用于处理惩罚安排于缓冲区中的每条信息;标志3a、3b和3c,也建设了一个线程,并在共享缓冲区中安排了持续的5条信息以便处理惩罚。这两个线程已被同步,因此处理惩罚者线程必需比及有"对象"放入到缓冲区中,才可以举办处理惩罚,且在前一条信息被处理惩罚完之前,不能放入第二条信息。在标志4中,将一直期待,直到建设者线程完成它的事情。
当标志5执行时,处理惩罚者线程必需处理惩罚所有建设者线程放入的信息,因为利用了Thread::Interrupt让其遏制事情,并继承期待标志6中挪用的Thread::Join,这个函数答允挪用线程阻塞它本身,直到其他线程竣事。(一个线程可指定一个期待的最大时间,而不消无限期待下去。)
线程CreateMessages很是清晰明白,它向共享缓冲区中写入了5条信息,并在每条信息之间期待2秒。为把一个线程挂起一个给定的时间(以毫秒计),我们挪用了Thread::Sleep,在此,一个睡眠的线程可再继承执行,原因在于运行时情况,而不是另一个线程。
线程ProcessMessages甚至越发简朴,因为它操作了类MessageBuffer来做它的所有事情。类MessageBuffer中的函数是被同步的,因此在同一时间,只有一个函数能会见共享缓冲区。
主措施首先启动处理惩罚者线程,这个线程会执行ProcessMessages,其将得到父工具的同步锁;然而,它当即挪用了标志10中的Wait函数,这个函数将让它一直期待,直到再次被告之运行,期间,它也交出了同步锁,这样,答允建设者线程获得同步锁并执行SetMessage。一旦函数把新的信息放入到共享缓冲区中,就会挪用标志8中的Pulse,其答允期待同步锁的任意线程被叫醒,并继承执行下去。可是,在SetMessage执行完成之前,这些都不行能产生,因为它在函数返回前都不行能交出同步锁。假如环境一旦产生,处理惩罚者线程将从头获得同步锁,并从标志10之后开始继承执行。此处要说明的是,一个线程即可无限期待,也可比及一个指定的时间达到。插1是措施的输出。
插1:
#p#分页标题#e#
Set new message M-1
Processed new message M-1
Set new message M-2
Processed new message M-2
Set new message M-3
Processed new message M-3
Set new message M-4
Processed new message M-4
Set new message M-5
Processed new message M-5
CreateMessages thread terminating
ProcessMessage interrupted
ProcessMessages thread terminating
Primary thread terminating
请仔细寄望,处理惩罚者线程启动于建设者线程之前。假如以相反的顺序启动,将会在没有处理惩罚者线程期待的环境下,添加第一条信息,此时,没有可供叫醒处理惩罚者线程,当处理惩罚者线程运行到它的第一个函数挪用Wait时,将会错过第一条信息,且只会在第二条信息存储时被叫醒。
打点线程
默认环境下,假如一个线程是前台线程,它将会一直执行下去,直到进入点函数竣事,而不管它父类的生命期是多久;而在另一方面,靠山线程则会在父类线程竣事时自动竣事。可通过配置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();
}
#p#分页标题#e#
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作为一个普通函数被挪用,而不是作为建设的新线程的一部门。
原子性与互锁操纵
假如存在这样一种环境:一个应用措施有多个线程并行运行,每个线程对某些共享的整形变量,都有写操纵–只是简朴地利用++把变量递增1。这看起来好像没什么问题,究竟,还算像是一个原子性操纵,但在大都系统中–至少从呆板指令的角度来看,C++/CLI执行情况对所有整形范例,并不能普各处担保无误。
作为示例,例4中的措施有三个线程,每个线程都同时递增一个共享的64位整形变量一千万次,最后显示出这个变量的最终值,从理论上说,应该共递增了三千万次。这个措施今朝可以两种方法运行:默认方法利用++操纵符以非同步方法运行;而另一种方法,通过带有呼吁行参数Y或y,这回利用了一个同步的库递增函数。
例4:
using namespace System;
using namespace System::Threading;
static bool interlocked = false;
const int maxCount = 10000000;
/*1*/ static long long value = 0;
void TMain()
{
if (interlocked)
{
for (int i = 1; i <= maxCount; ++i)
{
/*2*/ Interlocked::Increment(value);
}
}
else
{
for (int i = 1; i <= maxCount; ++i)
{
/*3*/ ++value;
}
}
}
int main(array<String^>^ argv)
{
if (argv->Length == 1)
{
if (argv[0]->Equals("Y") || argv[0]->Equals("y"))
{
interlocked = true;
}
}
/*4*/ Thread^ t1 = gcnew Thread(gcnew ThreadStart(&TMain));
Thread^ t2 = gcnew Thread(gcnew ThreadStart(&TMain));
Thread^ t3 = gcnew Thread(gcnew ThreadStart(&TMain));
t1->Start();
t2->Start();
t3->Start();
t1->Join();
t2->Join();
t3->Join();
Console::WriteLine("After {0} operations, value = {1}", 3 * maxCount, value);
}
当利用尺度++操纵符时,措施5次持续执行之后,输出如插3所示,可看出,功效与正确谜底相距甚远,简朴估算,或许有17%至50%的递增操纵未正确完成;当措施运行于同步方法时–纵然用Interlocked::Increment,所有的三千万次递增操纵都正常完成,功效计较正确。
插3:
利用++操纵符的输出
#p#分页标题#e#
After 30000000 operations, value = 14323443
After 30000000 operations, value = 24521969
After 30000000 operations, value = 20000000
After 30000000 operations, value = 24245882
After 30000000 operations, value = 25404963
利用Interlocked递增函数的输出
After 30000000 operations, value = 30000000
别的,增补一点,Interlocked类尚有另一个decrement函数。