《软件开发性能优化系列》摘抄

树表设计:

      树状表都是使用ID和IDParent两个字段来表示树关系。对树进行查找只能使用自关联方式,不光写法麻烦而且记录多的时候查询性能会非常差。建议在设计树表的时候可以考虑加入treePath字段,记载到该节点记录需要经历的树路径。虽然会增加Insert和Update的成本。但是对查询树关系非常有帮助。可以避免大部分的自关联查询。

 

分区表:

      记录超过一百万的表要考虑是否需要使用分区表。对于能够明确确定分区字段,并且经常通过分区访问记录的。分区表会提高查询性能。

 

冗余字段:

一些关系查询只查询类似Code、Name等很少的字段。可以考虑将频繁需要关联查询的这种字段冗余到主表中。这种表设计会要求同步更新两个字表,对于大部分查询,会减少表的关联,提高查询性能。对于自定义项引用自由项的需求,使用这种冗余设计能保证报表查询的便利,能避免联查表提高了查询效率。

 

Code、Name字段长度

     如果业务表中的Code、Name需要建立唯一索引,那Code长度小于nvarchar(32),Name长度小于nvarchar(200)比较合适,不要超过255,避免超过索引键长度900bytes的限制

 

字段命名

      字段命名试用版不要使用SQLServer关键字,此问题虽然和性能无关,但却是最容易引发BUG的因素。如果在开发阶段不杜绝这一问题,以后再修改会增加很多代码修改等连带成本。

=======================================================================================

无主键、索引或者没有查询索引无效,是产品查询慢的最常见问题,以下是数据库表主键和索引设计的主要原则

1、主键

主键ID,主键既是约束也是索引,同时也用于对象缓存的键值。

2、索引

       *组合或者引用关系的子表(数据量较大的时候),需要在关联主表的列上建立非聚集索引(如订单明细表中的产品ID字段、订单明细表中关联的订单ID字段)

 

      *索引键的大小不能超过900个字节,当列表的大小超过900个字节或者若干列的和超过900个字节时,数据库将报错。

 

      *表中如果建有大量索引将会影响INSERT、UPDATE和DELETE语句的性能,因为在表中的数据更改时,所有的索引都将必须进行适当的调整。需要避免对经常更新的表进行过多的索引,并且索引应保持较窄,就是说:列要尽可能的少。

 

      *为经常用于查询的谓词创建索引,如用于下拉参照快速查找的code、name等。在平台现有下拉参照的查询sql语句中的like条件语句要改成不带前置通配符。还有需要关注Order By和Group By谓词的索引设计,Order By和Group By的谓词是需要排序的,某些情况下为Order By和Group By的谓词建立索引,会避免查询时的排序动作。

 

      *对于内容基本重复的列,比如只有1和0,禁止建立索引,因为该索引选择性极差,在特定的情况下会误导优化器做出错误的选择,导致查询速度极大下降。

 

      *当一个索引有多个列构成时,应注意将选择性强的列放在前面。仅仅前后次序的不同,性能上就可能出现数量级的差异。

 

      *对小表进行索引可能不能产生优化效果,因为查询优化器在遍历用于搜索数据的索引时,花费的时间可能比执行简单的表扫描还长,设计索引时需要考虑表的大小。记录数不大于100的表不要建立索引。频繁操作的小数量表不建议建立索引(记录数不大于5000条)

==============================================================================================

阻塞原因

      在默认事务隔离情况下,数据库事务越长,一方面独占锁被持有的时间越长,写操作阻塞读操作的机会就越多;另一方面,在默认的读提交隔离模式下,读操作使用共享锁与独占锁不兼容,读操作也会阻塞写操作。

      阻塞也是死锁产生的基本条件,改善了阻塞就能有效减少死锁。

      在软件开发后期,在对大数据量的集成测试工程中,通过活动查看器可以观察到阻塞情况,主要产生阻塞的原因就是读和写相互阻塞在对同一个大表的操作上。因此对于读写阻塞问题需要加以足够考虑。

减少阻塞一些指导原则

整体原则

      *归结起来也依赖于代码、sql的优化,一方面要使逻辑代码优化到最快。另一方面,一个耗时较长的sql语句将会阻塞全部用户等待几十秒甚至几分钟,针对查询sql语句的优化也是最重要的。

      *修改批量操作的需求,批量操作耗时和记录数量是成正比的。为此设计时要避免在同一个自动事务服务方法中做批量的循环操作,可以将循环操作放到UI控制端,这一就使一个长事务变成多个短小的事务,将减少阻塞的机会。

      *减少读写操作使用锁的数量,比如减少批更新操作中修改行的数量,保证行锁定少,同时减少锁升级至表锁的机会。

      *一些耗时大、锁定数据多的操作需要避免和正常业务操作冲突,可以使用调度计划在系统闲置的时候来运行,或者使用互斥机制来保证其它用户暂退出操作独立运行,视业务情况而定。

读写锁阻塞的处理原则

      *减少读操作需要的共享锁

1、将事务隔离级别由默认读提交(ReadCommited)修改成读未提交(ReadUnCommited),将不会有读写阻塞,但是会造成读取其它事务未提交的数据。

     方法一:如果服务方法为自动事务,则在服务方法特性SerivceMethod指定IsolationLevel属性为ReadUnCommited

     方法二:对于使用手工事务的情况,使用DBSession接口带隔离级别参数的方法session.beginTransaction(IsolationLevel), level值为  ReadUnCommited.

2、在select语句中带NoLock提示,事务内无锁提示也不会有读写阻塞,与上面一样也会有脏读。

    Select上加无锁控制,在执行select操作时加 with(nolock)。如:SELECT * FROM person WITH(NoLock)

   

上面两种方式需要平台和业务涉及人员根据情况使用。事务隔离级别控制粒度较粗,使用时需要考虑对多个select语句的影响,NoLock提示控制单个select语句,粒度更细。

     *在sqlserver2005中使用基于行版本的快照隔离模式

          在快照模式下,读取数据不再使用共享锁,阻塞的现象能大大减少。

          方法:在建立数据库后执行下面命令:

          ALTER DATABASE 替换的数据库名 SET READ_COMMITIED_SNAPSHOT ON;

          ALTER DATABASE 替换的数据库名 SET ALLOW_SNAPSHOT_ISOLATION ON;

      注意:sqlserver2005和with(no lock)语句,这两种方案都能够避免阻塞,但是这两种方式是有区别的。

      举个小例子说明一下:比如数据库表T1中有两个字段Col1且其默认值为1.此时恰好有个A事务通过Update语句修改表T1的Col字段值为2,但还未提交,如下:

A事务:

{

          Begion tran

          Update T1 set Col1=2

         //Commit;//注释掉此句,模拟A事务未提交。

}

这时如果另一个事务如果使用Sql server2005的快照模式获取T1表Col1字段的值则取到的值是之前默认的1;而使用with(no lock)的方式获取到Col1字段的值为2.

其实不管使用哪种方式,得到的数据都不确保是准确的,这要取决于A事务是否执行并提交成功。

==============================================================================================

死锁原因:

      死锁是由两个相互阻塞的线程组成,它们互相等待对方完成,一般死锁情况下两个数据库事务之间存在着相反的操作。sqlserver中死锁监视器定时检查死锁,如果发现死锁,将选择其中回滚消耗最小的任务,这时候发生1025数据库错误。可以通过启用sqlserver2005快照模式,避免一些读/写的逆向阻塞造成的死锁.但是对于一些写/写阻塞的死锁可能无法解决,很多时候需要从业务的角度来避免一些写/写的逆向操作阻塞情况。

 

     死锁问题的解决很困难,但是可以通过一些手段来使死锁最小化。

 

死锁最小化方法:

       从理论上讲,数据库死锁无法避免,但是可以遵循一定原则使死锁发生的概率尽量降低。

 

写/写死锁

      *用相同的顺序访问对象,如果涉及到多于一张表的操作,要保证事务中都按照相同的顺序访问这些表。

 

      *减少一个事务中的大批量更新的操作,大批量操作写操作涉及记录独占锁太多而且一直到事务结束才能释放,更容易与其它事务造成死锁。

 

读/写死锁(原则上与前面提到的减少读/写阻塞方式一致

      *去掉读操作的共享锁

 

        最佳方式是使用sql2005的快照模式,其次方式是使用读未提交隔离模式或使用NOLock提示,需要平台和业务设计时依据情况进行sql组织的设计。

 

      按照相同的顺序访问对象可以避免相互持有对方请求资源的情况发生。例如一个操作主从表的处理流程,涉及查询和修改两个步骤。如果查询时是先查主表再查从表,则修改也应先修改主表再修改从表。

 

      另一个降低事务大小的一个主要手段,是将查询操作尽可能地提前(包括使用一些中间变量记录下查询结果提供后续使用),而把插入、修改等操作集中在方法靠后的部分。这样,可以让一个事务需要持有独占锁的时间尽可能缩短,减少死锁的发生概率。

==============================================================================================

1、参数化sql

        对于一般简单查询,数据库能自动参数啊以重用计划缓存,如:

SELECT * FROM table WHERE id=1;

SELECT * FROM table WHERE id=4

     在sqlserver内部能自动参数化这个查询,SELECT * FROM table WHERE id=@1

     但是一旦sql语句中带有join、union、top……等关键字,sqlserver内部将不会自动参数化。

     在sql2005中,通过alter database XXX set paramenterization forced的强制参数化命令能够将所有sql中的常量参数化,但是强制参数化会因为常量类型不一致造成查询结果误差。

 

2、使用查询中的索引有效

   a)、单列索引使用原则

     单列索引能响应大部分的简单比较,包括等价和不等价。对于like操作无前置通配符也是有效的。如:

能有效使用索引的条件语句:
[col1]=1
[col1]>100
[col1] between 0 and 100
[col1] like 'abc%'
下列条件语句不会有效利用索引:
ABS([col1])=1
[col1]+1>100 
[col1]+10 between 0 and 100 
[col1] like '%abc%' 
 

   b)、避免在WHERE字句中对字段进行函数或表达式操作

     看一下下面效率低下的例子和其解决方法

效率低下的写法:

SELECT     *
FROM  Production.Product
WHERE SafetystockLevel/2=100

高效写法:

SELECT     *
FROM  Production.Product
WHERE SafetystockLevel=2*100

     下面是SQLServer2005的优化报告

 image
      类似的例子:

效率低下的写法 高效的写法

SELECT     *
FROM  Production.Product
WHERE SubString(name,1,3)=’abc’

SELECT     *
FROM  Production.Product
WHERE name like ’abc%’

SELECT     *
FROM  Production.Product
WHERE datediff(day ,modifiedDate,’2005-11-30’)=0

SELECT     *
FROM  Production.Product
WHERE modifiedDate>=’2005-11-30’ andmodifiedDate<’2005-12-1’

      任何对列的操作都将导致表扫描,它包括数据库函数、计算表达式等等,查询时要尽可能将操作移至等号右边。

      避免使用!=或<>、is null 或is not null、 in、not in等这样的操作符,因为这会是系统无法使用索引,而只能直接搜索表中数据。

      例如:

      select id from employee where id!=’B%’

      优化器将无法通过索引来确定将要命中的行数,因此需要搜索该表的所有行。

   c)、多列索引使用原则

     则应考虑列的顺序。用于等于(=)、大于(>)、小于(<)或between搜索条件的where 字句或者参与联接的列应该放在最前面。其它列应该基于其非重要级别进行排序,就是说,从最不重复的列到最重复的列。

     例如:

     如果表中存在索引定义为LastName、FirstName,则该索引在搜索条件为where LastName=’Smith’或where LastName=Smith and FirstName like ’j%’时将很有用。不过,查询优化器不会将此索引用于基于FirstName(where FirstName=’Jane’)而搜索的查询。

==============================================================================================

一、SQL拼写建议

1、查询时不返回不需要的行、列

      业务代码要根据实际情况尽量减少对表的访问行数,最小化结果集,在查询时,不要过多地使用通配符如:select * from table1语句,要用到几列就选择几列,如:select col1,col2 from table1;在可能的情况下尽量限制结果集行数如:select top 100 col1,col2,col3 from talbe2,因为某些情况下用户是不需要那么多的数据的。

2、合理使用EXISTS, NOT EXISTS字句

     如下所示:

     SELECT SUM(T1.C1) FROM T1 WHERE ((SELECT COUNT(*) FROM T2 WHERE T2.C2=T1.C2)>0)

     SELECT SUM(T1.C1) FROM T1 WHERE EXISTS(SELECT * FROM T2 WHERE T2.C2=T1.C2)

     两种产生相同的结果,但是后者的效率显然要高过于前者。银行后者不会产生大量锁定的表扫描或是索引扫描。

     经常需要些一个T_SQLL语句比较一个父结果集和子结果集,从而找到是否存在在父结果集中有而在子结果集中乜嘢的记录,如:

     SELECT _a.hdr_key FROM hdr_tb1 a               -----------tb1 a 表示tb1用别名a代替

     WHERE NOT EXISTS (SELECT * FROM dt1_tb1 b WHERE a.hdr_key = b.hdr_key)

     SELECT _a.hdr_key FROM hdr_tb1 a               -----------tb1 a 表示tb1用别名a代替

     LEFT JION dt1_tb1 b ON  a.hdr_key = b.hdr_key WHERE b.hdr_key IS NULL

     SELECT hdr_key  FROM hdr_tb1

     WHERE hdr_key NOT IN (SELECT hdr_key FROM dt1_tb1)

     三种写法都可以得到同样的结果集,但是效率是依次降低

3、充分利用连接条件

     在某种情况下,两个表之间可能不止一个的连接条件,这时在where 字句中将谅解条件完整的写上,有可能大大提高查询速度。

     例:

    a)、SELECT SUM(A.AMOUNT) FROM ACCOUNT A left jion CARD B on A.CARD_NO = B.CARD_NO

    b)、SELECT SUM(A.AMOUNT) FROM ACCOUNT A left jion CARD B on A.CARD_NO = B.CARD_NO AND A.ACCOUNT_NO = B.ACCOUNT_NO

     第二句将比第一句执行快得多

4、WHERE 字句中关系运算符的选择

      a)、在关系运算中,尽量使用=,尽量不要使用<>。

      b)、WHERE字句中尽量不要使用NOT运算符,如:NOT IN ,NOT EXISTS, NOT>、NOT<等等NOT运算符一般可以去除。如NOT SALARY >10000K可以改为:salary<=100,如避免使用NOT IN,可以使用 left outer jion代替它。

      c)、where 字句中条件表达式间逻辑关系为AND时,将条件为假的概率高的放在前面,概率相同、条件计算简单的放在前面。

      d)、尽可能不要用Order by字句。使用Order by时,尽量减少列数、尽量减少排序数据行数、排序字段尽量是数字型(尽量不要是字符型)。GROUP BY、 SELECT DITINCT、UNION等字句,也经常导致Order运算。

      e)、不要使用Select count(*)方式来判断记录是否存在,建议使用Select top 1 from table1 where ……。

      f)、不要使用Group by而没有聚合列。

     g)、避免Select 语句的Where 字句条件用于假。如:where 1=0;

    h)、如果有多表连接时,应该有主从表之分,并尽量从一个表读取数,如select a.col1,a.col2 from a jion b on a.col3=b.col4 where b.col5=’a’.

     i)、在where 字句中,如果有多个过滤条件,应将所有列或过滤记录数量最多的条件应该放在前面。

二、使用Truncate清空表

      Truncate会将表中记录全部清空,而不能有选择性的删除指定记录。而DELETE可以指定删除的记录。由于Truncate操作在TransactionLog中只记录被Truncate的页号,而DELETE需要记载被删除记录的详细内容,因此Truncate会比DELETE更迅速。对大数据表使用Truncate,效果更加明显。Truncate Table只会删除表中记录。而不会对表的索引和结构造成影响。

三、Union和Union all

      Union将两个结果集合并后,会消除重复记录,而Union all不会消除重复记录,而是直接将两个结果集直接合并。明确得知两个结果集中没有重复记录或者重复记录不影响使用,建议使用Union all 代替Union。因为Union在消除重复记录的过程中需要进行排序过滤操作,对大结果集这种排序操作会非常影响性能。下面是Union 和Union all的简单性能比较:

---------------Union

select * from table1 where code=’01’

Union

select * from table1 where code=’02’

---------------Union all

select * from table1 where code=’01’

union all

select * from talbe1 where code=’02’

image

==============================================================================================

   垃圾回收时现代语言的标志之一。垃圾回收解放了手工管理对象释放的工作,提高了程序的健壮性,但是副作用就是程序代码可以对于创建对象变得随意。

1、避免不必要的对象创建

     由于垃圾回收的代价较高,所以C#程序开发要遵循的一个基本原则就是避免不必要的对象创建。以下列举一些常见的情型。

a)、避免循环创建对象

      如果对象并不会随每次循环改变而改变状态,那么在循环中反复创建对象将带来性能损耗。例如下面的例子:

            SqlBuildResults BuildUpdate(IEntityMap Map,IObjectValue date)
            {
                SqlBuildResults results = new SqlBuildResults();
                foreach(IORMap ormap in map.Maps)
                {
                    UpdateBuilder builder = new UpdateBuilder();
                    SqlBuildResults result = builder.BuildUpdate(ormap,date);
                    if(result != null)
                        results.AddRange(result);
                }
                return results;
            }

       高效的做法是将builder对象提到循环外面创建。

b)、在需要的逻辑分支中创建对象

      如果对象只在默写逻辑分支中才被用到,那么应该只在该逻辑分支中创建对象。例如:

        protected virtual object OnGetRelation(string childAttrName, IAssociaton association, object relation)
        {
            ObjectRelationEventArgs args1 = new ObjectRelationEventArgs(association, relation, relation);
            if (this.GetRelation != null)
            {
                this.GetRelation(childAttrName, this.Anchor, args1);
                relation = args1.NewRelation;
            }
            return relation;
        }

     正确的做法是:

        protected virtual object OnGetRelation(string childAttrName, IAssociaton association, object relation)
        {
            if (this.GetRelation != null)
            {
                ObjectRelationEventArgs args1 = new ObjectRelationEventArgs(association, relation, relation);
                this.GetRelation(childAttrName, this.Anchor, args1);
                relation = args1.NewRelation;
            }
            return relation;
        }

c)、使用常量避免创建对象

      如下例,程序中存在大量new decimal(0)的代码,这会导致小对象频繁创建及回收;

            if (convert1.FromDualQty.RateToBase == new decimal(0))
            {
                comvert1.FromDualQty.RateToBase == UOMConvertRatio.GetRationBy(……);
            }
            if (convert1.ToQty.RateToBase == new decimal(0))
            {
                comvert1.FromDualQty.RateToBase == UOMConvertRatio.GetRationBy(……);
            }

      正确的做法是使用Decimal.Zero常量。另外,我们也可以学习这个设计手法,应用到类似场景中。

d)、使用StringBuilder做字符串连接。

2、不要使用空析构函数

      如果类中包含析构函数,则创建对象时会在Finalize队列中添加对象的引用,以保证当对象无法到达时,人人可以调用到Finalize方法。垃圾回收器在运行期间,会启动一个低优先级的线程处理该队列。相比之下,没有析构函数的对象就没有没有这些小号。如果析构函数为空,这个消耗就毫无意义,只会导致性能降低!因此,我们尽量不要使用空的析构函数。

      从实际情况来看,许多是曾经在析构函数中包含有处理代码,但后来因为种种原因被注释掉或者删除掉了,只留下一个空的析构函数。此时应该注意把析构函数本身注释掉或者删除掉。

3、实现IDisposable接口

      垃圾回收事实上只支持托管内存的回收,对于其它的非托管的资源,例如:WindowsGDI句柄或数据库连接,在析构函数中是否资源有很大问题,原因是垃圾回收依赖于内存紧张情况,虽然数据库连接可能已濒临耗尽,但如果内存还很充足的话,垃圾回收是不会运行的。

      C#的IDisposable接口是一种显式释放资源的机制。通过提供using语句,还简化了使用方式(编译器自动生成try…finally块,并在finally块中调用Dispose方法)。对于申请了非托管资源的对象,应为其实现IDisposable接口,并保证资源一旦超出using语句范围,即得到及时的释放。这对于构造函数健壮且性能优良的程序非常有意义!

      为防止对象的Dispose方法不被调用的情况发生,一般还要提供析构函数,两者调用一个出来资源释放的公共方法。同时,Dispose方法应调用System.GC.SuppressFinalize(this),告诉垃圾回收器无需在处理Finalize方法了。

==============================================================================================

1、使用StringBuilder做字符串连接

      string是不变类,使用+操作连接字符串会导致创建一个新的字符串。如果字符串连接次数不是固定的,例如在一个循环操作中,则应该使用StringBuilder类来做字符串连接工作。因为StringBuilder内部有一个StringBuffer,连接字符操作不会每次分配新的字符串空间。只有当连接后的字符串超出Buffer大小是,才会申请信的Buffer空间。典型代码如下:

StringBuiler sb = new StringBuilder(256);

for(int i = 0; i < str.Count; i++)

{

      sb.Append(str[i]);

}

      而如果连接字符数十固定的并且只有几次,此时应该直接用+号连接,保持程序简洁易读。实际上,编译器已经做了优化,会依据加号次数调用不同参数个数的String.Concat方法。例如:

      String str =  str1 + str2 + str3 + str4;

      会被编译成:Sting.Concat(str1,str2,str3,str4).该方法内部会计算总的String长度,仅分配一次,并不会如通常想象的那样分配三次。作为一个值,当字符串连接操作达到10此以上时,则应该使用StringBuilder.

这里有个细节要注意:StringBuilder内部Buffer的缺省值为16,这个实在太小。按照StingBuilder的使用场景,Buffer肯定得重新分配。我建议使用256作为Buffer的初值。当然,如果能计算出最终生成字符串长度的话,则应该按这个值来设定Buffer的初值。我曾经开发过一个44位的UUID生成方法,仅仅把new StringBuilder()改为StringBuilder(44)前后就有3倍的效率差异。

2、避免不必要的调用ToUpper或ToLower方法

     String是不变类,调用ToUpper或ToLower方法都会导致创建一个新字符串。如果被频繁调用,将导致频繁创建字符串对象。这违背了前面讲到的“避免频繁创建对象”这一基本原则。

     例如,bool.Parse方法本身已经是忽略大小写的,但下面的代码每次访问IsNullable属性时,都要不必要的调用ToLower方法:

public virtual bool IsNullable
{
    get
        { 
            if(isSyncDictionary && this.DictionaryItem ! = null)
            {
                if(this.dictionaryItem.IsNullableValid)
                {
                    return bool.Parse(dictionaryItem.IsNullable.ToString().ToLower());
                }
            }
        }
}

      另外一个非常铺平的场景是字符串比较,例如:

        foreach (XmlNode node in DocumentElement.ChildNodes)
        {
            if (node is XmlElement)
            {
                if (node.Name.ToLower() == "appender")
                {
                    Respoitory.AppenderLoader.Load(node);
                }
                else if (node.Name.ToLower() == "Render")
                {
                    Respoitory.AppenderLoader.Load(node);
                }
                else if (node.Name.ToLower() == "Render")
                {
                    Respoitory.RegisterUtilRunner.RunningRegisterUtil(node);
                }
            }
        }

      高效的做法是使用Compare方法,这个方法可以做大小写忽略的比较,并且不会创建新字符串:

      if(String.Compare(node.Name,"appender",StringComparison.OrdinalIgnoreCase)==0)

      最后列举的一个示例是使用HashTable的时候,有时候无法保证传递key的大小写是否符合预期,往往会把key强制转换到大小写方式,例如:myTalbe.Add(myKey.ToLower(),myObject).实际上HashTable有不同的构造方式,完全支持采用忽略大小写的Key:new HashTable(StringComparer.OrdinalIgnoreCase).

3、最快的空串比较方法

     将String对象的Length属性与0比较式最快的方法:if(str.Length == 0)

      其次是与sting.Empty常量或空串比较;if(str == String.Empty)或if(str == "")

      注:C#在编译时会将程序集中声明的所有字符串常量放到保留池中(intern pool),相同常量不会重复分配。

==============================================================================================

1、 线程同步


      线程同步是编写多线程程序需要首先考虑问题。C#为同步提供了 Monitor、Mutex、AutoResetEvent 和 ManualResetEvent 对象来分别包装 Win32 的临界区、互斥对象和事件对象这几种基础的同步机制。C#还提供了一个lock语句,方便使用,编译器会自动生成适当的 Monitor.Enter 和 Monitor.Exit 调用。

a)、同步粒度

      同步粒度可以是整个方法,也可以是方法中某一段代码。为方法指定 MethodImplOptions.Synchronized 属性将标记对整个方法同步。例如:

[MethodImpl(MethodImplOptions.Synchronized)] 
public static SerialManager GetInstance() 
{ 
   if (instance == null ) 
   { 
       instance = new SerialManager(); 
   } 
   return instance; 
}


      通常情况下,应减小同步的范围,使系统获得更好的性能。简单将整个方法标记为同步不是一个好主意,除非能确定方法中的每个代码都需要受同步保护。

b)、同步策略


       使用 lock 进行同步,同步对象可以选择 Type、this 或为同步目的专门构造的成员变量。
      避免锁定Type
      锁定Type对象会影响同一进程中所有AppDomain该类型的所有实例,这不仅可能导致严重的性能问题,还可能导致一些无法预期的行为。这是一个很不好的习惯。即便对于一个只包含static方法的类型,也应额外构造一个static的成员变量,让此成员变量作为锁定对象。
避免锁定 this
      锁定 this 会影响该实例的所有方法。假设对象 obj 有 A 和 B 两个方法,其中 A 方法使用 lock(this) 对方法中的某段代码设置同步保护。现在,因为某种原因,B 方法也开始使用 lock(this) 来设置同步保护了,并且可能为了完全不同的目的。这样,A 方法就被干扰了,其行为可能无法预知。所以,作为一种良好的习惯,建议避免使用 lock(this) 这种方式。
      使用为同步目的专门构造的成员变量
      这是推荐的做法。方式就是 new 一个 object 对象, 该对象仅仅用于同步目的。
      如果有多个方法都需要同步,并且有不同的目的,那么就可以为些分别建立几个同步成员变量。

c)、 集合同步


      C#为各种集合类型提供了两种方便的同步机制:Synchronized 包装器和 SyncRoot 属性。

    // Creates and initializes a new ArrayList 
    ArrayList myAL = new ArrayList(); 
    myAL.Add( " The " ); 
    myAL.Add( " quick " ); 
    myAL.Add( " brown " ); 
    myAL.Add( " fox " );

    // Creates a synchronized wrapper around the ArrayList 
    ArrayList mySyncdAL = ArrayList.Synchronized(myAL); 


      调用 Synchronized 方法会返回一个可保证所有操作都是线程安全的相同集合对象。考虑 mySyncdAL[0] = mySyncdAL[0] + "test" 这一语句,读和写一共要用到两个锁。一般讲,效率不高。推荐使用 SyncRoot 属性,可以做比较精细的控制。

2、 使用 ThreadStatic 替代 NameDataSlot

     存取 NameDataSlot 的 Thread.GetData 和 Thread.SetData 方法需要线程同步,涉及两个锁:一个是 LocalDataStore.SetData 方法需要在 AppDomain 一级加锁,另一个是 ThreadNative.GetDomainLocalStore 方法需要在 Process 一级加锁。如果一些底层的基础服务使用了 NameDataSlot,将导致系统出现严重的伸缩性问题。
     规避这个问题的方法是使用 ThreadStatic 变量。示例如下:

public sealed class InvokeContext 
{ 
    [ThreadStatic] 
    private static InvokeContext current; 
    private Hashtable maps = new Hashtable(); 
} 

3、多线程编程技巧


     使用 Double Check 技术创建对象

internal IDictionary KeyTable 
{ 
   get 
   { 
      if ( this ._keyTable == null ) 
      { 
         lock ( base ._lock) 
         { 
             if ( this ._keyTable == null ) 
             { 
                 this ._keyTable = new Hashtable(); 
             } 
         } 
      } 
      return this ._keyTable; 
    } 
} 


      创建单例对象是很常见的一种编程情况。一般在 lock 语句后就会直接创建对象了,但这不够安全。因为在 lock 锁定对象之前,可能已经有多个线程进入到了第一个 if 语句中。如果不加第二个 if 语句,则单例对象会被重复创建,新的实例替代掉旧的实例。如果单例对象中已有数据不允许被破坏或者别的什么原因,则应考虑使用 Double Check 技术。

==============================================================================================

==============================================================================================

原文地址:https://www.cnblogs.com/linghe/p/1656734.html