您的位置:首页 > 编程语言 > C#

C#多线程同步的几种方法

2017-03-06 14:37 295 查看


1.为什么使用同步

多线程操作的时候我们知道要避免线程之间共享数据,但是很多时候我们要使用多线程并且还要访问同一块内存的数据,这是我们就必须要使用同步技术,确保一次只有一个线程访问和改变共享状态。
下面我就来说一下同步所用的几种方法。


2.Lock语句

lock是一种比较好用的简单的线程同步方式,它是通过为给定对象获取互斥锁来实现同步的。它是通过线程之间的互斥来达到同步效果的。用法如下:
public void DoSomething()
{
lock(this)
{
//你的代码逻辑...
}
}
如果多个线程同时调用DoSomething方法时,我们又不希望线程同时能够执行该方法,在方法体中添加Lock语句后,这样一次只能有一个线程访问该方法。
Lock的参数使用要注意以下几点:
1、不能是primitive type,比如int bool 等 
如果是int,那么传入时势必会发生装箱操作,这会导致每次lock的对象都不一样,就没办法实现多线程同步了。 

2、最好不是public类的对象 
如果是public类的对象,万一不知情的线程new出实例然后又对它加锁,就可能导致死锁。 

3、不能是string的实例 
c#中字符串都是在CLR暂留的,整个程序使用这个字符串的都是一个实例对象,如果有两个互不知情的线程对它加锁就会导致死锁了。 

4、不能是函数的局部变量 
如果函数内部声明一个变量int a,用lock(a)去锁这个函数的一段代码,多线程执行该段代码时都会产生新的a,就没办法告诉彼此是否在占用资源,也就无法实现同步。


3.Interlocked 类

Interlocked 类用于使变量的简单语句原子化。Interlocked 类提供了以线程安全的方式递增(Increment),递减(Decrement),交换(Exchange),读取值(CompareExchange)的方法。
static void Main(string[] args)
{
int i = 10;
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);
System.Threading.Interlocked.CompareExchange(ref i, 99, 100);
Console.WriteLine(i);
Console.ReadLine();
}




不过也有人会说使用Lock也可以做到线程同步,为什么我们还要单独使用Interlocked 类呢?因为与其他同步技术相比,Interlocked 类会快的多。


4.Monitor 类

Lock关键字实际上是一个语法糖,它将Monitor对象进行封装,使用时调用了Enter()方法,该方法会一直等待,直到线程锁定对象为止。
与lock语句相比,Monitor 类的主要优点是:可以添加一个等待被锁定的超时值。这样就不会无限期的等待被锁定。
bool Mlock = false;
Monitor.TryEnter(obj, 500, ref Mlock);
if(Mlock)
{
try
{
//相关操作1...
}finally
{
Monitor.Exit(obj);
}
}else
{
//相关操作2...
}
如果obj被锁定,TryEnter()方法就会把Mlock设为true,并同步地访问由对象obj锁定的状态。如果另一个线程锁定的obj超过500ms,TryEnter()就把遍历Mlock设置为false,线程不再等待,而是执行相关操作2。


5.SpinLock 类

如果Monitor开销由于垃圾回收而过高,则可以使用SpinLock 类(.NET 4.0开始引入)。SpinLock适用在有大量的锁定,且锁定的时间非常短。


6.WaitHandle 基类

在 WaitHandle类中SignalAndWait、WaitAll、WaitAny及WaitOne这几个方法都有重载形式,其中除WaitOne之外都是静态的。WaitHandle方法常用作同步对象的基类。WaitHandle对象通知其他的线程它需要对资源排他性的访问,其他的线程必须等待,直到WaitHandle不再使用资源和等待句柄没有被使用。

  WaitHandle方法有多个Wait的方法,这些方法的区别如下:

  WaitAll:等待指定数组中的所有元素收到信号。

  WaitAny:等待指定数组中的任一元素收到信号。

  WaitOne:当在派生类中重写时,阻塞当前线程,直到当前的 WaitHandle 收到信号。

  这些wait方法阻塞线程直到一个或者更多的同步对象收到信号。

下面的代码示例演示当主线程使用 WaitHandle 类的静态 M:System.Threading.WaitHandle.WaitAny(System.Threading.WaitHandle[]) 和Waitall方法等待任务完成时,两个线程可以如何完成后台任务。

using System;
using System.Threading;

public sealed class App
{
static WaitHandle[] waitHandles = new WaitHandle[]
{
new AutoResetEvent(false),
new AutoResetEvent(false)
};
static Random r = new Random();

static void Main()
{
DateTime dt = DateTime.Now;
Console.WriteLine("Main thread is waiting for BOTH tasks to complete.");
ThreadPool.QueueUserWorkItem(new WaitCallback(DoTask), waitHandles[0]);
ThreadPool.QueueUserWorkItem(new WaitCallback(DoTask), waitHandles[1]);
WaitHandle.WaitAll(waitHandles);
// The time shown below should match the longest task.
Console.WriteLine("Both tasks are completed (time waited={0})",
(DateTime.Now - dt).TotalMilliseconds);
dt = DateTime.Now;
Console.WriteLine();
Console.WriteLine("The main thread is waiting for either task to complete.");
ThreadPool.QueueUserWorkItem(new WaitCallback(DoTask), waitHandles[0]);
ThreadPool.QueueUserWorkItem(new WaitCallback(DoTask), waitHandles[1]);
int index = WaitHandle.WaitAny(waitHandles);
Console.WriteLine("Task {0} finished first (time waited={1}).",
index + 1, (DateTime.Now - dt).TotalMilliseconds);
}

static void DoTask(Object state)
{
AutoResetEvent are = (AutoResetEvent) state;
int time = 1000 * r.Next(2, 10);
Console.WriteLine("Performing a task for {0} milliseconds.", time);
Thread.Sleep(time);
are.Set();
}
}



7.Mutex 类

Mutex与上述的Monitor比较接近,不过Mutex不具备Wait,Pulse,PulseAll的功能,因此,我们不能使用Mutex实现类似的唤醒的功能。不过Mutex有一个比较大的特点,Mutex是跨进程的,因此我们可以在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体。尽管Mutex也可以实现进程内的线程同步,而且功能也更强大,但这种情况下,还是推荐使用Monitor,因为Mutex类是win32封装的,所以它所需要的互操作转换更耗资源。使用方法如下:

static void Main()
{
bool createdNew;
var mutex = new Mutex(false, "SingletonWinAppMutex", out createdNew);
if(!createdNew)
{
MessageBox.Show("系统已启用!");
Application.Exit();
return;
}
}


8.Semaphore 类

Semaphore 类用于限制访问某资源的线程数量,类似互斥锁。
通过使用一个计数器来控制对共享资源的访问,如果计数器大于0,就允许访问,如果等于0,就拒绝访问。计数器累计的是“许可证”的数目,为了访问某个资源。线程必须从信号量获取一个许可证。通常在使用信号量时,希望访问共享资源的线程将尝试获取一个许可证,如果信号量的计数器大于0,线程将获取一个许可证并将信号量的计数器减1,否则先线程将阻塞,直到获取一个许可证;当线程不再需要共享资源时,将释放锁拥有的许可证,并将许可证的数量加1,如果有其他的线程正在等待许可证,那么该线程将立刻获取许可证。
另外,允许同时访问的资源的进程数量是在创建信号量时指定的,如果创建一个允许进程访问的信号量数目为1,则该信号量就和互斥锁的用法一样。
public class mythread
{
public Thread thrd;
//创建一个可授权2个许可证的信号量,且初始值为2
static Semaphore sem = new Semaphore(2, 2);

public mythread(string name)
{
thrd = new Thread(this.run);
thrd.Name = name;
thrd.Start();
}
void run()
{
Console.WriteLine(thrd.Name + "正在等待一个许可证……");
//申请一个许可证
sem.WaitOne();
Console.WriteLine(thrd.Name + "申请到许可证……");
for (int i = 0; i < 4; i++)
{
Console.WriteLine(thrd.Name + ": " + i);
Thread.Sleep(1000);
}
Console.WriteLine(thrd.Name + " 释放许可证……");
//释放
sem.Release();
}
}
class Program
{

static void Main()
{
mythread mythrd1 = new mythread("Thrd #1");
mythread mythrd2 = new mythread("Thrd #2");
mythread mythrd3 = new mythread("Thrd #3");
mythread mythrd4 = new mythread("Thrd #4");
mythrd1.thrd.Join();
mythrd2.thrd.Join();
mythrd3.thrd.Join();
mythrd4.thrd.Join();
}
}




9.ReaderWriterLockSlim 类

读写锁的概念很简单,允许多个线程同时获取读锁,但同一时间只允许一个线程获得写锁,因此也称作共享-独占锁。在C#中,推荐使用ReaderWriterLockSlim类来完成读写锁的功能。某些场合下,对一个对象的读取次数远远大于修改次数,如果只是简单的用lock方式加锁,则会影响读取的效率。而如果采用读写锁,则多个线程可以同时读取该对象,只有等到对象被写入锁占用的时候,才会阻塞。简单的说,当某个线程进入读取模式时,此时其他线程依然能进入读取模式,假设此时一个线程要进入写入模式,那么他不得不被阻塞。直到读取模式退出为止。同样的,如果某个线程进入了写入模式,那么其他线程无论是要写入还是读取,都是会被阻塞的。
进入写入/读取模式有2种方法:
EnterReadLock尝试进入写入模式锁定状态。
TryEnterReadLock(Int32) 尝试进入读取模式锁定状态,可以选择整数超时时间。
EnterWriteLock 尝试进入写入模式锁定状态。
TryEnterWriteLock(Int32) 尝试进入写入模式锁定状态,可以选择超时时间。
退出写入/读取模式有2种方法:
ExitReadLock 减少读取模式的递归计数,并在生成的计数为 0(零)时退出读取模式。
ExitWriteLock 减少写入模式的递归计数,并在生成的计数为 0(零)时退出写入模式。
代码示例如下:

public class Program
{
static private ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();
static void Main(string[] args)
{
Thread t_read1 = new Thread(new ThreadStart(ReadSomething));
t_read1.Start();
Console.WriteLine("{0} Create Thread ID {1} , Start ReadSomething", DateTime.Now.ToString("hh:mm:ss fff"), t_read1.GetHashCode());
Thread t_read2 = new Thread(new ThreadStart(ReadSomething));
t_read2.Start();
Console.WriteLine("{0} Create Thread ID {1} , Start ReadSomething", DateTime.Now.ToString("hh:mm:ss fff"), t_read2.GetHashCode());
Thread t_write1 = new Thread(new ThreadStart(WriteSomething));
t_write1.Start();
Console.WriteLine("{0} Create Thread ID {1} , Start WriteSomething", DateTime.Now.ToString("hh:mm:ss fff"), t_write1.GetHashCode());
}
static public void ReadSomething()
{
Console.WriteLine("{0} Thread ID {1} Begin EnterReadLock...", DateTime.Now.ToString("hh:mm:ss fff"), Thread.CurrentThread.GetHashCode());
rwl.EnterReadLock();
try
{
Console.WriteLine("{0} Thread ID {1} reading sth...", DateTime.Now.ToString("hh:mm:ss fff"), Thread.CurrentThread.GetHashCode());
Thread.Sleep(5000);//模拟读取信息
Console.WriteLine("{0} Thread ID {1} reading end.", DateTime.Now.ToString("hh:mm:ss fff"), Thread.CurrentThread.GetHashCode());
}
finally
{
rwl.ExitReadLock();
Console.WriteLine("{0} Thread ID {1} ExitReadLock...", DateTime.Now.ToString("hh:mm:ss fff"), Thread.CurrentThread.GetHashCode());
}
}
static public void WriteSomething()
{
Console.WriteLine("{0} Thread ID {1} Begin EnterWriteLock...", DateTime.Now.ToString("hh:mm:ss fff"), Thread.CurrentThread.GetHashCode());
rwl.EnterWriteLock();
try
{
Console.WriteLine("{0} Thread ID {1} writing sth...", DateTime.Now.ToString("hh:mm:ss fff"), Thread.CurrentThread.GetHashCode());
Thread.Sleep(10000);//模拟写入信息
Console.WriteLine("{0} Thread ID {1} writing end.", DateTime.Now.ToString("hh:mm:ss fff"), Thread.CurrentThread.GetHashCode());
}
finally
{
rwl.ExitWriteLock();
Console.WriteLine("{0} Thread ID {1} ExitWriteLock...", DateTime.Now.ToString("hh:mm:ss fff"), Thread.CurrentThread.GetHashCode());
}
}
}
以上为几种线程同步方式,对于同步技术来说,还有很多种其他方式。能力有限,这里只介绍这几种。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C# 多线程