今天5月22号,阶段性总结下我的工作。
最近作的东西涉及到多线程的问题,第一次接触多线程,比较吃力。首先是线程的同步方式方面学习了一些东西:下面的部分是转载的,红字的注意总结等是自己写的注释。
在现代的程序开发中,资源的同步是一个比较重要的课题,在.Net中,对这部分有很丰富类库供我们使用,现在总结一下在各种情况下对资源同步的
机制。
1.将字段声明为volatile
当一个字段被声明为volatile时,CLR中一些管理代码和内存的内部机制将负责对字段进行同步,并且总能保证读取到的字段信息都为最新的值,被声明为
volatile的字段必须具备以下特征之一
1.为引用类型
2.一个指针(在不安全代码中)
3.sbyte,byte,short,ushort,int,uint,char,float,bool
4.一个使用底层类型的枚举类型
2.使用System.Threading.Interlocked
类
在许多增对整数的操作中,我们都比较容易忽视线程的问题,例如执行下列代码
i = i + 1;
实际上,上述代码分为3步骤
1). 从内存中,读取i的值
2). 将读取出来的值加1
3). 将新的值写入内存中
在单线程上,这个操作不会有任何问题,但是当i被多个线程访问时,问题就出现了,对i进行修改的线程,在上述的任何一部都有可能被其它读取线程打断,想象一下,
当操作线程执行完第二步,准备将新的值写入内存中时,此时其它读取线程获得了执行权,这时读取到的i的值并不是我们想要的,因此,这个操作不具备原子性,在.Net中,使用Interlocked类能确保操作的原子性,Interlocked类有以下的方法
Increment
Decrement
Exchange
上述的方法的参数都为带ref 标识的参数,因此我们说,这些方法保证了数据的原子性,在多线程中,涉及到整数的操作时,数据的原子性值得考虑,Interlocked的操作代码如下
int
i
=
0
;
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);
输出信息如下
3.使用lock关键字
地球人都知道,使用lock关键字可以获取一个对象的独占权,任何需要获取这个对象的操作的线程必须等待以获取该对象的线程释放独占权,lock提供了简单的同步资源的方法,与Java中的synchronized关键字类似。
lock关键字的使用示例代码如下
object
o
=
new
object
();
lock
(o)
{
Console.WriteLine("O");
}
4.使用System.Theading.Monitor类进行同步
System.Threading.Monitor类提供了与lock类似的功能,不过与lock不同的是,它能更好的控制同步块,当调用了Monitor的Enter(Object o)方法时,会获取o的独占权,直到调用Exit(Object o)方法时,才会释放对o的独占权,可以多次调用Enter(Object o)方法,只需要调用同样次数的Exit(Object o)方法即可,Monitor类同时提供了TryEnter(Object
o,[int])的一个重载方法,该方法尝试获取o对象的独占权,当获取独占权失败时,将返回false,如下代码
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace MonitorApplication
{
class Program
{
private static object m_monitorObject = new object();
static void Main(string[] args)
{
Thread thread = new Thread(Do);
thread.Name = "Thread1";
Thread thread2 = new Thread(Do);
thread2.Name = "Thread2";
thread.Start();
thread2.Start();
thread.Join();
thread2.Join();
}
static void Do()
{
if (!Monitor.TryEnter(m_monitorObject))
{
Console.WriteLine("Can't visit Object " + Thread.CurrentThread.Name);
return;
}
try
{
Monitor.Enter(m_monitorObject);
Console.WriteLine("Enter Monitor " + Thread.CurrentThread.Name);
Thread.Sleep(5000);
}
finally
{
Monitor.Exit(m_monitorObject);
}
}
}
}
当线程1获取了m_monitorObject对象独占权时,线程2尝试调用TryEnter(m_monitorObject),此时会由于无法获取独占权而返回false,输出信息如下
可以看到线程2无法获取到m_monitorObject的独占权,因此输出了一条错误信息.
Monitor类比lock类提供了一种更优秀的功能,考虑一下如下的场景
1.当你进入某家餐馆时,发现餐馆里坐满了客人,但是你又不想换地方,因此,你选择等待。
2.当餐馆内的服务员发现有空位置时,他通知前台的服务生,服务生给你安排了一个座位,于是你开始享受国际化的大餐。
使用Monitor类就可以实现如上的应用需求,在Monitor类中,提供了如下三个静态方法
Monitor.Pulse(Object o)
Monitor.PulseAll(Object o)
Monitor.Wait(Object o ) // 重载函数
当调用Wait方法时,线程释放资源的独占锁,并且阻塞在wait方法直到另外的线程获取资源的独占锁后,更新资源信息并调用Pulse方法后返回。
我们模拟上述代码如下
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace MonitorApplication
{
class Program
{
private static object m_isFull = new object();
static void Main(string[] args)
{
Thread thread = new Thread(HaveLunch);
thread.Name = "HaveLunchThread";
Thread thread2 = new Thread(SeatChange);
thread2.Name = "SeatChangeThread";
thread.Start();
System.Threading.Thread.Sleep(2000);
thread2.Start();
thread.Join();
thread2.Join();
}
private static void HaveLunch()
{
lock (m_isFull)
{
Console.WriteLine("Wati for seta");
Monitor.Wait(m_isFull);
Console.WriteLine("Have a good lunch!");
}
}
private static void SeatChange()
{
lock (m_isFull)
{
Console.WriteLine("Seat was changed");
Monitor.Pulse(m_isFull);
}
}
}
}
注意:Monitor.Wait(m_isFull);之一句释放了上面的lock(m_isFull)中被锁定的m_isFull,否则thread2永远不会进入m_isFull,永远不会执行Monitor.Pulse(m_isFull),那就死锁ing。
输出信息如下
可见,使用Monitor,我们能实现一种唤醒式的机制,相信在实际应用中有不少类似的场景。
总结:1.Monitor.Enter(m_monitorObject);
2.Monitor.TryEnter(m_monitorObject);
3.Monitor.Pulse(Object o);
4.Monitor.PulseAll(Object o);
5.Monitor.Wait(Object o);//重载函数
5.使用System.Threading.Mutex(互斥体)类实现同步(此个例子没太看懂)
在使用上,Mutex与上述的Monitor比较接近,不过Mutex不具备Wait,Pulse,PulseAll的功能(但是有WaitOne),因此,我们不能使用Mutex实现类似的唤醒的功能,不过Mutex有一个比较大的特点,Mutex是跨进程的,因此我们可以在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体。(上一个都是在类Progrom中定义的互斥体m_isFull。)
考虑如下的代码,代码通过获取一个称为ConfigFileMutex的互斥体,修改配置文件信息,写入一条数据,我们同时开启两个相同的进程进行测试
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.IO;
using System.Diagnostics;
namespace MonitorApplication
{
class Program
{
static void Main(string[] args)
{
Mutex configFileMutex = new Mutex(false, "configFileMutex");//定义了Mutex
Console.WriteLine("Wait for configFileMutex Process Id : " + Process.GetCurrentProcess().Id);
configFileMutex.WaitOne();
Console.WriteLine("Enter configFileMutex Process Id : " + Process.GetCurrentProcess().Id);
System.Threading.Thread.Sleep(10000);
if (!File.Exists(@".\config.txt"))
{
FileStream stream = File.Create(@".\config.txt");
StreamWriter writer = new StreamWriter(stream);
writer.WriteLine("This is a Test!");
writer.Close();
stream.Close();
}
else
{
String[] lines = File.ReadAllLines(@".\config.txt");
for (int i = 0; i < lines.Length; i++)
Console.WriteLine(lines[i]);
}
configFileMutex.ReleaseMutex();
configFileMutex.Close();
}
}
}
运行截图如下
此时,PID 为 4628的进程正在等待PID 为 3216的进程释放configFileMutex的互斥体,因此它阻塞在WaitOne函数中,当PID为3216的进程添加并写入了一条信息
至config.txt文件后,释放configFileMutex的独占权,PID为4628的进程获取configFileMutex的独占权,并从config.txt文件中读取输出PID3216进程写入的信息,截图如下
可见使用Mutex进行同步,同步的互斥体是存在于多个进程间的。
再来看一个关于Mutex的例子:
如何控制好多个线程相互之间的联系,不产生冲突和重复,这需要用到互斥对象,即:System.Threading 命名空间中的 Mutex 类。
我们可以把Mutex看作一个出租车,乘客看作线程。乘客首先等车,然后上车,最后下车。当一个乘客在车上时,其他乘客就只有等他下车以后才可以上车。而线程与Mutex对象的关系也正是如此,线程使用Mutex.WaitOne()方法等待Mutex对象被释放,如果它等待的Mutex对象被释放了,它就自动拥有这个对象,直到它调用Mutex.ReleaseMutex()方法释放这个对象,而在此期间,其他想要获取这个Mutex对象的线程都只有等待。
下面这个例子使用了Mutex对象来同步四个线程,主线程等待四个线程的结束,而这四个线程的运行又是与两个Mutex对象相关联的。
其中还用到AutoResetEvent类的对象,可以把它理解为一个信号灯。这里用它的有信号状态来表示一个线程的结束。
// AutoResetEvent.Set()方法设置它为有信号状态
// AutoResetEvent.Reset()方法设置它为无信号状态
Mutex 类的程序示例:
using
System;
using
System.Threading;
namespace
ThreadExample
{
public
class
MutexSample
{
static
Mutex gM1;
static
Mutex gM2;
const
int
ITERS
=
100
;
static
AutoResetEvent Event1
=
new
AutoResetEvent(
false
);
static
AutoResetEvent Event2
=
new
AutoResetEvent(
false
);
static
AutoResetEvent Event3
=
new
AutoResetEvent(
false
);
static
AutoResetEvent Event4
=
new
AutoResetEvent(
false
);
public
static
void
Main(String[] args)
{
Console.WriteLine(
"
Mutex Sample
"
);
//
创建一个Mutex对象,并且命名为MyMutex
gM1
=
new
Mutex(
true
,
"
MyMutex
"
);
//
创建一个未命名的Mutex 对象.
gM2
=
new
Mutex(
true
);
Console.WriteLine(
"
- Main Owns gM1 and gM2
"
);
AutoResetEvent[] evs
=
new
AutoResetEvent[
4
];
evs[
0
]
=
Event1;
//
为后面的线程t1,t2,t3,t4定义AutoResetEvent对象
evs[
1
]
=
Event2;
evs[
2
]
=
Event3;
evs[
3
]
=
Event4;
MutexSample tm
=
new
MutexSample( );
Thread t1
=
new
Thread(
new
ThreadStart(tm.t1Start));
Thread t2
=
new
Thread(
new
ThreadStart(tm.t2Start));
Thread t3
=
new
Thread(
new
ThreadStart(tm.t3Start));
Thread t4
=
new
Thread(
new
ThreadStart(tm.t4Start));
t1.Start( );
//
使用Mutex.WaitAll()方法等待一个Mutex数组中的对象全部被释放
t2.Start( );
//
使用Mutex.WaitOne()方法等待gM1的释放
t3.Start( );
//
使用Mutex.WaitAny()方法等待一个Mutex数组中任意一个对象被释放
t4.Start( );
//
使用Mutex.WaitOne()方法等待gM2的释放
Thread.Sleep(
2000
);
Console.WriteLine(
"
- Main releases gM1
"
);
gM1.ReleaseMutex( );
//
线程t2,t3结束条件满足
Thread.Sleep(
1000
);
Console.WriteLine(
"
- Main releases gM2
"
);
gM2.ReleaseMutex( );
//
线程t1,t4结束条件满足
//
等待所有四个线程结束
WaitHandle.WaitAll(evs);
Console.WriteLine(
"
Mutex Sample
"
);
Console.ReadLine();
}
public
void
t1Start( )
{
Console.WriteLine(
"
t1Start started, Mutex.WaitAll(Mutex[])
"
);
Mutex[] gMs
=
new
Mutex[
2
];
gMs[
0
]
=
gM1;
//
创建一个Mutex数组作为Mutex.WaitAll()方法的参数
gMs[
1
]
=
gM2;
Mutex.WaitAll(gMs);
//
等待gM1和gM2都被释放
Thread.Sleep(
2000
);
Console.WriteLine(
"
t1Start finished, Mutex.WaitAll(Mutex[]) satisfied
"
);
Event1.Set( );
//
线程结束,将Event1设置为有信号状态
}
public
void
t2Start( )
{
Console.WriteLine(
"
t2Start started, gM1.WaitOne( )
"
);
gM1.WaitOne( );
//
等待gM1的释放
Console.WriteLine(
"
t2Start finished, gM1.WaitOne( ) satisfied
"
);
Event2.Set( );
//
线程结束,将Event2设置为有信号状态
}
public
void
t3Start( )
{
Console.WriteLine(
"
t3Start started, Mutex.WaitAny(Mutex[])
"
);
Mutex[] gMs
=
new
Mutex[
2
];
gMs[
0
]
=
gM1;
//
创建一个Mutex数组作为Mutex.WaitAny()方法的参数
gMs[
1
]
=
gM2;
Mutex.WaitAny(gMs);
//
等待数组中任意一个Mutex对象被释放
Console.WriteLine(
"
t3Start finished, Mutex.WaitAny(Mutex[])
"
);
Event3.Set( );
//
线程结束,将Event3设置为有信号状态
}
public
void
t4Start( )
{
Console.WriteLine(
"
t4Start started, gM2.WaitOne( )
"
);
gM2.WaitOne( );
//
等待gM2被释放
Console.WriteLine(
"
t4Start finished, gM2.WaitOne( )
"
);
Event4.Set( );
//
线程结束,将Event4设置为有信号状态
}
}
}
程序的输出结果:
Mutex Sample
-
Main Owns gM1 and gM2
t1Start started, Mutex.WaitAll(Mutex[])
t2Start started, gM1.WaitOne( )
t3Start started, Mutex.WaitAny(Mutex[])
t4Start started, gM2.WaitOne( )
-
Main releases gM1
t2Start finished, gM1.WaitOne( ) satisfied
t3Start finished, Mutex.WaitAny(Mutex[])
-
Main releases gM2
t1Start finished, Mutex.WaitAll(Mutex[]) satisfied
t4Start finished, gM2.WaitOne( )
Mutex Sample
从执行结果可以很清楚地看到,线程t2,t3的运行是以gM1的释放为条件的,而t4在gM2释放后开始执行,t1则在gM1和gM2都被释放了之后才执行。Main()函数最后,使用WaitHandle等待所有的AutoResetEvent对象的信号,这些对象的信号代表相应线程的结束。
总结:Mutex.WaitAll(),Mutex.WaitOne(),gM.WaitOne(),gM.ReleaseMutex(),Hanler.WaitAll(evs)
注意:此段代码是有错误的,在每个等待Mutex的线程的代码段都有gM1.WaitOne()等语句但是缺少了释放互斥体的gM1.ReleaseMutex()语句,所以跑起来会抛异常!