.NET 4.0 多线程开发系列之
对象的延迟创建与多线程安全访问
=========================
版权声明:
本文作者金旭亮拥有此文的原创版权,任何人均可以出于学习与交流目的在网络中共享与传播此文,但不得用于商业目的,比如用于出版技术书籍或者进行以盈利为目的的商业培训。
另外,如有转贴请注明出处。
有培训需求的单位请直接与本人联系。
此声明适用于本人在互联网上发表的所有原创类型文章和相关的技术与教学资源。
====================================
1 使用多线程延迟创建“唯一”的对象
在实际开发中,我们可能会希望将一个对象的创建延迟到需要真正用到它的时候。最典型的是使用数据库连接对象访问数据库。在分布式的软件系统中,客户端与服务器一般不会在同一台计算机上,创建数据库对象并启动到远程数据库连接是一件比较耗废系统资源的事情。当然,通过编写一些条件判断语句我们可以实现“在需要的时候才创建对象”这一目标。
以下是一段典型代码 :
class Parent
{
A obj = null ;
public void VisitEmbedObject()
{
if (obj == null )
obj = new A ();
}
}
然而,当延迟动态创建的对象会被多线程共享访问时,就麻烦了,想想上述代码放在线程函数中,由多个线程同时执行,如果不施加任何的同步手段, A 对象可能会被创建多个!这很容易理解,由于操作系统采用分时时间片的调度方法将 CPU 分配给特定线程执行,因此完全可能发生某个线程还没有执行完毕,另一个线程又投入运行的情况。
在本例中,有可能一个线程在创建对象过程中(而此时 obj 仍是 null ),另一个线程尝试访问 obj 会发现它仍是 null ,于是又会创建另一个 A 对象!
在过去,为了解决这个问题,一般需要给多线程共享资源加锁:
class Parent
{
A obj = null ;
public void VisitEmbedObject()
{
lock ( this )
{
if (obj == null )
obj = new A ();
}
//...
}
}
这个方法是传统的编程方法。
然而,到了 .NET 4.0 ,有更简单更方便的方法达到同样的目的。
2 泛型类 Lazy<T>
泛型类 Lazy<T> 位于 System 命名空间,是 .NET 4.0 新引入的。它的功能就是解决多线程运行环境下的对象延迟创建问题。
通过实例可以很清楚地掌握它的用法( Demo : UseLazyExample )。
本例中我们定义了一个很简单的类型 A :
class A
{
public A()
{
Console.WriteLine("A 对象创建,其标识: {0}",this.GetHashCode());
}
public int IntValue
{
get; set;
}
}
以下代码实现了对象的延迟创建:
class Program
{
static void Main(string[] args)
{
Lazy<A> AObj = new Lazy<A>();
Console.WriteLine(" 现在将给 A 对象的 IntValue 属性赋值 100");
A obj = AObj.Value; // 此处导致对象创建!
obj.IntValue = 100;
Console.WriteLine("A 对象 {0} 的 IntValue 属性 ={1}",
obj.GetHashCode(),obj.IntValue);
Console.ReadKey();
}
}
运行结果如下:
<!-- [if gte vml 1]><v:shapetype id="_x0000_t75" coordsize="21600,21600" o:spt="75" o:preferrelative="t" path="m@4@5l@4@11@9@11@9@5xe" filled="f" stroked="f"> <v:stroke joinstyle="miter" /> <v:formulas> <v:f eqn="if lineDrawn pixelLineWidth 0" /> <v:f eqn="sum @0 1 0" /> <v:f eqn="sum 0 0 @1" /> <v:f eqn="prod @2 1 2" /> <v:f eqn="prod @3 21600 pixelWidth" /> <v:f eqn="prod @3 21600 pixelHeight" /> <v:f eqn="sum @0 0 1" /> <v:f eqn="prod @6 1 2" /> <v:f eqn="prod @7 21600 pixelWidth" /> <v:f eqn="sum @8 21600 0" /> <v:f eqn="prod @7 21600 pixelHeight" /> <v:f eqn="sum @10 21600 0" /> </v:formulas> <v:path o:extrusionok="f" gradientshapeok="t" o:connecttype="rect" /> <o:lock v:ext="edit" aspectratio="t" /> </v:shapetype><v:shape id="图片_x0020_1" o:spid="_x0000_i1028" type="#_x0000_t75" style='width:267.75pt;height:92.25pt;visibility:visible;mso-wrap-style:square'> <v:imagedata src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image001.png" mce_src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image001.png" o:title="" /> </v:shape><![endif]--><!-- [if !vml]--><!-- [endif]-->
上述代码虽然实现了对象的延迟创建,但示例代码运行于单线程环境下,还没有显示出使用 泛型类 Lazy<T> 的好处。
3 多线程环境下使用泛型类 Lazy<T>
考虑一下新的编程场景:
现在有多个线程都在运行中,这些线程都需要调用 A 类型的对象所提供的功能。我们希望只创建一个 A 对象并且能让多个线程安全地访问它。
请看示例 UseLazyInMultiThreadEnvironment 。这是一控制台程序,以下代码位于 Program 类中。
首先定义一个用于多线程共享的对象 AObj ,注意它使用 Lazy<T> 进行了封装:
static Lazy < A > AObj = null ;
紧接着定义一个用于创建 A 对象的工厂函数:
static Func < A > valueFactory = delegate ()
{
Console .WriteLine( " 调用工厂函数创建 A 对象 " );
A obj = new A { IntValue = ( new Random ()).Next(1,100) };
return obj;
};
注意上面用到了 C# 中的匿名方法实现给委托变量赋值。之所以将这个方法称为“工厂函数”,来自于《设计模式》一书中的“抽象类工厂”设计模式,简言之,可将负责创建特定类型对象的函数称为“工厂”,创建出来的对象就是这个工厂的“产品”。
紧接着是一个线程函数,将被多个线程同时执行:
static void ThreadFunc()
{
Console.WriteLine(" 对象 {0} 的 IntValue={1}",
AObj.Value .GetHashCode(), AObj.Value .IntValue);
}
注意:上面访问共享对象是通过 Lazy<A> 进行的,这是实现多线程同步的关键所在。
好了,以下是实验代码:
static void Main(string[] args)
{
Console.WriteLine("/n 敲任意键开始演示, ESC 退出 ...");
while (Console.ReadKey(true).Key != ConsoleKey.Escape)
{
Console.WriteLine();
// 注意将第 2 个参数改为不同的值:
//1 NotThreadSafe
//2 AllowMultipleThreadSafeExecution
//3 EnsureSingleThreadSafeExecution
// 运行看看结果有何不同?
AObj = new Lazy<A>(valueFactory, LazyExecutionMode.EnsureSingleThreadSafeExecution);
for (int i = 0; i < 10; i++)
{
Thread th = new Thread(ThreadFunc);
th.Start();
}
}
}
}
上述代码运行时,敲任意键将创建 10 个线程,这 10 个线程将尝试访问同一个 A 类型的对象。
这里要特别注意 Lazy<A> 的构造函数,它的第一个参数表示当创建共享对象时要调用的工厂函数,第二个参数对程序的执行有着重大影响。以下是部分实验结果:
LazyExecutionMode. NotThreadSafe
<!-- [if gte vml 1]><v:shape id="图片_x0020_4" o:spid="_x0000_i1027" type="#_x0000_t75" style='width:243.75pt;height:284.25pt;visibility:visible; mso-wrap-style:square'> <v:imagedata src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image003.png" mce_src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image003.png" o:title="" /> </v:shape><![endif]--><!-- [if !vml]--><!-- [endif]-->
可以看到, 10 个线程运行时创建了 3 个对象,不同线程得到的值可能相同也可能不同,而且程序执行时对象创建的次数还会有变化。
LazyExecutionMode. AllowMultipleThreadSafeExecution
<!-- [if gte vml 1]><v:shape id="图片_x0020_7" o:spid="_x0000_i1026" type="#_x0000_t75" style='width:237.75pt; height:260.25pt;visibility:visible;mso-wrap-style:square'> <v:imagedata src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image005.png" mce_src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image005.png" o:title="" /> </v:shape><![endif]--><!-- [if !vml]--><!-- [endif]-->
可以看到,虽然在这种情况下 A 对象也创建了多个,但多个线程最终访问的却是同一个对象,这个同步是由 Lazy<A> 实现的。
LazyExecutionMode. EnsureSingleThreadSafeExecution
<!-- [if gte vml 1]><v:shape id="图片_x0020_10" o:spid="_x0000_i1025" type="#_x0000_t75" style='width:213.75pt; height:236.25pt;visibility:visible;mso-wrap-style:square'> <v:imagedata src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image007.png" mce_src="file:///D:/Users/JINXUL~1/AppData/Local/Temp/msohtmlclip1/01/clip_image007.png" o:title="" /> </v:shape><![endif]--><!-- [if !vml]--><!-- [endif]-->
可以看到,不管有多个线程, Lazy<A> 将保证只调用工厂函数一次,仅创建一个 A 对象。
注意:
Lazy<T>仅能保证多线程访问的是同一个对象,但这并不是说Lazy<T>能自动同步对此共享对象的访问。这意味着您必须在线程函数中使用lock等线程同步手段避免“多线程访问共享资源导致数据存取错误”现象的发生。
4 小结:
.NET 4.0 是 .NET 历史上一个重要的版本,引入了不少新的技术,同时对原有的组件也进行了更新,在后面的系列文章中,我将带领大家再去探索 .NET 4.0 多线程开发的另外一些实用的开发技巧。