代码改变世界

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

2011-09-19 14:09  Shawn.Cheng  阅读(3079)  评论(0编辑  收藏  举报

 


首先,StreamWriter线程安全么?

 

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

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

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

 


 

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

这是线程--0的内容

这是线程--1的内容

这是线程--2的内容

这是线程--5的内容

这是线程--3的内容

这是线程--4的内容

这是线程--6的内容

这是线程--7的内容

这是线程--8的内容

这是线程--9的内容

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

 

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

 

 


 

正常分析理解,

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方法,出现异常,则说明非线程安全可没有异常,说明线程安全。

代码如下

 


 

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

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

这是线程--0的内容

这是线程--1的内容

这是线程--2的内容

这是线程--3的内容

这是线程--4的内容

这是线程--5的内容

这是线程--6的内容

这是线程--7的内容

这是线程--8的内容

这是线程--9的内容

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

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

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

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

代码如下:


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


从堆栈跟踪来看:

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

这里是日志内容一部分



 

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

日志文件结束

 


 

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

 

如果我对其加锁呢?

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

代码如下

 

正常运行。查看日志结果


 

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

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

但是,加锁后,这样的线程还有意义么

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

我们该怎么办?

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

但是还有很多问题没解决,还有很多不清楚的地方,不是么?OK,在下一篇文章中,我们将继续对C#中的线程做进一步探究。