线程同步
所谓同步,是指在某一时刻只有一个线程可以访问变量
当一个线程写入一个变量,同时有其他线程读取或写入这个变量时,就应同步变量
高级语言程序中的一条语句在最后编译好的汇编语言机器码中可能被翻译为多条语句,在操作系统调度时被划分到不同时间片中
只要一条C#语句被翻译为多个本地代码命令,线程的时间片就有可能在执行该语句的进程中终止
- 读“垃圾”数据
- 不正确写入
在C#中处理同步
通过对指定对象的加锁和解锁可以同步代码段的访问
System.Threading.Monitor类提供的静态方法
- public static void Enter ( Object obj )
在指定对象上获取排它锁 - public static bool TryEnter ( Object obj )
试图获取指定对象上的排它锁 - public static bool TryEnter( Object obj, TimeSpan timeout )
可以指定一个等待被锁定的超时值 - public static void TryEnter( Object obj, TimeSpan timeout, ref bool lockTaken )
- public static void Exit ( Object obj )
释放指定对象上的排它锁 - public static bool Wait ( Object obj )
释放对象上的锁并阻塞当前线程,直到它重新获取该锁。 - public static void Pulse ( Object obj )
通知等待队列中的线程锁定对象状态的更改。 - public static void PulseAll ( Object obj )
通知所有的等待线程对象状态的更改。
public void some_method ( )
{
//获取锁
Monitor.Enter (this);
//处理需要同步的代码
//释放锁
Monitor.Exit (this);
}
如果代码在获取锁和释放锁之间发生异常,将导致锁永远
得不到释放,其它线程将得不到锁,可能导致死锁。
public void some_method ( )
{
//获取锁
Monitor.Enter (this);
try {
//处理需要同步的代码
}
catch { … }
//释放锁
finally {
Monitor.Exit (this);
}
}
if ( Monitor.TryEnter ( obj, 500 ) )
//如果得到obj的锁定,TryEnter方法就返回true,访问由obj锁定的状态;如果另一个线程锁定obj的时间超过了500ms,TryEnter就返回false,线程不再等待,执行其他操作。
{
try
{
……
}
finally
{
Monitor.Exit(obj);
}
}
else
{
……
}
Bool lockTaken=false;
Monitor.TryEnter(obj, 500, ref lockTaken);
if (lockTaken)
{
try {
// acquired the lock
// synchronized region for obj
}
finally
{
Monitor.Exit(obj);
}
}
else { …. }
C#的lock关键字提供了与Monitor.Enter和Monitor.Exit同样的功能
lock ( x )
{
//使用x的语句
}
当执行带有lock关键字的复合语句时,独占锁会保留下来
同步
lock语句与线程安全
Lock语句是设置锁定和解除锁定的一种简单方式
SyncRoot模式:创建一个私有对象syncRoot,将这个对象用于lock语句
Interlocked
Interlocked类用于使变量的简单语句原子化
Interlocked类提供了以线程安全的方式递增、递减和交换值的方法
与其他同步技术相比,使用Interlocked类会快得多。但是,它只能用于简单的同步问题
Interlocked类的常用方法
long Add ( ref long location1, long value )
以原子操作的形式,相加两个64位整数并用两者的和替换第一个整数
long Increment( ref long location )
以原子操作的形式,递增指定的变量的值并存储结果
long Decrement ( ref long location )
以原子操作的形式,递减指定的变量的值并存储结果
long Exchange ( ref long location1, long value )
以原子操作的形式,将一个变量设置为指定的值并返回变量的初始值
public int State
{
get
{
lock (this)
{
return ++state;
}
}
}
=>
public int State
{
get
{
return Interlocked.
Increment(ref state);
}
}
等待句柄
WaitHandle是一个抽象基类
WaitHandle类定义的执行等待的方法
WaitOne
-
bool WaitOne (int millisecondsTimeout, bool exitContext)
是一个实例方法,利用它可以等待一个信号的发生。也可以为最大等待时间指定一个超时值
exitContext是true,则等待之前先退出上下文的同步域,然后稍后重新获取它,否则为false。 -
public static bool WaitAll(WaitHandle[] waitHandles)
是一个静态方法,用于传送WaitHandle对象的数组,并等待所有的句柄发出信号 -
public static int WaitAny(WaitHandle[] waitHandles )
是一个静态方法,用于传送WaitHandle对象的数组,并等待其中一个句柄发出信号。这个方法返回发出信号的等待句柄对象的索引,以便确定可以在程序中继续执行什么功能。如果在句柄发出信号之前超时,WaitAny( )就返回WaitTimeout。
IAsyncResult ar = d1.BeginInvoke(1, 3000, null, null);
while (true)
{
Console.Write(".");
if (ar.AsyncWaitHandle.WaitOne(50, false))
{
Console.WriteLine("Can get the result now");
break;
}
}
int result = d1.EndInvoke(ar);
Console.WriteLine("result: {0}", result);
同步时要注意的问题
线程同步的代价
在对象上放置和解开锁会带来某些系统开销
线程同步使用得越多,等待释放对象的线程就越多,临时丧失了多线程的优势
死锁的产生
两个线程需要访问互锁的资源
lock (a)
{
lock (b)
{
// do something
}
}
lock (b)
{
lock (a)
{
// do something
}
}
避免死锁
让两个线程以相同的顺序在对象上声明加锁
lock (a)
{
lock (b)
{
// do something
}
}
lock (a)
{
lock (b)
{
// do something
}
}
竞态条件
当几个线程试图访问同一个数据,但没有充分考虑其他线程的执行情况时,就会发生竞态
例:有一个对象数组,它所在类包含了一个属性,表示已处理完毕的对象数目。包含了一个方法,获取指定下标的对象。多个线程对该对象数组进行访问,并修改属性值。
class ArrayController
{
private object[] ary;
private int cnt = 0;
private int size;
public ArrayController(object[] aa)
{
ary = aa;
size = aa.Length; }
object GetObject(int index)
{
return ary[index];
}
int ObjectsProcessed {
get
{ return cnt; }
set
{ cnt = value; }
}
public void ProcessObject()
{
int nextIndex;
while (true)
{
Console.WriteLine(Thread.CurrentThread.Name);
lock (this)
{
nextIndex = ObjectsProcessed; //可能产生竞态
Console.WriteLine("object to be processed next is" + nextIndex);
++ObjectsProcessed;
}
if (nextIndex >= size) {
Console.WriteLine("下标越界");
return;
}
else {
object next = GetObject(nextIndex);
Console.WriteLine(next);
}
}
}
class Student
{
private int id;
private string name;
private int age;
public Student(int i, string n, int a)
{
id=i;
name=n;
age=a;
}
public override string ToString()
{
return "id= "+id+" name= "+name+" age= "+age;
}
}
static void Main(string[] args)
{
Student[] ss=new Student[10];
for (int i = 0; i < 10; i++)
ss[i] = new Student(200800 + i, "student" + i, 20 + i);
ArrayController ac=new ArrayController(ss);
Thread t1=new Thread(new ThreadStart(ac.ProcessObject));
t1.Name = "thread A";
Thread t2=new Thread(new ThreadStart(ac.ProcessObject));
t2.Name = "thread B";
Thread t3= new Thread(new ThreadStart(ac.ProcessObject));
t3.Name = "thread C";
t1.Start();
t2.Start();
t3.Start();
}