您的位置:首页 > 其它

任务,线程和同步(六)之线程问题:争用条件和死锁

2017-02-25 14:57 429 查看

线程问题

用多个线程编程并不容易。在启动访问相同数据的多个线程时,会间歇性地遇到难以发现的问题。如果使用任务,并行LINQ或Pararllel类,也会遇到这行问题,必须特别注意同步问题和多线程可能发生的其他问题。

1.争用条件

如果2个或者2个以上的线程访问相同的对象,并且对共享状态的访问没有同步,就会出现争用条件。

下面的一个例子定义一个类StateObject类,它包含一个int字段和一个ChangeState方法。在ChangeState方法的实现代码中,验证状态变量是否包含5,如果包含,就将其递增。Trace.Assert,它立刻验证state现在是否包含6.

在给包含5的变量递增1后,可能认为该变量的值为6,但事实上不一定是这样。如果一个线程刚刚执行完if(state==5)语句,它就被其他线程抢占,调度器运行另一个线程。第二个线程现在进入IF中,因为state的值仍是6.第一个线程现在再次被调度,在下一条语句中,state递增到7.这时就发生了争用条件,并显示断言信息。

public class StateObject
{
private  int state = 5;

public   void ChangeState(int loop)
{
if (state == 5)
{
state++;
Trace.Assert(state == 6, "race condition occurred after" + loop + "loops");
}

state = 5;
}

}


下面来验证这一段代码,用一个无限while循环中,调用ChangeState方法,变量i仅用于显示断言的信息中的循环次数。

public class SampleTask
{
public void RaceCondition(object o)
{
Trace.Assert(o is StateObject, "o must be of type stateobjec");
StateObject state = o as StateObject;
int i = 0;
while (true)
{
state.ChangeState(i++);
}
}
}


在main方法中,新建一个stateobject对象,它由所有任务共享。通过使用传递给Task的run方法的lambda表达式调用RaceCondition方法来创建Task对象。然后,主线程等待用户输入,但是会出现争用,程序在读取之前就会挂起。

/// <summary>
/// 应用程序的主入口点。
/// </summary>
static void Main()
{

var state = new StateObject();
for (int i = 0; i < 2; i++)
{
Task.Run(() => new SampleTask().RaceCondition(state));
}

Console.ReadKey();
}


运行结果


启动程序,就会出现争用条件。第一个出现争用条件取决于系统以及程序构建为发布版本还是调试版本。

为了避免该问题,可以设置共享对象,这个可以在线程中完成:用下面的LOCK语句锁定在线程中共享state变量。只有一个线程能在锁定块中处理共享state对象。由于这个对象在所在的线程之间共享,因此如果一个线程锁定state,另一个线程就必须等待该锁定解除。一旦接受锁定,线程就拥有该锁定,直到该锁定块的末尾才解除锁定。如果改变state变量引用的对象的每个线程都使用一个锁定,就不会出现争用条件。

public void RaceCondition(object o)
{
Trace.Assert(o is StateObject, "o must be of type stateobjec");
StateObject state = o as StateObject;
int i = 0;

while (true)
{
lock (state)//
{
state.ChangeState(i++);
}
}
}


在使用共享对象时,除了进行锁定之外,还可以将共享设置为线程安全的对象。

public class StateObject
{
private  int state = 5;

private object sync = new object();
public   void ChangeState(int loop)
{
lock (sync)
{
if (state == 5)
{
state++;
Trace.Assert(state == 6, "race condition occurred after" + loop + "loops");
}
state = 5;
}
}
}


2.死锁

过多的锁定也会有麻烦。在死锁中,至少有2个线程被挂起,并等待对方解除锁定。由于2个线程都在等待对方,就会出现死锁,线程将无线等待下去。

public class StateObject
{
private  int state = 5;

private object sync = new object();
public  void ChangeState(int loop)
{

if (state == 5)
{
state++;
Trace.Assert(state == 6, "race condition occurred after" + loop + "loops");
}
state = 5;
}
}

public class SampleTask1
{
private StateObject s1;
private StateObject s2;

public SampleTask1(StateObject s1,StateObject s2)
{
this.s1 = s1;
this.s2 = s2;
}

public void Deadlock1()
{
int i = 0;
while (true)
{
lock (s1)
{
lock (s2)
{
s1.ChangeState(i);
s2.ChangeState(i++);
Console.WriteLine("still running {0}",i);
}
}

}
}

public void Deadlock2()
{
int i = 0;
while (true)
{
lock (s2)
{
lock (s1)
{
s1.ChangeState(i);
s2.ChangeState(i++);
Console.WriteLine("still running {0}", i);
}
}

}
}

}


Deadlock1,Deadlock2方法现在改变2个对象s1和s2的状态,所以产生了2个锁。Deadlock1先锁定s1,接着锁定s2。Deadlock2先锁定s2,再锁定s1。现在,有可能Deadlock1方法在s1锁定会被解除,接着出现一次线程切换,Deadlock2方法开始运行并锁定S2.第2个线程现在等待s1锁定的解除。因为它需要等待,所以线程调度器会再次调度第一个线程,但第一个线程在等待s2锁定的解除。这两个线程现在都在等待,只要锁定块没有结束,就不会解除锁定。

/// <summary>
/// 应用程序的主入口点。
/// </summary>
static void Main()
{

var state1 = new StateObject();
var state2 = new StateObject();

new Task(new SampleTask1(state1,state2).Deadlock1).Start();
new Task(new SampleTask1(state1, state2).Deadlock2).Start();

Console.ReadKey();
}


结果是,运行程序会许多次循环,不久就没有响应了。“仍在运行”的消息仅写入控制台几次。同样,死锁问题的发生频率也取决于系统配置,每次运行的结果都不同。

为了避免这个问题,可以在应用程序的体系架构中,从一开始就设计好锁定顺序,也可以为了锁定定义超时时间。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  争用条件 死锁 线程