多线程:从对象池(object pool)谈同步(syncronization) ——一个调试的问题
 

2009-02-18 作者:SilenceCliff 来源:网络

 

最近,刚看过Jeffry Richter的《Programming Application for Microsoft Windows 4th Edition》。眼下正看《C# Threading HandBook》。看了前三章,觉得很不错。觉得这本书很系统,自己也想把以前在一些书上看到的分散的东西彻底归整一遍。于是就从这里开个头吧。

还记得Jeffry Richter在《Appiled .NET Framework Programming》里的那个利用对象复苏设计的那个对象池吗?

且请容许我把代码在这里再贴一遍:

  1using System;
  2
  3using System.Collections;
  4
  5 
  6
  7namespace RichtersObjectPool
  8
  9
 10
 11       class Expensive
 12
 13       {
 14
 15              static Stack pool = new Stack();
 16
 17 
 18
 19              public static Expensive GetObjectFromPool()
 20
 21              {
 22
 23                     return (Expensive) pool.Pop();
 24
 25              }

 26
 27 
 28
 29              public static void ShutdownThePool()
 30
 31              {
 32
 33                     pool = null;
 34
 35              }

 36
 37 
 38
 39              public Expensive()
 40
 41              {
 42
 43                     //构造对象花费较长时间
 44
 45                     pool.Push(this);
 46
 47              }

 48
 49 
 50
 51              ~Expensive()
 52
 53              {
 54
 55                     if (pool != null)
 56
 57                     {
 58
 59                            GC.ReRegisterForFinalize(this);
 60
 61 
 62
 63                            pool.Push(this);
 64
 65                     }

 66
 67              }

 68
 69       }

 70
 71 
 72
 73       class App
 74
 75       {
 76
 77              [STAThread]
 78
 79              static void Main(string[] args)
 80
 81              {
 82
 83                     for (int i = 0; i < 10; i++)
 84
 85                            new Expensive();
 86
 87 
 88
 89                     //一些操作
 90
 91 
 92
 93                     Expensive e = Expensive.GetObjectFromPool();
 94
 95 
 96
 97                     //使用e
 98
 99 
100
101                     Expensive.ShutdownThePool();
102
103 
104
105              }

106
107       }

108
109}

110
111

现在且就这个object pool的实现我们来仔细看看。从整个设计来讲,Expensive类是一个多例模式。它通过一个聚集(静态的Stack)来管理该类的多个实例。从技巧上看,利用的是GC的对象复苏特性,即重载了Finalize方法的类(在C#中即是析构函数)在第一次垃圾收集时会经历一个终止化链表到终止化可达队列的转移的过程,如此从“死亡”既而又获得了“重生”。在这个实现中,在Finalize()中调用GC.ReRegisterForFinalize()是实现的关键。

但是,这个实现很大的限制了我们的应用。

我们被限制的应用有哪些呢?

  • 这个object pool还不够智能化,每次我们在开始运行时,要自己手动构造一些对象,在应用程序退出时,还必须牢记要自己关闭object pool。
  • 栈中的元素只能增加,不能减少。即每当我再次构造一个对象压栈后,以前栈中的对象就必须多次调用GetObjectFromPool()才能得到。这个似乎不是很方便。
  • 如果在程序运行期间构造了太多这样的对象,那么势必会耗费很多的资源,而实际上,可能只有在object pool中少数的对象正被使用或经常被使用。在某些场景下,我们需要一种对象生命周期的管理方法。
  • 最后,最重要的是这个类不是线程安全的。

首先,我们可以考虑把管理多个对象的数据结构换做线程安全的哈希表:

在这里我要对HashTable多说两句。《Professinal C# 3rd Edition》里说的很清楚:

  • 容量为素数的话,工作效率更高,且当散列表扩大容量重新分配内存的时候,总会选择一个素数作为其新的容量。
  • 负载最大值越小,工作效率越高,但占据内存也越大。
  • HaskTable确定两个键A和B是否相等的方式是调用A.Equals(B)。即必须确保如果A.Equals(B)是true,则A.GetHashCode()和B.GetHashCode()必须返回相同的散列。

上面的第三条也就是为什么编译器会以警告的方式强制必须同时重写Equals()和GetHashCode()的原因。

对于System.Object来说,Equals()仅仅比较引用,而GetHashCode()会根据对象的地址返回一个散列。因此如果你的类这两个方法都不重载,将其运用到HashTable是可以正常工作的,但这样的类会受到“同一与相等”这个典型问题的限制。因此,最后自己为要用做键的类重写这两个方法。

此外,MS已经为String提供了一种虽然复杂、但很有效的散列算法。我们可以在自己的实现中利用这个算法。

最后,一般简单高效率的散列算法的设计是:获取字段,把它们与较大的素数相乘,再把结果加起来。

第二,既然我们选择了HashTable,那么用什么做主键,什么做值呢?

还记得,我们提过想把生命周期管理拿进来,而且希望对象的创建和销毁更自动化。鉴于此二者。我们可以把对象本身用做主键,而值用创建该对象时的时间来填充,每当被使用后,该值即立刻被更新。通过一个定时触发器根据对象的最近使用时间。来管理object pool中的对象。

第三,也就是关于对象的使用问题。其实,本质上讲,是一个有状态和无状态的问题。如果对象是有状态的,当我们从object pool取出一个对象后,该对象的状态不一定符合我们使用的要求。比如一个数据库连接,当我们从object pool取出时,它很有可能是关闭的。因此,这就很有必要在我们的取出操作中进行对象状态的验证。

第四,同步。除了谈到的使用线程安全的HashTable外,我们还有一些操作是需要原子特性的。我们可以把lock或者monitor施加在critical section上来得到保证。

说了这么多,我们来看看《C# Threading Handbook》中的这个更具使用价值的object pool的实现:

作为该书中的一个完整的例子。书中提供了一个数据库连接object pool的实现。代码如下:

using System;

using System.Data.SqlClient;

namespace WroxCS

{

     
public sealed class DBConnectionSingleton : ObjectPool

     
{

       
private DBConnectionSingleton() {}

 

       
public static readonly DBConnectionSingleton Instance =

           
new DBConnectionSingleton();

 

       
private static string _connectionString =

           @"server=(local); Integrated Security=SSPI;database=northwind";

 

       
public static string ConnectionString

       
{

         
set

         
{

           _connectionString = value;

         }


         
get

         
{

           
return _connectionString;

         }


       }


 

       
protected override object Create()

       
{

         SqlConnection temp = 
new SqlConnection( ConnectionString);

         temp.Open();

         
return(temp);

       }


 

       
protected override bool Validate(object o)

       
{

         
try

         
{

           SqlConnection temp = (SqlConnection)o;

          
return(

            ! ((temp.State.Equals(System.Data.ConnectionState.Closed))));

         }


         
catch (SqlException)

         
{

           
return false;

         }


       }


 

       
protected override void Expire(object o)

       
{

         
try

         
{

           ((SqlConnection) o ).Close();

         }


         
catch (SqlException)

         
{

         }


       }


 

       
public SqlConnection BorrowDBConnection()

       
{

         
try

         
{

           
return((SqlConnection)base.GetObjectFromPool());

         }


         
catch (Exception e)

         
{

           
throw e;

         }


       }


       

       
public void ReturnDBConnection(SqlConnection c)

       
{

         
base.ReturnObjectToPool(c);

       }


     }


}

使用这个类的方法大致如下:

// Initialize the Pool

       DBConnectionSingleton pool;

       pool = DBConnectionSingleton.Instance;

       
// Set the ConnectionString of the DatabaseConnectionPool

       DBConnectionSingleton.ConnectionString =

       "server=(local);User ID=sa;Password=;database=northwind";

       
// Borrow the SqlConnection object from the pool

       SqlConnection myConnection = pool.BorrowDBConnection();

       
// Return the Connection to the pool after using it

pool.ReturnDBConnection(myConnection);

由于下载不到源代码,我自己写了个简单的测试程序。创建一个简单窗体项目。拖了一个TextBox。想的是利用object pool中的连接来访问Nothwind并把一些数据显示到TextBox中。

我添加的代码如下:

private DBConnectionSingleton pool = DBConnectionSingleton.Instance;

  
private void DBOperation()

              {
                     
// Set the ConnectionString of the DatabaseConnectionPool
                     //DBConnectionSingleton.ConnectionString =
                     //   "server=(local);User ID=sa;Password=;database=northwind";

                     // Borrow the SqlConnection object from the pool

                     SqlConnection myConnection = pool.BorrowDBConnection();

                     SqlCommand catCMD = myConnection.CreateCommand();

                     catCMD.CommandText = "SELECT CategoryID,CategoryName FROM Categories";

                     SqlDataReader myReader = catCMD.ExecuteReader();

                     
string[] args = new string[2];

                     UpdateData UIDel = 
new UpdateData(UpdateUI);

                     
while (myReader.Read())

                     {
                            
//textBox1.Text += myReader.GetInt32(0).ToString() + myReader.GetString(1);

                            args[0] = myReader.GetInt32(0).ToString();

                            args[1]=  myReader.GetString(1);

                            
//this.BeginInvoke(UIDel, args);

                UpdateUI(args[0],args[1]);
                     }
                     myReader.Close();

                     
// Return the Connection to the pool after using it

                     pool.ReturnDBConnection(myConnection);

              }

       
private void Connect_Click(object sender, System.EventArgs e)

              {

                     
//DBOperationThread = new Thread(new ThreadStart(this.DBOperation));

                     //DBOperationThread.Start();

                     
this.DBOperation();

              }

然后,我试着单步调试。并且注意观察内存的使用情况和代码的覆盖。我用的是PureCoverage和Purify。而且监视lock和unlock的变化。

结果出现了令人费解的情况。每当pool.ReturnDBConnection(myConnection) 返回时,unlock便从1变成了0——一个完全毫无道理的事情。

而且当我检查代码覆盖情况时,发现Validate()方法从未执行过,准确的讲是每次从object pool中获取连接时,它总是创建新的对象——因为unlock为空。

而且,在这种情况下内存的使用随着每次创建新的连接而激增!

但如果我不单步执行,那么就不会存在上面的问题。从代码覆盖上就可以看到unlock曾经不是空的。

Hit Line Source Code

       1     using System;

       2     using System.Collections;

       3     using System.Timers;

       4    

       5     namespace WroxCS

       6     {

       7    

       8          public abstract class ObjectPool

       9          {

       10           //Last Checkout time of any object from the pool.

       11           private long lastCheckOut;

       12   

       13           //Hashtable of the checked-out objects

       14           private static Hashtable locked;

       15   

       16           //Hashtable of available objects

       17           private static Hashtable unlocked;

       18   

       19           //Clean-Up interval

       20           internal static long GARBAGE_INTERVAL = 5 * 1000; // 90 seconds

       21           static ObjectPool()

       22           {

       23             locked = Hashtable.Synchronized(new Hashtable());

       24             unlocked = Hashtable.Synchronized(new Hashtable());

       25           }

       26   

       27           internal ObjectPool()

       28           {

       29              lastCheckOut = DateTime.Now.Ticks;

       30   

       31             //Create a Time to track the expired objects for cleanup.

       32             System.Timers.Timer aTimer = new System.Timers.Timer();

       33             aTimer.Enabled = true;

       34             aTimer.Interval = GARBAGE_INTERVAL;

       35             aTimer.Elapsed += new

       36                 System.Timers.ElapsedEventHandler(CollectGarbage);

       37           }

       38   

       39           protected abstract object Create();

       40   

       41           protected abstract bool Validate(object o);

       42   

       43           protected abstract void Expire(object o);

       44   

       45           internal object GetObjectFromPool()

       46           {

       47             long now = DateTime.Now.Ticks;

       48             lastCheckOut = now;

       49             object o = null;

       50   

       51             lock(this)

       52             {

       53               try

       54               {

       55                 foreach (DictionaryEntry myEntry in unlocked)

       56                 {

       57                   o = myEntry.Key;

       58                   if (Validate(o))

       59                   {

       60                     unlocked.Remove(o);

       61                     locked.Add(o, now);

       62                     return(o);

       63                   }

       64                   else

       65                   {

       66                     unlocked.Remove(o);

       67                     Expire(o);

       68                     o = null;

       69                 }

       70               }

       71             } catch (Exception){}

       72               o = Create();

       73               locked.Add(o, now);

       74             }

       75             return(o);

       76           }

       77   

       78           internal void ReturnObjectToPool(object o)

       79           {

       80            if (o != null)

       81             {

       82               lock(this)

       83               {

       84                 locked.Remove(o);

       85                 unlocked.Add(o, DateTime.Now.Ticks);

       86               }

       87             }

       88           }

       89   

       90           private void CollectGarbage(object sender,

       91               System.Timers.ElapsedEventArgs ea)

       92           {

       93             lock(this)

       94             {

       95               object o;

       96               long now = DateTime.Now.Ticks;

       97               IDictionaryEnumerator e = unlocked.GetEnumerator();

       98   

       99               try

       100             {

       101               while(e.MoveNext())

       102               {

       103                 o = e.Key;

       104 

       105                 if ((now - ((long) unlocked[ o ])) > GARBAGE_INTERVAL )

       106                 {

       107                   unlocked.Remove(o);

       108                   Expire(o);

       109                   o = null;

       110                 }

       111               }

       112             }

       113             catch (Exception){}

       114           }

       115         }

       116       }

       117     }

而且从内存的使用情况,我还发现:

当应用程序运行一段时间后,每当我再次点击“连接”时,系统都会自动执行一次垃圾收集。设置断点后,再执行,则又会回到上面的情况。

问题出在哪里?还在思考中……


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织