多线程共享一个进程的地址空间,虽然线程间通信容易进行,但是多线程同时访问共享对象时需要引入同步和互斥机制。
线程同步:多个任务按照约定的顺序相互配合完成一件事,基于信号量的概念提出了一种同步机制,由信号量来决定线程是继续运行还是阻塞等待。
线程互斥:线程间同步访问临界资源时的互相等待,引入互斥锁的目的是用来保证共享资源数据操作的完整性。互斥锁主要用来保护临界资源,每个临界资源都有一个互斥锁来保护,任何时刻最多只能有一个线程能访问该资源。线程必须先获得互斥锁才能访问临界资源,访问玩临界资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。临界资源是指每次仅允许一个进程访问的资源,属于临界资源的硬件有打印机、磁带机等,软件有消息缓冲队列、变量、数组、缓冲区等,线程间应采取互斥方式,实现对这种资源的共享。每个线程中访问临界资源的那段代码称为临界区。
在C#中实现线程的同步有几种方法:lock、Mutex、Monitor、Semaphore、Interlocked和ReaderWriterLock等。同步策略也可以分为同步上下文、同步代码区、手动同步几种方式。
1、同步代码区
同步代码区,它是针对特定部分代码进行同步的一种方法。
(1)、lock同步, lock语句是一种有效的、不跨越多个方法的小代码块同步的做法,也就是使用lock语句只能在某个方法的部分代码之间,不能跨越方法。
参考程序:
public class ThreadTwo : MonoBehaviour
{
private Thread _trdOne;
private Thread _trdTwo;
private List<string> _slTricket = new List<string>();
private object _oLock = new object();
void Start ()
{
TricketList();
_trdOne = new Thread(new ThreadStart(Run));
_trdTwo = new Thread(new ThreadStart(Run));
_trdOne.Name = "trdOne";
_trdTwo.Name = "trdTwo";
_trdOne.Start();
_trdTwo.Start();
}
void TricketList()
{
for(int i = 1 ;i <= 100 ;i++)
{
_slTricket.Add(i.ToString().PadLeft(3,'0'));
}
}
void Run()
{
lock(_oLock)
{
while(_slTricket.Count >0)
{
string m_sTricket = _slTricket[0];
print(Thread.CurrentThread.Name + " 售出一张票,票号: " + m_sTricket);
_slTricket.RemoveAt(0);
}
}
}
}
(2)、Monitor类
参考程序:
void Run()
{
Monitor.Enter(_oLock);
while(_slTricket.Count >0)
{
string m_sTricket = _slTricket[0];
print(Thread.CurrentThread.Name + " 售出一张票,票号: " + m_sTricket);
_slTricket.RemoveAt(0);
}
Monitor.Exit(_oLock);
}
这段代码最终运行的效果和使用lock关键字来同步的效果一样。比较之下,大家会发现使用lock关键字来保持同步的差别不大:”lock(objLock){“被换成了”Monitor.Enter(objLock);”,而”}”被换成了” Monitor.Exit(objLock);”。
实际上如果你通过其它方式查看最终生成的IL代码,你会发现使用lock关键字的代码实际上是用Monitor来实现的。
lock (oLock){
//同步代码
}
实际上是相当于:
try{
Monitor.Enter(objLock);
//同步代码
}
finally
{
Monitor.Exit(objLock);
}
Monitor类除了Enter()和Exit()方法之外,还有Wait()和Pulse()方法。Wait()方法是临时释放当前获得的锁,并使当前对象处于阻塞状态,Pulse()方法是通知处于等待状态的对象可以准备就绪了,它一会就会释放锁。
下面我们利用这两个方法来完成一个协同的线程,一个线程负责随机产生数据,一个线程负责将生成的数据显示出来。参考程序:
using UnityEngine;
using System;
using System.Collections;
using System.Threading;
public class ThreadThree : MonoBehaviour {
private Thread _trdOne;
private Thread _trdTwo;
void Start ()
{
ThreadWaitAndPluse m_cWaitAPluse =new ThreadWaitAndPluse();
Thread _trdOne = new Thread(new ThreadStart(m_cWaitAPluse.ThreadMethodOne));
_trdOne.Start();
Thread _trdTwo = new Thread(new ThreadStart(m_cWaitAPluse.ThreadMethodTwo));
_trdTwo.Start();
}
}
public class ThreadWaitAndPluse
{
private object _oLock;
private int _iRandomNum;
private System.Random _radRandom;
public ThreadWaitAndPluse()
{
_oLock = new object();
_radRandom = new System.Random();
}
//显示随机生成的数据
public void ThreadMethodOne()
{
//获取对象锁
Monitor.Enter(_oLock);
Debug.Log("当前进入的线程:显示随机线程" + Thread.CurrentThread.GetHashCode());
for (int i = 0; i < 5; i++)
{
//释放对象锁,并阻止当前线程
Monitor.Wait(_oLock);
Debug.Log("显示随机值线程 :工作");
Debug.Log("显示随机值线程 :得到了数据,RandomNum= " + _iRandomNum + " ;Thread ID= " + Thread.CurrentThread.GetHashCode());
//通知其它等待锁的对象状态已经发生改变,当这个对象释放锁之后等待锁的对象将会活得锁
Monitor.Pulse(_oLock);
}
Debug.Log("退出当前线程:显示随机线程" + Thread.CurrentThread.GetHashCode());
//释放对象锁
Monitor.Exit(_oLock);
}
//生成随机数据
public void ThreadMethodTwo()
{
//获取对象锁
Monitor.Enter(_oLock);
Debug.Log("当前进入的线程:生成随机线程" + Thread.CurrentThread.GetHashCode());
for (int i = 0; i < 5; i++)
{
//通知其它等待锁的对象状态已经发生改变,当这个对象释放锁之后等待锁的对象将会活得锁
Monitor.Pulse(_oLock);
Debug.Log("生成随机值线程 :工作");
_iRandomNum = _radRandom.Next(DateTime.Now.Millisecond) ;//生成随机数
Debug.Log("生成随机值线程 :生成了数据,RandomNum=" + _iRandomNum + " ;Thread ID=" + Thread.CurrentThread.GetHashCode());
//释放对象锁,并阻止当前线程
Monitor.Wait(_oLock);
}
Debug.Log("退出当前线程:生成随机线程" + Thread.CurrentThread.GetHashCode());
//释放对象锁
Monitor.Exit(_oLock);
}
}
运行结果:
一般情况下会看到上面的结果,原因是_trdOne的Start()方法在先,所以一般会优先获得执行,_trdOne执行后首先获得对象锁,然后在循环中通过Monitor.Wait(lockObject)方法临时释放对象锁,_trdOne这时处于阻塞状态;这样_trdTwo获得对象锁并且得以执行,trdTwo进入循环后通过Monitor.Pulse(lockObject)方法通知等待同一个对象锁的_trdOne准备好,然后在生成随机数之后临时释放对象锁;接着_trdOne获得了对象锁,执行输出trdTwo生成的数据,之后_trdOne通过Monitor.Wait(lockObject)通知trdTwo准备就绪,并在下一个循环中通过 Monitor.Wait(lockObject)方法临时释放对象锁,就这样_trdOne和trdTwo交替执行,得到了上面的结果。
但是有时也会出现_trdTwo优先执行的情况出现,尽管_trdOne.Start() 在_trdTwo.Start() 之前,但是不一定_trdOne 一定在_trdTwo 之前执行。