美文网首页
[转]C#中的多线程 - 基础知识(一)

[转]C#中的多线程 - 基础知识(一)

作者: 综合对接组 | 来源:发表于2016-07-10 23:18 被阅读0次

    原文:Threading in C#

    1 简介及概念

    ·C# 支持通过多线程并行执行代码,线程有其独立的执行路径,能够与其它线程同时执行。

    ·一个 C# 客户端程序(Console 命令行、WPF 以及 Windows Forms)开始于一个单线程,这个线程(也称为“主线程”)是由 CLR 和操作系统自动创建的,并且也可以再创建其它线程。以下是一个简单的使用多线程的例子:

    所有示例都假定已经引用了以下命名空间:

    >using System;
    >using System.Threading;
    
    class ThreadTest
    {
        static void Main()
        {
            Thread t = new Thread(WriteY);  // 创建新线程
            t.Start();                       // 启动新线程,执行WriteY()
    
            // 同时,在主线程做其它事情
            for (int i = 0; i < 1000; i++) Console.Write("x");
        }
    
        static void WriteY()
        {
            for (int i = 0; i < 1000; i++) Console.Write("y");
        }
    }
    

    输出结果:

    xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy
    yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx
    ...
    

    主线程创建了一个新线程t来不断打印字母 “ y “,与此同时,主线程在不停打印字母 “ x “。



    线程一旦启动,线程的IsAlive属性值就会为true,直到线程结束。当传递给Thread的构造方法的委托执行完成时,线程就会结束。一旦结束,该线程不能再重新启动。

    CLR 为每个线程分配各自独立的栈空间,因此局部变量是独立的。在下面的例子中,我们定义一个拥有局部变量的方法,然后在主线程和新创建的线程中同时执行该方法。

    static void Main()
    {
        new Thread(Go).Start();      // 在新线程执行Go()
        Go();                         // 在主线程执行Go()
    }
    
    static void Go()
    {
        // 定义和使用局部变量 - 'cycles'
        for (int cycles = 0; cycles < 5; cycles++) Console.Write('?');
    }
    

    输出结果:??????????

    变量cycles的副本是分别在各自的栈中创建的,因此才会输出 10 个问号。

    线程可以通过对同一对象的引用来共享数据。例如:

    class ThreadTest
    {
        bool done;
    
        static void Main()
        {
            ThreadTest tt = new ThreadTest();   // 创建一个公共的实例
            new Thread(tt.Go).Start();
            tt.Go();
        }
    
        // 注意: Go现在是一个实例方法
        void Go()
        {
            if (!done) { done = true; Console.WriteLine("Done"); }
        }
    }
    

    由于两个线程是调用了同一个的ThreadTest实例上的Go(),它们共享了done字段,因此输出结果是一次 “ Done “,而不是两次。

    输出结果:Done

    静态字段提供了另一种在线程间共享数据的方式,以下是一个静态的done字段的例子:

    class ThreadTest
    {
        static bool done;    // 静态字段在所有线程中共享
    
        static void Main()
        {
            new Thread(Go).Start();
            Go();
        }
    
        static void Go()
        {
            if (!done) { done = true; Console.WriteLine("Done"); }
        }
    }
    

    以上两个例子引出了一个关键概念线程安全(thread safety)。上述两个例子的输出实际上是不确定的:” Done “ 有可能会被打印两次。如果在Go
    方法里调换指令的顺序,” Done “ 被打印两次的几率会大幅提高:

    static void Go()
    {
        if (!done) { Console.WriteLine("Done"); done = true; }
    }
    

    输出结果:

    Done
    Done(很可能!)
    

    这个问题是因为一个线程对if中的语句估值的时候,另一个线程正在执行WriteLine语句,这时done还没有被设置为true。

    修复这个问题需要在读写公共字段时,获得一个排它锁(互斥锁,exclusive lock )。C# 提供了lock来达到这个目的:

    class ThreadSafe
    {
        static bool done;
        static readonly object locker = new object();
    
        static void Main()
        {
            new Thread(Go).Start();
            Go();
        }
    
        static void Go()
        {
            lock (locker)
            {
                if (!done) { Console.WriteLine("Done"); done = true; }
            }
        }
    }
    

    当两个线程同时争夺一个锁的时候(例子中的locker),一个线程等待,或者说阻塞,直到锁变为可用。这样就确保了在同一时刻只有一个线程能进入临界区(critical section,不允许并发执行的代码),所以 “ Done “ 只被打印了一次。像这种用来避免在多线程下的不确定性的方式被称为线程安全(thread-safe)。

    在线程间共享数据是造成多线程复杂、难以定位的错误的主要原因。尽管这通常是必须的,但应该尽可能保持简单。

    一个线程被阻塞时,不会消耗 CPU 资源。


    1.1 Join 和 Sleep

    可以通过调用Join方法来等待另一个线程结束,例如:

    static void Main()
    {
        Thread t = new Thread(Go);
        t.Start();
        t.Join();
        Console.WriteLine("Thread t has ended!");
    }
    
    static void Go()
    {
        for (int i = 0; i < 1000; i++) Console.Write("y");
    }
    

    输出 “ y “ 1,000 次之后,紧接着会输出 “ Thread t has ended! “。当调用Join时可以使用一个超时参数,以毫秒或是TimeSpan形式。如果线程正常结束则返回true,如果超时则返回false。

    Thread.Sleep会将当前的线程阻塞一段时间:

    Thread.Sleep (TimeSpan.FromHours (1));  // 阻塞 1小时
    Thread.Sleep (500);                     // 阻塞 500 毫秒
    

    当使用Sleep或Join等待时,线程是阻塞(blocked)状态,因此不会消耗 CPU 资源。

    Thread.Sleep(0)会立即释放当前的时间片,将 CPU 资源出让给其它线程。Framework 4.0 新的Thread.Yield()方法与其相同,除了它只会出让给运行在相同处理器核心上的其它线程。

    Sleep(0)和Yield在调整代码性能时偶尔有用,它也是一个很好的诊断工具,可以用于找出线程安全(thread safety)的问题。如果在你代码的任意位置插入Thread.Yield()会影响到程序,基本可以确定存在 bug。

    1.2 线程是如何工作的

    线程在内部由一个线程调度器(thread scheduler)管理,一般 CLR 会把这个任务交给操作系统完成。线程调度器确保所有活动的线程能够分配到适当的执行时间,并且保证那些处于等待或阻塞状态(例如,等待排它锁或者用户输入)的线程不消耗CPU时间。

    在单核计算机上,线程调度器会进行时间切片(time-slicing),快速的在活动线程中切换执行。在 Windows 操作系统上,一个时间片通常在十几毫秒(译者注:默认 15.625ms),远大于 CPU 在线程间进行上下文切换的开销(通常在几微秒区间)。

    在多核计算机上,多线程的实现是混合了时间切片和真实的并发,不同的线程同时运行在不同的 CPU 核心上。几乎可以肯定仍然会使用到时间切片,因为操作系统除了要调度其它的应用,还需要调度自身的线程。

    线程的执行由于外部因素(比如时间切片)被中断称为被抢占(preempted)。在大多数情况下,线程无法控制其在何时及在什么代码处被抢占。

    1.3 线程 vs 进程

    好比多个进程并行在计算机上执行,多个线程是在一个进程中并行执行。进程是完全隔离的,而线程是在一定程度上隔离。一般的,线程与运行在相同程序中的其它线程共享堆内存。这就是线程为何有用的部分原因,一个线程可以在后台获取数据,而另一个线程可以同时显示已获取到的数据。

    1.4线程的使用与误用

    多线程有许多用处,下面是通常的应用场景:

    维持用户界面的响应

    使用工作线程并行运行时间消耗大的任务,这样主UI线程就仍然可以响应键盘、鼠标的事件。

    有效利用 CPU

    多线程在一个线程等待其它计算机或硬件设备响应时非常有用。当一个线程在执行任务时被阻塞,其它线程就可以利用这个空闲出来的CPU核心。

    并行计算

    在多核心或多处理器的计算机上,计算密集型的代码如果通过分治策略(divide-and-conquer,见第 5 部分)将工作量分摊到多个线程,就可以提高计算速度。

    推测执行(speculative execution)

    在多核心的计算机上,有时可以通过推测之后需要被执行的工作,提前执行它们来提高性能。LINQPad就使用了这个技术来加速新查询的创建。另一种方式就是可以多线程并行运行解决相同问题的不同算法,因为预先不知道哪个算法更好,这样做就可以尽早获得结果。

    允许同时处理请求

    在服务端,客户端请求可能同时到达,因此需要并行处理(如果你使用 ASP.NET、WCF、Web Services 或者 Remoting,.NET Framework 会自动创建线程)。这在客户端同样有用,例如处理 P2P 网络连接,或是处理来自用户的多个请求。

    如果使用了 ASP.NET 和 WCF 之类的技术,可能不会注意到多线程被使用,除非是访问共享数据时(比如通过静态字段共享数据)。如果没有正确的加锁,就可能产生线程安全问题。
    多线程同样也会带来缺点,最大的问题是它提高了程序的复杂度。使用多个线程本身并不复杂,复杂的是线程间的交互(一般是通过共享数据)。无论线程间的交互是否有意为之,都会带来较长的开发周期,以及带来间歇的、难以重现的 bug。因此,最好保证线程间的交互尽量少,并坚持简单和已被证明的多线程交互设计。这篇文章主要就是关于如何处理这种复杂的问题,如果能够移除线程间交互,那会轻松许多。

    一个好的策略是把多线程逻辑使用可重用的类封装,以便于独立的检验和测试。.NET Framework 提供了许多高层的线程构造,之后会讲到。

    当频繁地调度和切换线程时(并且如果活动线程数量大于 CPU 核心数),多线程会增加资源和 CPU 的开销,线程的创建和销毁也会增加开销。多线程并不总是能提升程序的运行速度,如果使用不当,反而可能降低速度。 例如,当需要进行大量的磁盘 I/O 时,几个工作线程顺序执行可能会比 10 个线程同时执行要快。(在使用Wait和Pulse进行同步中,将会描述如何实现 生产者 / 消费者队列,它提供了上述功能。)

    参考文献:

    http://www.codeproject.com/Articles/98346/Microsecond-and-Millisecond-NET-Timer
    http://www.codeproject.com/Articles/571289/Obtaining-Microsecond-Precision-in-NET
    http://www.pinvoke.net/default.aspx/winmm/timeSetEvent.html
    http://www.geisswerks.com/ryan/FAQS/timing.html
    http://blog.gkarch.com/topic/threading.html
    http://omeg.pl/blog/2011/11/on-winapi-timers-and-their-resolution/
    https://randomascii.wordpress.com/2013/07/08/windows-timer-resolution-megawatts-wasted/
    http://www.windowstimestamp.com/description

    相关文章

      网友评论

          本文标题:[转]C#中的多线程 - 基础知识(一)

          本文链接:https://www.haomeiwen.com/subject/fowdjttx.html