您的位置:首页 > 编程语言 > C#

由StreamWriter.WriteLine 引发对C#多线程的深入思考(一)

2016-02-26 13:30 1016 查看
http://blog.csdn.net/nndtdx/article/details/6789810

首先,StreamWriter线程安全么?

答:StreamWriter 的构造以及StreamWriter.WriteLine(string)都是非线程安全的

我们封装两个写日志的方法。

底层都是由StreamWriter.writeline来实现.一个加锁,一个不加锁。将加锁的那个命名为safewritelog,另一个命名为unsafeWritelog.然后利用两个循环。不停的分别创建个线程,去写日志。测试看哪个会出现写异常。代码如下:

[csharp] view
plain copy

print?

namespace ThreadWriteLog

{

class Program

{

private static object ob = "哟内容!!";

static void Main(string[] args)

{

for (int i = 0; i < 10; i++)

{

Thread wrtieThread = new Thread(SafyWriteLog);

wrtieThread.Name = "线程--" + i;

string content = "这是" + wrtieThread.Name + "的内容Y";

wrtieThread.Start(content);

}

for (int i = 0; i < 10; i++)

{

Thread wrtieThread = new Thread(UnSafyWriteLog);

wrtieThread.Name = "线程¨¬--" + i;

string content = "这是" + wrtieThread.Name + "的内容Y";

wrtieThread.Start(content);

}

Console.WriteLine("结束");

Console.Read();

}

public static void SafyWriteLog(object content)

{

string path = @"C:\SafeLog.txt";

lock (ob)

{

StreamWriter sw = File.AppendText(path);

sw.WriteLine(content.ToString());

sw.Close();

}

}

public static void UnSafyWriteLog(object content)

{

string path = @"C:\UnSafeLog.txt";

StreamWriter sw = File.AppendText(path);

sw.WriteLine(content.ToString());

sw.Close();

}

}

}

运行后,第一个for循环顺利结束,文件中显示 0-9进程没有问题。

这是线程--0的内容

这是线程--1的内容

这是线程--2的内容

这是线程--5的内容

这是线程--3的内容

这是线程--4的内容

这是线程--6的内容

这是线程--7的内容

这是线程--8的内容

这是线程--9的内容

也符合线程的概念,随着系统的随机调度而运行。

而第二个for循环没有正常完成,抛出异常

[csharp] view
plain copy

print?

未处理的异常: 未处理的异常: 未处理的异常: 未处理的异常: 未处理的异常: 未处理的异

常: System.IO.IOException: 文件“C:\UnSafeLog.txt”正由另一进程使用,因此

该进程无法访问该文件。

在 System.IO.__Error.WinIOError(Int32 errorCode, StringmaybeFullPath)

在 System.IO.FileStream.Init(String path, FileMode mode,FileAccess access, I

nt32rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions o

ptions,SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy)

在 System.IO.FileStream..ctor(String path, FileMode mode,FileAccess access,

FileShareshare, Int32 bufferSize, FileOptions options)

在 System.IO.StreamWriter.CreateFile(String path, Booleanappend)

在 System.IO.StreamWriter..ctor(String path, Booleanappend, Encoding encodin

g, Int32bufferSize)

在 System.IO.StreamWriter..ctor(String path, Booleanappend)

在 System.IO.File.AppendText(Stringpath)

在 caTestProj.Program.UnSafyWriteLog(Object content) 位置 F:\ASP.NET\MyCode\c

aTestProj\caTestProj\Program.cs:行号 51

在 System.Threading.ThreadHelper.ThreadStart_Context(Objectstate)

在 System.Threading.ExecutionContext.Run(ExecutionContextexecutionContext, C

ontextCallbackcallback, Object state)

在 System.Threading.ThreadHelper.ThreadStart(Objectobj)System.IO.IOException

: 文件“C:\UnSafeLog.txt”正由另一进程使用,因此该进程无法访问该文件。

在 System.IO.__Error.WinIOError(Int32 errorCode, StringmaybeFullPath)

正常分析理解,

StreamWriter.WriteLine方法本身没有线程同步方法,多线程写日志时(注意这里,我们不同的线程使用的是不同的StreamWriter),多个线程同时访问文件,出现异常。

但是 确实是WriteLine出错了么?

从堆栈跟踪来看,错误出现在Thread线程回调UnSafyWriteLog方法出现错误,即执行AppendText时出错。

在到里边看,构造StreamWriter对象出错-à FileStream对象构造出错-- 调用FileStream.Init出错,最后到了win32函数winIoError.也就是构造FileStream对象时出错。我们很明白肯定一个共享写的问题了。

那么,可以断定,问题在于,调用File.APpendTest时,会构造StreamWriter,而这个StreamWriter是独占式的

由于该文件已被另一个线程访问,所以StreamWriter构造出现异常,

而并不是在StreamWriter.WriteLine上出的错误。

对于SafeWritelog,我们对Streamwriter的构造以及write都加了锁,也就是说,每次构造streamWriter的时候,还是Writeline的时候,我们都保证了有唯一的对象对磁盘文件(或者缓存)进行操作。

那如果我用同一个StreamWriter呢?也就是说,Writeline本身是不是多线程安全的?

现在我们使用同一个Streamwriter,测试writeline的线程安全特性。

也就是说,如果同时有两个线程同时调用Wtriteline方法,出现异常,则说明非线程安全可没有异常,说明线程安全。

代码如下

[csharp] view
plain copy

print?

class Program

{

static object ob=new object();

static string path = @"C:\UnSafeLog.txt";

private static StreamWriter sw2;

static void Main(string[] args)

{

for (int i = 0; i < 10; i++)

{

Thread wrtieThread = new Thread(SafyWriteLog);

wrtieThread.Name = "线程--" + i;

string content = "这是" + wrtieThread.Name + "的内容";

wrtieThread.Start(content);

}

sw2 = File.AppendText(path);

for (int i = 0; i < 10; i++)

{

Thread wrtieThread = new Thread(UnSafyWriteLog);

wrtieThread.Name = "线程--" + i;

string content = "这是" + wrtieThread.Name + "的内容Y";

wrtieThread.Start(content);

}

sw2.Close();

Console.WriteLine("结束");

Console.Read();

}

public static void SafyWriteLog(object content)

{

string path = @"C:\SafeLog.txt";

lock (ob)

{

StreamWriter sw = File.AppendText(path);

sw.WriteLine(content.ToString());

sw.Close();

}

}

public static void UnSafyWriteLog(object content)

{

sw2.WriteLine(content.ToString());

}

}

运行后,貌似没出现什么问题。两个for循环都正常执行完毕了。

而且日志文件中的记录也是按顺序来的,没有出现日志文字错乱现象。

这是线程--0的内容

这是线程--1的内容

这是线程--2的内容

这是线程--3的内容

这是线程--4的内容

这是线程--5的内容

这是线程--6的内容

这是线程--7的内容

这是线程--8的内容

这是线程--9的内容

但真的是这样的么?构造是非线程安全的,而writeline是线程安全的??不可能的。

仔细考虑,只有在某一个线程阻塞在WriteLine的时候,另一个线程也访问该方法的时候,才会出现多线程写的情况。

那么,ok,我们加大一次文字的写入量,使其阻塞在WriteLine这里

现在,我们构造更加简单的场景,创建两个线程,一次写入大量日志,使用同一个StreamWriter对象。如果writeline方法是非线程安全的,那么肯定会出现异常。

代码如下:

[csharp] view
plain copy

print?

class Program

{

public static StreamWriter sw = new StreamWriter("C:\\threadlog.txt", true);

private static string path = @"C:\Users\cjt.IT\Desktop\20110807\S20110807031902.info";//大文件内容10M级

private Thread t1;

private Thread t2;

static void Main(string[] args)

{

Program p=new Program();

p.Test();

}

public void Test()

{

StreamReader sr = new StreamReader(path);

string content = sr.ReadToEnd();

BeginWrite1(content);

BeginWrite2(content);

sr.Close();

//t1.Join();

//sw.Close();

}

private void BeginWrite1(string content)

{

t1 = new Thread(WriteLog);

t1.Start("---------线程1Begin--------" + Environment.NewLine + content + "---------线程1 End--------" + Environment.NewLine);

}

private void BeginWrite2(string content)

{

t2 = new Thread(WriteLog);

t2.Start("---------线程2Begin--------" + Environment.NewLine + content + "---------线程2End--------" + Environment.NewLine);

}

private void WriteLog(object content)

{

sw.WriteLine(content.ToString());//内容过多线程会阻塞在该处

sw.Flush();//这a样能够保证缓o冲区内的数据全部写磁盘¨¬

}

}

运行之后,立即出现异常。

[csharp] view
plain copy

print?

未处理的异常: System.IndexOutOfRangeException: 在复制内存时检测到可能的 I/O 争

用条件。默认情况下,I/O 包不是线程安全的。在多线程应用程序中,必须以线程安全方式

(如 TextReader 或 TextWriter 的 Synchronized 方法返回的线程安全包装)访问流。这也

适用于 StreamWriter 和 StreamReader 这样的类。

在 System.Buffer.InternalBlockCopy(Array src, Int32 srcOffset, Array dst, Int

32 dstOffset, Int32 count)

在 System.IO.StreamWriter.Write(Char[] buffer, Int32 index, Int32 count)

在 System.IO.TextWriter.WriteLine(String value)

在 caTestProj.Program.WriteLog(Object content) 位置 F:\ASP.NET\MyCode\caTestP

roj\caTestProj\Program.cs:行号 51

在 System.Threading.ThreadHelper.ThreadStart_Context(Object state)

在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, C

ontextCallback callback, Object state)

在 System.Threading.ThreadHelper.ThreadStart(Object obj)

请按任意键继续. . .

从堆栈跟踪来看:

在调用TextWriter.WriteLine(Streamwriter继承于此)时出错,系统要进行内存拷贝,出现I/O争用。也就是说,线程不安全本质是由(至少该例子中是由I/o争用导致的)。两者都要将自己的内容拷贝到磁盘上,显然要出错。writeline方法缺少同步机制,抛出异常。

这里是日志内容一部分



可以看出来中间有个线程2begin,可以肯定,在这里,线程1暂时阻塞,然后线程2开始写,也就是说从这里开始,日志文件就开始混乱了,因为1已经阻塞了,这里没有出现异常。

日志文件结束



中间并没有出现线程2end的标记,说明,线程2中间也阻塞了。这是启动了线程1,造成中间这个界限丢失了。也有可能从线程2begin这一段,线程1和线程2就是混着写的。直到他们两个突然决定同时写,系统出现I/O争用错误,抛出异常。

如果我对其加锁呢?

显然我们如果控制了同时writelien的只有一个线程。那么写日志就不会有问题。如果一个阻塞在writeline处,那么另一个就会阻塞在lock外。等待阻塞线程释放锁后,进入该代码段。那么可以断定,日志文件也是有逻辑的。

代码如下

[csharp] view
plain copy

print?

class Program

{

public static StreamWriter sw = new StreamWriter("C:\\threadlog.txt", true);

private static string path = @"C:\Users\cjt.IT\Desktop\S20110908002112.info";//大䨮文?件t内¨²容¨Y10M级?

private Thread t1;

private Thread t2;

static void Main(string[] args)

{

Program p=new Program();

p.Test();

}

public void Test()

{

StreamReader sr = new StreamReader(path);

string content = sr.ReadToEnd();

BeginWrite1(content);

BeginWrite2(content);

sr.Close();

//t1.Join();

//sw.Close();

}

private void BeginWrite1(string content)

{

t1 = new Thread(WriteLog);

t1.Start("---------线?程¨¬1Begin--------" + Environment.NewLine + content + "---------线?程¨¬1 End--------" + Environment.NewLine);

}

private void BeginWrite2(string content)

{

t2 = new Thread(WriteLog);

t2.Start("---------线?程¨¬2Begin--------" + Environment.NewLine + content + "---------线?程¨¬2End--------" + Environment.NewLine);

}

private void WriteLog(object content)

{

lock (path)//如果不加同步 由于 writeLine会发生阻塞.所´以当À另外一个?线?程¨¬也°2到Ì?WriteLine处ä|的Ì?时º¡À候¨°,ê?会¨¢发¤¡é生¦¨² 同ª?时º¡À写¡ä 异°¨¬常¡ê

{

sw.WriteLine(content.ToString());//内¨²容¨Y过y多¨¤,ê?线?程¨¬会¨¢阻Á¨¨塞¨?在¨²该?处ä|

sw.Flush();//这a样¨´能¨¹够?保À¡ê证¡è 缓o冲?区?内¨²的Ì?数ºy据Y全¨?部?写¡ä入¨?磁ä?盘¨¬

}

}

}

正常运行。查看日志结果



可以看到可以认为是两个线程顺序执行了。没有出现混乱的情况。

(你可能已经注意到,上边的代码有两个问题,1 sw没有关闭 2 由于线程是前台线程,main结束后,该线程并没有结束,也就是说,我们不能单从窗口显示”结束”,已到达main结尾来断定写日志完成,我们需要等待一段时间,认为写日志完成了,再去查看日志)

但是,加锁后,这样的线程还有意义么
线程的目的是使得程序并发进行。也就是说,如果我们要想使得写日志加快,可以采用多线程写日志。但是,按照上边的情况来写日志,根本没有起到线程的优势。因为我们在一个线程阻塞的时候,并没有办法启动另一个线程!!而是等到这个阻塞完毕后,再调用另一个,这样和顺序执行就没有任何差别了。我们要的是及时唤醒另一个写进程(我们知道,这样带来的后果就是使得日志开始错乱,分不清是第一个写的还是第二个写的,甚至没有正常的语法!)

我们该怎么办?

好吧,这一篇已经够长了,大家估计已经没有耐性读下去了。。。

但是还有很多问题没解决,还有很多不清楚的地方,不是么?OK,在下一篇文章中,我们将继续对C#中的线程做进一步探究。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: