深入分析C# 线程同步

上一篇介绍了如何开启线程,线程间相互传递参数,及线程中本地变量和全局共享变量区别。

本篇主要说明线程同步。

如果有多个线程同时访问共享数据的时候,就必须要用线程同步,防止共享数据被破坏。如果多个线程不会同时访问共享数据,可以不用线程同步。

线程同步也会有一些问题存在:

  1. 性能损耗。获取,释放锁,线程上下文建切换都是耗性能的。
  2. 同步会使线程排队等待执行。

线程同步的几种方法:

阻塞

当线程调用Sleep,Join,EndInvoke,线程就处于阻塞状态(Sleep使调用线程阻塞,Join、EndInvoke使另外一个线程阻塞),会立即从cpu退出。(阻塞状态的线程不消耗cpu)

当线程在阻塞和非阻塞状态间切换时会消耗几毫秒时间。

//Join
static void Main()
{
 Thread t = new Thread (Go);
 Console.WriteLine ("Main方法已经运行....");
 t.Start();
 t.Join();//阻塞Main方法
 Console.WriteLine ("Main方法解除阻塞,继续运行...");
}

static void Go()
{
 Console.WriteLine ("在t线程上运行Go方法...");
}

//Sleep
static void Main()
{
 Console.WriteLine ("Main方法已经运行....");
 Thread.CurrentThread.Sleep(3000);//阻塞当前线程
 Console.WriteLine ("Main方法解除阻塞,继续运行...");
}

 //Task
 static void Main()
{
 Task Task1=Task.Run(() => {
  Console.WriteLine("task方法执行...");
  Thread.Sleep(1000);
  });
 Console.WriteLine(Task1.IsCompleted);
 Task1.Wait();//阻塞主线程 ,等该Task1完成
 Console.WriteLine(Task1.IsCompleted);
}

加锁(lock)

加锁使多个线程同一时间只有一个线程可以调用该方法,其他线程被阻塞。

同步对象的选择:

  • 使用引用类型,值类型加锁时会装箱,产生一个新的对象。
  • 使用private修饰,使用public时易产生死锁。(使用lock(this),lock(typeof(实例))时,该类也应该是private)。
  • string不能作为锁对象。
  • 不能在lock中使用await关键字

锁是否必须是静态类型?

如果被锁定的方法是静态的,那么这个锁必须是静态类型。这样就是在全局锁定了该方法,不管该类有多少个实例,都要排队执行。

如果被锁定的方法不是静态的,那么不能使用静态类型的锁,因为被锁定的方法是属于实例的,只要该实例调用锁定方法不产生损坏就可以,不同实例间是不需要锁的。这个锁只锁该实例的方法,而不是锁所有实例的方法.*

class ThreadSafe
{
 private static object _locker = new object();

 void Go()
 {
 lock (_locker)
 {
 ......//共享数据的操作 (Static Method),使用静态锁确保所有实例排队执行
 }
 }

private object _locker2=new object();
 void GoTo()
 {
 lock(_locker2)
 //共享数据的操作,非静态方法,是用非静态锁,确保同一个实例的方法调用者排队执行
 }
}

同步对象可以兼作它lock的对象

如:

class ThreadSafe
{
 private List <string> _list = new List <string>();
 void Test()
 {
 lock (_list)
 {
 _list.Add ("Item 1");
 }
 }
}

Monitors

lock其实是Monitors的简洁写法。

lock (x)
{
 DoSomething();
} 

两者其实是一样的。

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
 DoSomething();
}
finally
{
 System.Threading.Monitor.Exit(obj);
} 

互斥锁(Mutex)

互斥锁是一个互斥的同步对象,同一时间有且仅有一个线程可以获取它。可以实现进程级别上线程的同步。

class Program
 {
 //实例化一个互斥锁
 public static Mutex mutex = new Mutex();

 static void Main(string[] args)
 {
  for (int i = 0; i < 3; i++)
  {
  //在不同的线程中调用受互斥锁保护的方法
  Thread test = new Thread(MutexMethod);
  test.Start();
  }
  Console.Read();
 }

 public static void MutexMethod()
 {
  Console.WriteLine("{0} 请求获取互斥锁", Thread.CurrentThread.Name);
  mut.WaitOne();
  Console.WriteLine("{0} 已获取到互斥锁", Thread.CurrentThread.Name);
  Thread.Sleep(1000);
  Console.WriteLine("{0} 准备释放互斥锁", Thread.CurrentThread.Name);
  // 释放互斥锁
  mut.ReleaseMutex();
  Console.WriteLine("{0} 已经释放互斥锁", Thread.CurrentThread.Name);
 }
 }

互斥锁可以在不同的进程间实现线程同步

使用互斥锁实现一个一次只能启动一个应用程序的功能。

 public static class SingleInstance
 {
 private static Mutex m;

 public static bool IsSingleInstance()
 {
  //是否需要创建一个应用
  Boolean isCreateNew = false;
  try
  {
  m = new Mutex(initiallyOwned: true, name: "SingleInstanceMutex", createdNew: out isCreateNew);
  }
  catch (Exception ex)
  {

  }
  return isCreateNew;
 }
 }

互斥锁的带有三个参数的构造函数

  1. initiallyOwned: 如果initiallyOwned为true,互斥锁的初始状态就是被所实例化的线程所获取,否则实例化的线程处于未获取状态。
  2. name:该互斥锁的名字,在操作系统中只有一个命名为name的互斥锁mutex,如果一个线程得到这个name的互斥锁,其他线程就无法得到这个互斥锁了,必须等待那个线程对这个线程释放。
  3. createNew:如果指定名称的互斥体已经存在就返回false,否则返回true。

信号和句柄

lockmutex可以实现线程同步,确保一次只有一个线程执行。但是线程间的通信就不能实现。如果线程需要相互通信的话就要使用AutoResetEvent,ManualResetEvent,通过信号来相互通信。它们都有两个状态,终止状态和非终止状态。只有处于非终止状态时,线程才可以阻塞。

AutoResetEvent:

AutoResetEvent 构造函数可以传入一个bool类型的参数,false表示将AutoResetEvent对象的初始状态设置为非终止。如果为true标识终止状态,那么WaitOne方法就不会再阻塞线程了。但是因为该类会自动的将终止状态修改为非终止,所以,之后再调用WaitOne方法就会被阻塞。

WaitOne 方法如果AutoResetEvent对象状态非终止,则阻塞调用该方法的线程。可以指定时间,若没有获取到信号,返回false

set 方法释放被阻塞的线程。但是一次只可以释放一个被阻塞的线程。

class ThreadSafe
{
 static AutoResetEvent autoEvent; 

 static void Main()
 {
 //使AutoResetEvent处于非终止状态
 autoEvent = new AutoResetEvent(false); 

 Console.WriteLine("主线程运行...");
 Thread t = new Thread(DoWork);
 t.Start(); 

 Console.WriteLine("主线程sleep 1秒...");
 Thread.Sleep(1000); 

 Console.WriteLine("主线程释放信号...");
 autoEvent.Set();
 } 

 static void DoWork()
 {
 Console.WriteLine(" t线程运行DoWork方法,阻塞自己等待main线程信号...");
 autoEvent.WaitOne();
 Console.WriteLine(" t线程DoWork方法获取到main线程信号,继续执行...");
 } 

} 

输出

主线程运行...
主线程sleep 1秒...
  t线程运行DoWork方法,阻塞自己等待main线程信号...
主线程释放信号...
  t线程DoWork方法获取到main线程信号,继续执行...

ManualResetEvent

ManualResetEventAutoResetEvent用法类似。

AutoResetEvent在调用了Set方法后,会自动的将信号由释放(终止)改为阻塞(非终止),一次只有一个线程会得到释放信号。而ManualResetEvent在调用Set方法后不会自动的将信号由释放(终止)改为阻塞(非终止),而是一直保持释放信号,使得一次有多个被阻塞线程运行,只能手动的调用Reset方法,将信号由释放(终止)改为阻塞(非终止),之后的再调用Wait.One方法的线程才会被再次阻塞。

public class ThreadSafe
{
 //创建一个处于非终止状态的ManualResetEvent
 private static ManualResetEvent mre = new ManualResetEvent(false);

 static void Main()
 {
 for(int i = 0; i <= 2; i++)
 {
  Thread t = new Thread(ThreadProc);
  t.Name = "Thread_" + i;
  t.Start();
 }

 Thread.Sleep(500);
 Console.WriteLine("\n新线程的方法已经启动,且被阻塞,调用Set释放阻塞线程");

 mre.Set();

 Thread.Sleep(500);
 Console.WriteLine("\n当ManualResetEvent处于终止状态时,调用由Wait.One方法的多线程,不会被阻塞。");

 for(int i = 3; i <= 4; i++)
 {
  Thread t = new Thread(ThreadProc);
  t.Name = "Thread_" + i;
  t.Start();
 }

 Thread.Sleep(500);
 Console.WriteLine("\n调用Reset方法,ManualResetEvent处于非阻塞状态,此时调用Wait.One方法的线程再次被阻塞");

 mre.Reset();

 Thread t5 = new Thread(ThreadProc);
 t5.Name = "Thread_5";
 t5.Start();

 Thread.Sleep(500);
 Console.WriteLine("\n调用Set方法,释放阻塞线程");

 mre.Set();
 }

 private static void ThreadProc()
 {
 string name = Thread.CurrentThread.Name;

 Console.WriteLine(name + " 运行并调用WaitOne()");

 mre.WaitOne();

 Console.WriteLine(name + " 结束");
 }
}

//Thread_2 运行并调用WaitOne()
//Thread_1 运行并调用WaitOne()
//Thread_0 运行并调用WaitOne()

//新线程的方法已经启动,且被阻塞,调用Set释放阻塞线程

//Thread_2 结束
//Thread_1 结束
//Thread_0 结束

//当ManualResetEvent处于终止状态时,调用由Wait.One方法的多线程,不会被阻塞。

//Thread_3 运行并调用WaitOne()
//Thread_4 运行并调用WaitOne()

//Thread_4 结束
//Thread_3 结束

///调用Reset方法,ManualResetEvent处于非阻塞状态,此时调用Wait.One方法的线程再次被阻塞

//Thread_5 运行并调用WaitOne()
//调用Set方法,释放阻塞线程
//Thread_5 结束

Interlocked

如果一个变量被多个线程修改,读取。可以用Interlocked

计算机上不能保证对一个数据的增删是原子性的,因为对数据的操作也是分步骤的:

  1. 将实例变量中的值加载到寄存器中。
  2. 增加或减少该值。
  3. 在实例变量中存储该值。

Interlocked为多线程共享的变量提供原子操作。
Interlocked提供了需要原子操作的方法:

  • public static int Add (ref int location1, int value); 两个参数相加,且把结果和赋值该第一个参数。
  • public static int Increment (ref int location); 自增。
  • public static int CompareExchange (ref int location1, int value, int comparand);

location1 和comparand比较,被value替换.

value 如果第一个参数和第三个参数相等,那么就把value赋值给第一个参数。

comparand 和第一个参数对比。

ReaderWriterLock

如果要确保一个资源或数据在被访问之前是最新的。那么就可以使用ReaderWriterLock.该锁确保在对资源获取赋值或更新时,只有它自己可以访问这些资源,其他线程都不可以访问。即排它锁。但用改锁读取这些数据时,不能实现排它锁。

lock允许同一时间只有一个线程执行。而ReaderWriterLock允许同一时间有多个线程可以执行读操作,或者只有一个有排它锁的线程执行写操作。

 class Program
 {
 // 创建一个对象
 public static ReaderWriterLock readerwritelock = new ReaderWriterLock();
 static void Main(string[] args)
 {
  //创建一个线程读取数据
  Thread t1 = new Thread(Write);
  // t1.Start(1);
  Thread t2 = new Thread(Write);
  //t2.Start(2);
  // 创建10个线程读取数据
  for (int i = 3; i < 6; i++)
  {
  Thread t = new Thread(Read);
  // t.Start(i);
  }

  Console.Read();

 }

 // 写入方法
 public static void Write(object i)
 {
  // 获取写入锁,20毫秒超时。
  Console.WriteLine("线程:" + i + "准备写...");
  readerwritelock.AcquireWriterLock(Timeout.Infinite);
  Console.WriteLine("线程:" + i + " 写操作" + DateTime.Now);
  // 释放写入锁
  Console.WriteLine("线程:" + i + "写结束...");
  Thread.Sleep(1000);
  readerwritelock.ReleaseWriterLock();

 }

 // 读取方法
 public static void Read(object i)
 {
  Console.WriteLine("线程:" + i + "准备读...");

  // 获取读取锁,20毫秒超时
  readerwritelock.AcquireReaderLock(Timeout.Infinite);
  Console.WriteLine("线程:" + i + " 读操作" + DateTime.Now);
  // 释放读取锁
  Console.WriteLine("线程:" + i + "读结束...");
  Thread.Sleep(1000);

  readerwritelock.ReleaseReaderLock();

 }
 }
//分别屏蔽writer和reader方法。可以更清晰的看到 writer被阻塞了。而reader没有被阻塞。

//屏蔽reader方法
//线程:1准备写...
//线程:1 写操作2017/7/5 17:50:01
//线程:1写结束...
//线程:2准备写...
//线程:2 写操作2017/7/5 17:50:02
//线程:2写结束...

//屏蔽writer方法
//线程:3准备读...
//线程:5准备读...
//线程:4准备读...
//线程:5 读操作2017/7/5 17:50:54
//线程:5读结束...
//线程:3 读操作2017/7/5 17:50:54
//线程:3读结束...
//线程:4 读操作2017/7/5 17:50:54
//线程:4读结束...

参考:

  • MSDN
  • 《CLR via C#》

以上就是深入分析C# 线程同步的详细内容,更多关于c# 线程同步的资料请关注我们其它相关文章!

(0)

相关推荐

  • C# 线程同步详解

    前言 当线程池的线程阻塞时,线程池会创建额外的线程,而创建.销毁和调度线程所需要相当昂贵的内存资源,另外,很多的开发人员看见自己程序的线程没有做任何有用的事情时习惯创建更多的线程,为了构建可伸缩.响应灵敏的程序,我们在前面介绍了C#异步编程详解 但是异步编程同样也存在着很严重的问题,如果两个不同的线程访问相同的变量和数据,按照我们异步函数的实现方式,不可能存在两个线程同时访问相同的数据,这个时候我们就需要线程同步.多个线程同时访问共享数据的时,线程同步能防止数据损坏,之所以强调同时这个概念,因为

  • C#使用Interlocked实现线程同步

    通过System.Threading命名空间的Interlocked类控制计数器,从而实现进程 的同步.Iterlocked类的部分方法如下表: 示例,同时开启两个线程,一个写入数据,一个读出数据 代码如下:(但是运行结果却不是我们想象的那样) using System; using System.Threading; namespace 线程同步 { class Program { static void Main(string[] args) { //缓冲区,只能容纳一个字符 char bu

  • C#多线程及同步示例简析

    60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建.撤消与切换存在较大的时空开销,因此需要引入轻型进程:二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大. 因此在80年代,出现了能独立运行的基本单位--线程(Threads).        线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元.一个标准的线程由线程ID,当前指令指针(P

  • c#.net多线程编程教学——线程同步

    随着对多线程学习的深入,你可能觉得需要了解一些有关线程共享资源的问题. .NET framework提供了很多的类和数据类型来控制对共享资源的访问. 考虑一种我们经常遇到的情况:有一些全局变量和共享的类变量,我们需要从不同的线程来更新它们,可以通过使用System.Threading.Interlocked类完成这样的任务,它提供了原子的,非模块化的整数更新操作. 还有你可以使用System.Threading.Monitor类锁定对象的方法的一段代码,使其暂时不能被别的线程访问. System

  • 详解C#多线程之线程同步

    多线程内容大致分两部分,其一是异步操作,可通过专用,线程池,Task,Parallel,PLINQ等,而这里又涉及工作线程与IO线程:其二是线程同步问题,鄙人现在学习与探究的是线程同步问题. 通过学习<CLR via C#>里面的内容,对线程同步形成了脉络较清晰的体系结构,在多线程中实现线程同步的是线程同步构造,这个构造分两大类,一个是基元构造,一个是混合构造.所谓基元则是在代码中使用最简单的构造.基原构造又分成两类,一个是用户模式,另一个是内核模式.而混合构造则是在内部会使用基元构造的用户模

  • c#线程同步使用详解示例

    在应用程序中使用多个线程的一个好处是每个线程都可以异步执行.对于 Windows 应用程序,耗时的任务可以在后台执行,而使应用程序窗口和控件保持响应.对于服务器应用程序,多线程处理提供了用不同线程处理每个传入请求的能力.否则,在完全满足前一个请求之前,将无法处理每个新请求.然而,线程的异步特性意味着必须协调对资源(如文件句柄.网络连接和内存)的访问.否则,两个或更多的线程可能在同一时间访问相同的资源,而每个线程都不知道其他线程的操作. 线程同步的方式 线程同步有:临界区.互斥区.事件.信号量四种

  • C#实现多线程的同步方法实例分析

    本文主要描述在C#中线程同步的方法.线程的基本概念网上资料也很多就不再赘述了.直接接入 主题,在多线程开发的应用中,线程同步是不可避免的.在.Net框架中,实现线程同步主要通过以下的几种方式来实现,在MSDN的线程指南中已经讲了几种,这里结合作者实际中用到的方式一起说明一下. 1. 维护自由锁(InterLocked)实现同步 2. 监视器(Monitor)和互斥锁(lock) 3. 读写锁(ReadWriteLock) 4. 系统内核对象 1) 互斥(Mutex), 信号量(Semaphore

  • C#中实现线程同步lock关键字的用法详解

    1. lock关键字保证一个代码块在执行的过程中不会受到其他线程的干扰,这是通过在该代码块的运行过程中对特定的对象加互斥锁来实现的. 2. lock关键字的参数必须是引用类型的对象.lock对基本数据类型如int,long等无效,因为它所作用的类型必须是对象.如果传入long类型数据,势必被转换为Int64结构类型,则加锁的是全新的对象引用.如果需要对它们进行互斥访问限制,可以使用System.Threading.Interlocked类提供的方法,这个类是提供原子操作的. 3. lock(th

  • 详细解析C#多线程同步事件及等待句柄

    最近捣鼓了一下多线程的同步问题,发现其实C#关于多线程同步事件处理还是很灵活,这里主要写一下,自己测试的一些代码,涉及到了AutoResetEvent 和 ManualResetEvent,当然还有也简要提了一下System.Threading.WaitHandle.WaitOne .System.Threading.WaitHandle.WaitAny和System.Threading.WaitHandle.WaitAll ,下面我们一最初学者的角度来看,多线程之间的同步. 假设有这样的一个场

  • C#线程同步的几种方法总结

    我们在编程的时候,有时会使用多线程来解决问题,比如你的程序需要在后台处理一大堆数据,但还要使用户界面处于可操作状态:或者你的程序需要访问一些外部资源如数据库或网络文件等.这些情况你都可以创建一个子线程去处理,然而,多线程不可避免地会带来一个问题,就是线程同步的问题.如果这个问题处理不好,我们就会得到一些非预期的结果. 在网上也看过一些关于线程同步的文章,其实线程同步有好几种方法,下面我就简单的做一下归纳. 一.volatile关键字 volatile是最简单的一种同步方法,当然简单是要付出代价的

随机推荐