zoukankan      html  css  js  c++  java
  • 学习TPL(二)

    回顾

        前面一篇简单的介绍了TPL,以及一个最简单的例子。这一篇,来讨论一下线程安全的问题。

    TPL不需要考虑线程安全?

        好吧,我不知道为什么会有人提出这样的想法,首先要确定的是在MS的开发人员是人,不是神,不可能让一个类库聪明到这样的程度。要是真的有这么聪明的话,估计我们都可以转行做需求分析了,更不不需要写代码的人了。

        如何证明考虑线程安全是必要的哪?来一个简单的示例就可以了:

    const int count = 100000;
    int value = 0;
    Action action = ()=>
    {
    for (int i = 0; i < count; i++)
    value++;
    };
    Parallel.Invoke(action, action, action, action);
    Console.WriteLine(value);
    Contract.Assert(value == count * 4, "Not thread safe at all!");

        由于写这篇文章的时候用的是4核心的机器,所以启动了4个action,做++操作,如果TPL自动保证对value的访问是线程安全的,那么value的结果必然是count*4,这里,直接用Contract来验证这一点,运行起来看到什么?

    image

        这个value只有254726,明显比预期的值小了近一半。是Action偷懒了吗?好吧,做一个小修改:

    const int count = 100000;
    int value = 0;
    Action action = ()=>
    {
    for (int i = 0; i < count; i++)
    Interlocked.Increment(ref value);
    };
    Parallel.Invoke(action, action, action, action);
    Console.WriteLine(value);
    Contract.Assert(value == count * 4, "Not thread safe at all!");

        再看看运行结果:

    image

        仅仅是使用一个线程安全的Interlocked.Increment方法代替++,就可以得到一个正确的结果,这足以证明,TPL并不会职能的帮助我们完成线程安全的工作,TPL仅仅负责安排任务并执行,并不关心这些任务是否用到了那些临界资源。

        所以,不要以为使用了TPL就不需要考虑线程安全了,恰恰相反,TPL是基于每个Task的实现本身是线程安全的基础上的。

    那TPL能干了些什么?

        看到这里,一个疑问必定会出来,TPL不管线程安全,那它能为我们带来什么哪?

        看看上面的例子,线程安全的Interlocked.Increment方法固然很好,但是,这个方法肯定没有直接++来的快,那么如果每个线程在运行中都自己保留一份本地value,到最后执行完成时,将所有的本地value(线程安全的)合并到全局value岂不是代价更小?(这里认为加锁的消耗远大于委托的消耗)

        于是,改造原来的代码,增加一个运行时间:

    const int count = 100000;
    int value = 0;
    Stopwatch sw = Stopwatch.StartNew();
    Action action = () =>
    {
    for (int i = 0; i < count; i++)
    Interlocked.Increment(ref value);
    };
    Parallel.Invoke(action, action, action, action);
    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds + "ms");
    Console.WriteLine(value);
    Contract.Assert(value == count * 4, "Not thread safe at all!");

        然后再加入一个刚才说的保留线程本地value的实现:

    const int count = 100000;
    int value = 0;
    Stopwatch sw = Stopwatch.StartNew();
    Parallel.For(0, count * 4, () => 0,
    (i, state, local) => local + 1,
    local => Interlocked.Add(ref value, local));
    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds + "ms");
    Console.WriteLine(value);
    Contract.Assert(value == count * 4, "Not thread safe at all!");

        这里的local变量,都存放着一个线程本地的value,因为是每个线程有自己的值,所以直接使用最简单的+1操作,而在将local添加到全局的value中时,全局的value会被多个线程操作,所以需要使用线程安全的Interlocked.Add方法。

        看上去复杂了不少,而且对委托的Invoke操作也增加了100000倍,这段程序真的能比上面的跑得更快吗?

        说真的,自己心里也没底。。。还是看看实际的结果吧:

    image

        28ms vs. 13ms,快了1倍,是不是很惊讶,不妨增加count到1000000看看结果:

    image

        差距被拉大了,接近6倍,count再加到10000000看看:

    image

        稳定在6倍附近了,所以千万不要小看同步的代价,即使最快的Interlocked.Increment方法也至少是调用Delegate的6倍。

    后话

        当然用Action也可以实现本地缓存:

    Action action = () =>
    {
    int local = 0;
    for (int i = 0; i < count; i++)
    local++;
    Interlocked.Add(ref value, local);
    };
    Parallel.Invoke(action, action, action, action);

        并且这样的运行的运行速度是最快的,既没有很多的同步,也没有很多的委托调用。

  • 相关阅读:
    [转]xshell实现端口转发
    Windows下gvim配置
    Linux环境下段错误的产生原因及调试方法小结
    elasticsearch的服务器响应异常及应对策略
    scp不可用:WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED
    windows下python安装pyquery
    python实现简单爬虫功能
    关于Elasticsearch单个索引文档最大数量问题
    pthread_mutex_lock
    一道模拟题:改进的Joseph环
  • 原文地址:https://www.cnblogs.com/vwxyzh/p/1714665.html
Copyright © 2011-2022 走看看