📄 线程的同步与互斥.txt
字号:
实验五要求
实验四的内容,估计大家应该是有很多内容是看不明白的。
*********************************************
* 本实验涉及程序,均可在命令提示符下使用:*
* CSC 程序名称 *
* 编译并执行。 *
*********************************************
1 一个公共变量的多线程减问题,程序如下:文件名称:Join线程\Th1.cs
namespace th1
{
class ThTest1
{
public int N=20;
public void M1()
{
int i;
for(i=0;i<10;i++)
{
N--;
Console.WriteLine("In M1:N="+N);
}
}
public void M2()
{
int i;
for(i=0;i<10;i++)
{
N--;
Console.WriteLine("In M2:N="+N);
}
}
static void Main(string[] args)
{
ThTest1 P=new ThTest1();
ThreadStart ts1=new ThreadStart(P.M1);
ThreadStart ts2=new ThreadStart (P.M2);
Thread t1=new Thread(ts1);
Thread t2=new Thread(ts2);
t1.Start();
t2.Start();
}
}
}
这个程序中,N假设是我们程序中的临界资源,这个程序中没有对线程、公共变量N做任何控制,执行这个程序,
结果如下:
In M2:N=18
In M2:N=17
In M1:N=19
In M1:N=16
In M1:N=15
In M1:N=14
In M1:N=13
In M1:N=12
In M1:N=11
In M1:N=10
In M1:N=9
In M1:N=8
In M2:N=7
In M2:N=6
In M2:N=5
In M2:N=4
In M2:N=3
In M2:N=2
In M2:N=1
In M2:N=0
我们不难发现:在线程M2中,10次循环减N未完成,则已经有M1线程插入,或一开始就执行M1线程,但同样在10次
递减未完成时,就已经插入M1线程。
事实上,可能每一次运行这个程序,结果都是不同的,而且不同的计算机上运行,结果也不一致。
我们希望的结果是这样的:
In M1:N=19
In M1:N=18
In M1:N=17
In M1:N=16
In M1:N=15
In M1:N=14
In M1:N=13
In M1:N=12
In M1:N=11
In M1:N=10
In M2:N=9
In M2:N=8
In M2:N=7
In M2:N=6
In M2:N=5
In M2:N=4
In M2:N=3
In M2:N=2
In M2:N=1
In M2:N=0
就是说:由一个线程完成10次递减工作后,再有另一个线程继续做递减。我们需要一个每次都一致的、
有序的准确结果,而不是每次都不一样的结果。
解决上述问题,就是所谓的线程同步问题,我们必须强制在一个时间内仅仅只有一个线程来工作。解决这个问题的方法
之一就是进程Join
2 Join线程
程序如下:文件名称:Join线程\Th2.cs
using System;
using System.Threading;
namespace th2
{
class ThTest2
{
public int N=20;
public void M1()
{
int i;
for(i=0;i<10;i++)
{
N--;
Console.WriteLine("In M1:N="+N);
}
}
public void M2()
{
int i;
for(i=0;i<10;i++)
{
N--;
Console.WriteLine("In M2:N="+N);
}
}
static void Main(string[] args)
{
ThTest2 P=new ThTest2();
ThreadStart ts1=new ThreadStart(P.M1);
ThreadStart ts2=new ThreadStart (P.M2);
Thread t1=new Thread(ts1);
Thread t2=new Thread(ts2);
t1.Start();
t1.Join(); //这里是最关键的语句:合并两个线程。同Th1.cs比较,仅仅这里加了一句;
t2.Start();
}
}
}
合并t1、t2两个线程,强制使两个线程的执行次序成为一个有序的队列,这样,实际就相当于仅有一个线程、在
特定的时刻访问临界资源N。
这个程序的结果就是我们所需要的了。
请修改上述程序Th1.cs,使用教材P83、解法1:只能有一个线程递减N。事实上,该方法在单CPU计算机上有很有效的。
在没有提供诸如Join()这类方法的时候,老程序员们都这么做。
在C#中,另一个更有效的方法是临界资源加锁,再看一个程序,这也是一个没做资源、线程控制的例子:
3 临界资源不加锁的多线程程序
本程序启动了两个线程:t1、t2,每个线程所做的处理仅仅为:x++;y++;
由于x、y未加锁,所以多线程处理后,这两个变量中的结果完全可能是不同的。
注意:该程序请用Ctrl-C来结束
程序文件:Lock临界资源\Th3.cs
using System;
using System.Threading;
namespace th3
{
class ThTest3
{
static void Main(string[] args)
{
MyData num=new MyData(); //说明num是一个MyData类型的对象
Thread t1=new Thread(new ThreadStart(num.Run)); //建立两个线程,共同处理Run()
Thread t2=new Thread(new ThreadStart(num.Run)); //两个线程使用同一个对象num,所以也是同一个x,y
t1.Start();
t2.Start(); //启动两个线程,运行一个程序函数Run(),加x,y
for (int i=0;i<10;i++) //鉴于Run()实际是死循环,所以主线程Main等它运行10秒吧!
{
Thread.Sleep(1000);
num.TestEquals(); //对比x,y并显示结果
}
}
}
class MyData
{
private int x=0,y=0;
public void Inc()
{
x++;y++;
}
public void TestEquals()
{
Console.WriteLine (x+","+y+":"+(x==y));//类似C中:printf("%d,%d:%s\n",x,y,(x==y)?"True":"False);
}
public void Run()
{
while(true)
Inc();
}
}
}
注意:这个程序中有两个类:ThTest3、MyData。
类MyData中的Inc()方法很简单,加x、y,而Run()则就有点不厚道:死循环加啊,当然,这是为了让大家看明白结果的
愚蠢做法,实际编程中请一定不要这样。
先分析Inc():
public void Inc()
{
x++;y++;
}
从程序中可以看出:x,y是一个线程里“同时”被加的变量。从一般意义上理解:
Main()中的t1、t2线程中运算,x,y应该是相等的!也就是说,在最后调用TestEquale()时,结果都应该是:真!
但实际运行这个程序,10秒内肯定会有False的结果出现!
为什么?很简单:仅仅在t1这个线程内、仅仅做x++;y++这样的最简单操作,由于时间片的循环,当只做完x++的时候,
y++还没来得及做,已经被t2线程所截断!结果肯定是x、y的结果不同了!
这就是所谓抢断式分时操作系统的特性!
这是我的一组结果:当然,你们的或许和这个也并不一致,但有False就算。
66759156,66759156:True
148574656,148574656:True
216236716,216236716:True
283601280,283601280:True
351035547,351035547:True
418685630,418685630:True
480403593,480403592:False
537051104,537051103:False
593892308,593892308:True
651068184,651068184:True
^C
什么意思:这个程序没有同步加x、y,这是很恐怖的结果!很多情况下,我们需要处理类似这样的工作:扣你的钱、再
加到我的名字下面(想的很美吧?),这种工作必须是不可分割的!不允许有上述这种情况发生。
解决这个问题的方法就是加锁。
4 有加锁机制的多线程编程
程序文件:Lock临界资源\Th4.cs
基于3的问题,我们首先要对x、y变量加锁,看看下面的程序,加锁是很简单的事情
using System;
using System.Threading;
namespace th4
{
class ThTest4
{
static void Main(string[] args)
{
MyData num=new MyData();
Thread t1=new Thread(new ThreadStart(num.Run));
Thread t2=new Thread(new ThreadStart(num.Run));
t1.Start();
t2.Start();
for (int i=0;i<10;i++)
{
Thread.Sleep(1000);
num.TestEquals();
}
}
}
class MyData
{
private int x=0,y=0;
public void Inc()
{
lock(this) //加锁,就这点学问,不过请看明白this是什么意思!
{
x++;y++;
}
}
public void TestEquals()
{
Console.WriteLine (x+","+y+":"+(x==y));
}
public void Run()
{
while(true)
Inc();
}
}
}
再编译运行这个程序吧,肯定没False了,再有砸MS去啊!
数据库经常用这个技术,分的更细致:表、记录加锁都是这个意思。
加锁的含义就是:仅仅允许一个进程访问这个临界资源!
请总结3、4,别忘了这些手艺即可。
最后,请分析在什么情况下使用Join()方法,什么时候使用Lock比较好。
5 多线程求PI
看了1、2、3、4,估计有这样的想法:这么处理线程,线程都互斥了,还有什么意义啊?
这要根据实际问题分析:实际中有的问题,临界资源是互斥的,有的也不一定,例如以下这个例子:
Pi/4=1-1/3+1/5-1/5.......
我们期望一个线程计算前2000项,一个线程计算后2000项,结果加在一个变量里面,这个问题中,求和变量就不是临界资源,
两个线程就不必互斥,如果互斥,那结果就麻烦了!
所以说:是否需要同步线程,首先要看是否有临界资源!
但实际情况中,一个程序如果没有临界资源,线程就不必同步了,下面的这个程序,其函数S1()、S2()中使用了变量InS1、
InS2,并且在Main()中使用了非常关键的一段:
while(P.InS1==1 || P.InS2==1)
;
请分析这段语句的作用吧!看看注释后是否还能显示结果?
这一招大概是操作系统教材中没有的,但很多老手们都知道这种做法,简单而且实用。请总结这种做法适用的条件。
using System;
using System.Threading;
namespace PIC
{
class MyClass
{
public double PI;
public short InS1=0,InS2=0;
public void GetPI(int n,int m)
{
int i;
double Sign=1;
for(i=n;i<m;i++)
{
PI=PI+Sign/(2.0*(double )i+1.0);
Sign=-Sign;
}
}
public void S1()
{
InS1=1;
GetPI(0,100000);
InS1=0;
}
public void S2()
{
InS2=1;
GetPI(100001,200000);
InS2=0;
}
public static void Main(string[] args)
{
MyClass P=new MyClass();
P.PI=0;
ThreadStart ts1=new ThreadStart(P.S1);
ThreadStart ts2=new ThreadStart(P.S2);
Thread t1=new Thread(ts1);
Thread t2=new Thread(ts2);
t1.Start();
t2.Start();
while(P.InS1==1 || P.InS2==1)
;
Console.WriteLine("PI="+P.PI*4);
}
}
}
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -