C# 线程资源同步方式总结

转自:http://www.cnblogs.com/VincentWP/archive/2008/06/25/1229104.html

    在现代的程序开发中,资源的同步是一个比较重要的课题,在.Net中,对这部分有很丰富类库供我们使用,现在总结一下在各种情况下对资源同步的
机制。

     1.将字段声明为volatile
       
        当一个字段被声明为volatile时,CLR中一些管理代码和内存的内部机制将负责对字段进行同步,并且总能保证读取到的字段信息都为最新的值,被声明为
volatile的字段必须具备以下特征之一
         
        1.为引用类型
        2.一个指针(在不安全代码中)
        3.sbyte,byte,short,ushort,int,uint,char,float,bool
        4.一个使用底层类型的枚举类型

     2.使用System.Threading.Interlocked 类

         在许多增对整数的操作中,我们都比较容易忽视线程的问题,例如执行下列代码

         i = i + 1;

         实际上,上述代码分为3步骤
              1).  从内存中,读取i的值
              2).  将读取出来的值加1
              3).  将新的值写入内存中

         在单线程上,这个操作不会有任何问题,但是当i被多个线程访问时,问题就出现了,对i进行修改的线程,在上述的任何一部都有可能被其它读取线程打断,想象一下,
当操作线程执行完第二步,准备将新的值写入内存中时,此时其它读取线程获得了执行权,这时读取到的i的值并不是我们想要的,因此,这个操作不具备原子性,在.Net中,使用Interlocked类能确保操作的原子性,Interlocked类有以下的方法

          Increment
          Decrement
          Exchange
         
         上述的方法的参数都为带ref 标识的参数,因此我们说,这些方法保证了数据的原子性,在多线程中,涉及到整数的操作时,数据的原子性值得考虑,Interlocked的操作代码如下

     

             int  i  =   0 ;
            System.Threading.Interlocked.Increment(
ref  i);
            Console.WriteLine(i);
            System.Threading.Interlocked.Decrement(
ref  i);
            Console.WriteLine(i);
            System.Threading.Interlocked.Exchange(
ref  i,  100 );
            Console.WriteLine(i);

          输出信息如下

          

       3.使用lock关键字
            
          地球人都知道,使用lock关键字可以获取一个对象的独占权,任何需要获取这个对象的操作的线程必须等待以获取该对象的线程释放独占权,lock提供了简单的同步资源的方法,与Java中的synchronized关键字类似。

          lock关键字的使用示例代码如下
object  o  =   new   object ();
            
lock  (o)
            
{
                Console.WriteLine(
"O");
            }


       4.使用System.Theading.Monitor类进行同步

          System.Threading.Monitor类提供了与lock类似的功能,不过与lock不同的是,它能更好的控制同步块,当调用了Monitor的Enter(Object o)方法时,会获取o的独占权,直到调用Exit(Object o)方法时,才会释放对o的独占权,可以多次调用Enter(Object o)方法,只需要调用同样次数的Exit(Object o)方法即可,Monitor类同时提供了TryEnter(Object o,[int])的一个重载方法,该方法尝试获取o对象的独占权,当获取独占权失败时,将返回false,查看如下代码

          
using  System;
using  System.Collections.Generic;
using  System.Text;
using  System.Threading;
namespace  MonitorApplication
{
    
class Program
    
{
        
private static object m_monitorObject = new object();
        
static void Main(string[] args)
        
{
            Thread thread 
= new Thread(Do);
            thread.Name 
= "Thread1";
            Thread thread2 
= new Thread(Do);
            thread2.Name 
= "Thread2";
            thread.Start();
            thread2.Start();
            thread.Join();
            thread2.Join();

        }


        
static void Do()
        
{
            
if (!Monitor.TryEnter(m_monitorObject))
            
{
                Console.WriteLine(
"Can't visit Object " + Thread.CurrentThread.Name);
                
return;
            }

            
try
            
{
                Monitor.Enter(m_monitorObject);
                Console.WriteLine(
"Enter Monitor " + Thread.CurrentThread.Name);
                Thread.Sleep(
5000);
            }

            
finally
            
{
                Monitor.Exit(m_monitorObject);
            }

        }

    }

}



 当线程1获取了m_monitorObject对象独占权时,线程2尝试调用TryEnter(m_monitorObject),此时会由于无法获取独占权而返回false,输出信息如下



可以看到线程2无法获取到m_monitorObject的独占权,因此输出了一条错误信息.

Monitor类比lock类提供了一种更优秀的功能,考虑一下如下的场景

      1.当你进入某家餐馆时,发现餐馆里坐满了客人,但是你又不想换地方,因此,你选择等待。
      2.当餐馆内的服务员发现有空位置时,他通知前台的服务生,服务生给你安排了一个座位,于是你开始享受国际化的大餐。

使用Monitor类就可以实现如上的应用需求,在Monitor类中,提供了如下三个静态方法

       Monitor.Pulse(Object o)
       Monitor.PulseAll(Object o)
       Monitor.Wait(Object o ) // 重载函数

当调用Wait方法时,线程释放资源的独占锁,并且阻塞在wait方法直到另外的线程获取资源的独占锁后,更新资源信息并调用Pulse方法后返回。

我们模拟上述代码如下

using  System;
using  System.Collections.Generic;
using  System.Text;
using  System.Threading;
namespace  MonitorApplication
{
    
class Program
    
{
        
private static  object m_isFull = new object();
        
static void Main(string[] args)
        
{
            Thread thread 
= new Thread(HaveLunch);
            thread.Name 
= "HaveLunchThread";
            Thread thread2 
= new Thread(SeatChange);
            thread2.Name 
= "SeatChangeThread";
            thread.Start();
            System.Threading.Thread.Sleep(
2000);
            thread2.Start();
            thread.Join();
            thread2.Join();
        }


        
private static void HaveLunch()
        
{
            
lock (m_isFull)
            
{
                Console.WriteLine(
"Wati for seta");
                Monitor.Wait(m_isFull);
                Console.WriteLine(
"Have a good lunch!");
            }

        }


        
private static void SeatChange()
        
{
            
lock (m_isFull)
            
{
                Console.WriteLine(
"Seat was changed");
                Monitor.Pulse(m_isFull);
            }

        }

    }

}



输出信息如下



可见,使用Monitor,我们能实现一种唤醒式的机制,相信在实际应用中有不少类似的场景。


          5.使用System.Threading.Mutex(互斥体)类实现同步

          在使用上,Mutex与上述的Monitor比较接近,不过Mutex不具备Wait,Pulse,PulseAll的功能,因此,我们不能使用Mutex实现类似的唤醒的功能,不过Mutex有一个比较大的特点,Mutex是跨进程的,因此我们可以在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体。

           考虑如下的代码,代码通过获取一个称为ConfigFileMutex的互斥体,修改配置文件信息,写入一条数据,我们同时开启两个相同的进程进行测试
         
using  System;
using  System.Collections.Generic;
using  System.Text;
using  System.Threading;
using  System.IO;
using  System.Diagnostics;
namespace  MonitorApplication
{
    
class Program
    
{
        
static void Main(string[] args)
        
{
            Mutex configFileMutex 
= new Mutex(false"configFileMutex");
            Console.WriteLine(
"Wait for configFileMutex Process Id : " + Process.GetCurrentProcess().Id);
            configFileMutex.WaitOne();
            Console.WriteLine(
"Enter configFileMutex Process Id : " + Process.GetCurrentProcess().Id);
            System.Threading.Thread.Sleep(
10000);
            
if (!File.Exists(@".\config.txt"))
            
{
                FileStream stream 
= File.Create(@".\config.txt");
                StreamWriter writer 
= new StreamWriter(stream);
                writer.WriteLine(
"This is a Test!");
                writer.Close();
                stream.Close();
            }

            
else
            
{
                String[] lines 
= File.ReadAllLines(@".\config.txt");
                
for (int i = 0; i < lines.Length; i++)
                    Console.WriteLine(lines[i]);
            }

            configFileMutex.ReleaseMutex();
            configFileMutex.Close();
        }

    }

}


           
运行截图如下



此时,PID 为 4628的进程正在等待PID 为 3216的进程释放configFileMutex的互斥体,因此它阻塞在WaitOne函数中,当PID为3216的进程添加并写入了一条信息
至config.txt文件后,释放configFileMutex的独占权,PID为4628的进程获取configFileMutex的独占权,并从config.txt文件中读取输出PID3216进程写入的信息,截图如下


可见使用Mutex进行同步,同步的互斥体是存在于多个进程间的。


            6. 使用System.Threading.ReaderWriterLock类实现多用户读/单用户写的同步访问机制

                在考虑资源访问的时候,惯性上我们会对资源实施lock机制,但是在某些情况下,我们仅仅需要读取资源的数据,而不是修改资源的数据,在这种情况下获取资源的独占权无疑会影响运行效率,因此.Net提供了一种机制,使用ReaderWriterLock进行资源访问时,如果在某一时刻资源并没有获取写的独占权,那么可以获得多个读的访问权,单个写入的独占权,如果某一时刻已经获取了写入的独占权,那么其它读取的访问权必须进行等待,参考以下代码


             
using  System;
using  System.Collections.Generic;
using  System.Text;
using  System.Threading;
using  System.IO;
using  System.Diagnostics;
namespace  MonitorApplication
{
    
class Program
    
{
        
private static ReaderWriterLock m_readerWriterLock = new ReaderWriterLock();
        
private static int m_int = 0;
        
static void Main(string[] args)
        
{
            Thread readThread 
= new Thread(Read);
            readThread.Name 
= "ReadThread1";
            Thread readThread2 
= new Thread(Read);
            readThread2.Name 
= "ReadThread2";
            Thread writeThread 
= new Thread(Writer);
            writeThread.Name 
= "WriterThread";
            readThread.Start();
            readThread2.Start();
            writeThread.Start();
            readThread.Join();
            readThread2.Join();
            writeThread.Join();

        }


        
private static void Read()
        
{
            
while (true)
            
{
                Console.WriteLine(
"ThreadName " + Thread.CurrentThread.Name + " AcquireReaderLock");

                m_readerWriterLock.AcquireReaderLock(
10000);

                Console.WriteLine(String.Format(
"ThreadName : {0}  m_int : {1}", Thread.CurrentThread.Name, m_int));

                m_readerWriterLock.ReleaseReaderLock();
            }

        }


        
private static void Writer()
        
{
            
while (true)
            
{
                Console.WriteLine(
"ThreadName " + Thread.CurrentThread.Name + " AcquireWriterLock");

                m_readerWriterLock.AcquireWriterLock(
1000);

                Interlocked.Increment(
ref m_int);

                Thread.Sleep(
5000);

                m_readerWriterLock.ReleaseWriterLock();

                Console.WriteLine(
"ThreadName " + Thread.CurrentThread.Name + " ReleaseWriterLock");
            }

        }

    }

}


在程序中,我们启动两个线程获取m_int的读取访问权,使用一个线程获取m_int的写入独占权,执行代码后,输出如下



可以看到,当WriterThread获取到写入独占权后,任何其它读取的线程都必须等待,直到WriterThread释放掉写入独占权后,才能获取到数据的访问权,
应该注意的是,上述打印信息很明显显示出,可以多个线程同时获取数据的读取权,这从ReadThread1和ReadThread2的信息交互输出可以看出。


         7.使用System.Runtime.Remoting.Contexts.SynchronizationAttribute对类对象进行同步控制

            当我们确定某个类的实例在同一时刻只能被一个线程访问时,我们可以直接将类标识成Synchronization的,这样,CLR会自动对这个类实施同步机制,
实际上,这里面涉及到同步域的概念,当类按如下设计时,我们可以确保类的实例无法被多个线程同时访问

                      1). 在类的声明中,添加System.Runtime.Remoting.Contexts.SynchronizationAttribute属性。
                      2). 继承至System.ContextBoundObject

            需要注意的是,要实现上述机制,类必须继承至System.ContextBoundObject,换句话说,类必须是上下文绑定的.

            一个示范类代码如下

           
 [System.Runtime.Remoting.Contexts.Synchronization]
 
public   class  SynchronizedClass : System.ContextBoundObject
 
{

 }



            还有AutoResetEvent,ManualReset,EventWaitHandle,Semaphore等可以实现资源的控制,不过它们更多是是基于一种事件唤醒的机制,
如果有兴趣可以查阅MSDN相关的文档。


评论:

#1楼   2008-06-25 15:51  钢钢       
不错,不知道我博客里的东东能不能对你有所帮助 ^_^ 

C#多线程学习(一) 多线程的相关概念 
http://www.cnblogs.com/xugang/archive/2008/04/06/1138856.html 

C#多线程学习(二) 如何操纵一个线程 

C#多线程学习(三) 生产者和消费者 

C#多线程学习(四) 多线程的自动管理(线程池) 

C#多线程学习(五) 多线程的自动管理(定时器) 

C#多线程学习(六) 互斥对象 

(以上都在一个分类中) 


  回复  引用  查看   
#2楼 [ 楼主2008-06-25 16:02  VincentWP       
谢谢钢钢,你的资料对我很有帮助,谢谢
  回复  引用  查看   
#3楼   2008-06-25 16:11  飄lá┽蕩去       
写的不错。
  回复  引用  查看   
#4楼   2008-06-25 16:28  Angel Lucifer       
强烈不推荐使用 ReaderWriterLock 和 SynchronizationAttribute 。

ReaderWriterLock 性能太低,可以考虑使用 ReaderWriterLockSlim(.NET 3.5新增)。

SynchronizationAttribute 的毛病跟 lock(this) 和 lock(typeof(TypeName)) 一样,容易导致死锁。

volatile 要小心使用,按理也在不推荐之列。详情可以参考:

并发数据结构:谈谈volatile变量

  回复  引用  查看   
#5楼 [ 楼主2008-06-25 16:35  VincentWP       
谢谢 Angel Lucifer,我对C#中性能的概念还很模糊,一定拜读一下
  回复  引用  查看   
#6楼   2008-06-25 17:07  HelloCode       
lock关键字锁定的object对象需要声明为private static的才行。 

以前看到的一片微软的文章讲到如果是非静态的,就会产生多个object,锁住的将不是同一个object。 

如果是非private,声明为Public之类的,则可能其他对象锁住这个object造成死锁。

  回复  引用  查看   
#7楼   2008-06-25 17:35  Angel Lucifer       
@HelloCode
private 没错,是否 static 还要看情况。

@VincentWP
互相学习,:-)

  回复  引用  查看   
#8楼   2008-06-25 17:45  鼠·神·泪.NET       
由于字符串的拘留池机制,建议不要lock字符串。
  回复  引用  查看   
#9楼 [ 楼主2008-06-25 17:50  VincentWP       
对,对于lock是否需要static,我认为在单例模式下,lock关键字锁定的Object对象似乎没有必要是static的,声明为private static给人的感觉这个对象锁是全局锁, 

但它又是私有的,意义就混淆了.

  回复  引用  查看   
#10楼   2008-06-25 23:02  Steven Chen       
哦 lz和1F,2F,4F的资料都很棒哦,特别是4F。 


在这里说一声,谢谢了 

一个真正优秀的程序员应该关注于知识的分享 

--------------------------------------

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C# 中,线程并发和线程同步是处理多线程编程中重要的概念。 线程并发指的是多个线程同时执行,这可能导致资源竞争和不确定的结果。为了避免这种情况,我们可以使用线程同步机制来确保线程之间的协调和有序执行。 C# 提供了多种线程同步的机制,下面是一些常用的方法: 1. 互斥锁(Mutex):互斥锁是一种排他锁,只允许一个线程访问被保护的资源。可以使用 `Mutex` 类来创建和管理互斥锁。 2. 信号量(Semaphore):信号量是一种计数器,用于控制同时访问某个资源线程数。可以使用 `Semaphore` 类来创建和管理信号量。 3. 自旋锁(SpinLock):自旋锁是一种忙等待锁,线程会一直尝试获取锁,直到成功为止。可以使用 `SpinLock` 结构来创建和管理自旋锁。 4. 互斥量(Mutex):互斥量是一种特殊的信号量,只能被一个线程持有。可以使用 `Mutex` 类来创建和管理互斥量。 5. 事件(Event):事件是一种同步机制,在多个线程之间发送信号进行通信。可以使用 `ManualResetEvent` 或 `AutoResetEvent` 类来创建和管理事件。 除了上述方法外,还有一些其他的线程同步机制,如读写锁(ReaderWriterLock)、条件变量(Monitor)等。选择适合场景的线程同步机制很重要,以确保线程安全和性能。 需要注意的是,线程并发和线程同步是一个复杂的主题,需要深入学习和实践才能掌握。在编写多线程代码时,建议仔细考虑并发问题,并使用适当的线程同步机制来确保代码的正确性和可靠性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值