zoukankan      html  css  js  c++  java
  • 由StreamWriter.WriteLine 引发对C#多线程的深入思考(一)


    首先,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#中的线程做进一步探究。

  • 相关阅读:
    BZOJ 3506 机械排序臂 splay
    BZOJ 2843 LCT
    BZOJ 3669 魔法森林
    BZOJ 2049 LCT
    BZOJ 3223 文艺平衡树 splay
    BZOJ 1433 假期的宿舍 二分图匹配
    BZOJ 1051 受欢迎的牛 强连通块
    BZOJ 1503 郁闷的出纳员 treap
    BZOJ 1096 ZJOI2007 仓库设计 斜率优化dp
    BZOJ 1396: 识别子串( 后缀数组 + 线段树 )
  • 原文地址:https://www.cnblogs.com/linecheng/p/2210952.html
Copyright © 2011-2022 走看看