在前一面篇文章[设计安全的多线程应用程序(线程安全)]中,我们讲了,什么是线程安全,列举一些常见的线程安全和非线程安全的情况。还没对线程安全了解的同学请点上面的链接。现在我们来看线程不安全的本质是什么。
我们来想想在单线程的情况下安全,为什么在多线程的情况下是不安全的呢?无非就是因为多线程是并行执行,当然并行执行本身是没有错的。如果这些并行跑起来的线程,出现下列情况时可能会导致线程不安全。第一,争抢独占的资源如同时写一个文件,对独占的资源的排它锁。第二,程序中的全局对象,如类中对静态成员,被多个线程同时更新(修改或者删除),会产生脏的数据或者是其它线程得不到正确的数据的后果;同一个类实例的非静态成员,被此实例中的函数使用(其中有修改实例成员的功能),此时两个以上线程同时使用时,就可能使类成员的状态对同一个线程的使用的不一致性。
由此,可以看出,线程安全的本质就是,要保护全局对象,在使用时只能被一个线程使用(包括读,写,删除)(注意,对全局对象只限于只读,那么是线程安全的)。解决的方法无非就是两种,第一,把这个对象或者资源锁住,即等我用完了,其它线程才能再能,否则其它线程等待,这也是最常见的方式。第二,给每个线程分配属于只属于自己的"全局对象"(这样听起来好像有点别扭)。
要注意的一点是,很多语句的类库中的类,都没在实现线程安全,这是因为基于性能的考虑,这样的情况下,就让我们程序员自己来决定实现线程安全。
下面这两种设计线程安全类的例子:
第一,用lock语句(在c#中)
1,锁住同一个实例中的全局资源(同一实例中非静态成员)
{
System.Object lockThis = new System.Object();
lock(lockThis)
{
// Access thread-sensitive resources.
}
}
2,锁住类中静态成员
static System.Object lockThis = new System.Object();
public void Function()
{
lock(lockThis)
{
CstrBuild.Append("test");
}
}
第二,使用给每一个线程分配自己的“全局对象”的方法,实现一个带有缓存的写文件功能的线程安全的类(避免每条数据写一次,造成IO瓶颈)。
它的原则是给每个线程分配自己的“全局对象”。
实现功能:
当每个线程的StringBuilder对象,到达一定长度 const int CBufferLenght = 10000*38; 后才写入文件。
步骤是:
1, 声明一个分局对象集合,我用了Dictionary对象,key是线程id,value就是它的“全局对象”
2,需要注意的是,当第一次将数据加入到全局集合对象Dictionary时,需要锁定Dictionary对象
{
CLBufferOfEThread[Thread.CurrentThread.ManagedThreadId] = new StringBuilder(pstrContent);
}
3, 给Dictionary对象赋值时,无需使用lock(因为它是线程独占的全局对象,不会冲突,我给每个线程id分配了自己的“全局对象”,即集合中的某个元素)
4,在虚构函数处理还未写入文件的数据
{
foreach (StringBuilder valus in CLBufferOfEThread.Values)
{
if (valus.Length > 0 && CLimpIDfileName.Length != 0)
{
using (StreamWriter logWrite = new StreamWriter(CLimpIDfileName, true, Encoding.Default))
{
logWrite.WriteLine(valus.ToString());
}
valus.Remove(0, valus.Length);
}
}
}
下面就是具体的代码,
调用代码:
/// <summary>
/// Writing content once a content was created
/// </summary>
/// <param name="pstrLogName">limpid file name</param>
/// <param name="pstrContent"></param>
public void WriteLimpIDLog(string pstrContent)
{
try
{
StreamWriteWithBuffer.WriteLine("Stats/" + CLimpIDfileName, pstrContent);
}
catch (Exception ex)
{
Console.WriteLine("Exception of log file:" + ex.Message);
}
}
类实现:
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Threading;
namespace Project.Common
{
/// <summary>
/// the class for writing something into a file,it can have a buffer to store the data which need to write before write to file really.
/// </summary>
class StreamWriteWithBuffer : IDisposable
{
string CLimpIDfileName = string.Empty;
const int CBufferLenght = 10000*38;
//static StringBuilder CBuffer = new StringBuilder();
static Dictionary<int, StringBuilder> CLBufferOfEThread = new Dictionary<int, StringBuilder>();
public StreamWriteWithBuffer(string pfileName)
{
CLimpIDfileName = pfileName;
}
public StreamWriteWithBuffer()
{
}
~StreamWriteWithBuffer()
{
foreach (StringBuilder valus in CLBufferOfEThread.Values)
{
if (valus.Length > 0 && CLimpIDfileName.Length != 0)
{
using (StreamWriter logWrite = new StreamWriter(CLimpIDfileName, true, Encoding.Default))
{
logWrite.WriteLine(valus.ToString());
}
valus.Remove(0, valus.Length);
}
}
}
private static Object lockWrite = new Object();
private static Object lockBuffer = new Object();
public void WriteLine(string pLimpIDfileName, string pstrContent){
try
{
if (CLBufferOfEThread.ContainsKey(Thread.CurrentThread.ManagedThreadId))
{
if (CLBufferOfEThread[Thread.CurrentThread.ManagedThreadId].Length < CBufferLenght)
{
CLBufferOfEThread[Thread.CurrentThread.ManagedThreadId].AppendLine(pstrContent);
CLimpIDfileName = pLimpIDfileName;
}
else
{
lock (lockWrite)
{
using (StreamWriter logWrite = new StreamWriter(pLimpIDfileName, true, Encoding.Default))
{
logWrite.WriteLine(CLBufferOfEThread[Thread.CurrentThread.ManagedThreadId].ToString());
}
}
CLBufferOfEThread[Thread.CurrentThread.ManagedThreadId].Remove(0, CLBufferOfEThread[Thread.CurrentThread.ManagedThreadId].Length);
}
}
else
{
lock (lockBuffer)
{
CLBufferOfEThread[Thread.CurrentThread.ManagedThreadId] = new StringBuilder(pstrContent);
}
}
}
catch (Exception ex)
{
Console.WriteLine("Exception of limpID log file:" + ex);
}
}
public void Dispose()
{
}
}
}
总之,线程安全除了在设计时要尽量避免以上的情况之外,还要反复的测试你的类,或者程序,才能更终实现线程安全类。