• C#中的线程 线程入门


    内容预告:

    • 线程入门(线程概念,创建线程)
    • 同步基础(同步本质,线程安全,线程中断,线程状态,同步上下文)
    • 使用线程(后台任务,线程池,读写锁,异步代理,定时器,本地存储)
    • 高级话题(非阻塞线程,扶起和恢复)

    概览:

    C#支持通过多线程并行地执行代码,一个线程是独立的执行个体,可以和其他线程同时运行。

    CLR和操作系统会给C#程序开启一个线程(主线程),可以被用来作为创建多线程的起点,例子:

    class ThreadTest {
    static void Main() {
    Thread t = new Thread (WriteY);
    t.Start(); // Run WriteY on the new thread
    while (true) Console.Write ("x"); // Write 'x' forever
    }
    static void WriteY() {
    while (true) Console.Write ("y"); // Write 'y' forever
    }
    }

    运行结果将是:

    xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy
    yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx
    xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
    yyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

    主线程创建了一个线程 t ,执行了重复输出y的操作,主要线程执行了重复输出x的操作。
    CLR给每个线程分配了单独的线程栈,所以本地变量是每个线程单独保存的,下面的例子,我们用一个本地变量定义一个函数,然后在main函数和新的线程里同时执行这个函数

    static void Main() {
    new Thread (Go).Start(); // Call Go() on a new thread
    Go(); // Call Go() on the main thread
    }
    static void Go() {
    // Declare and use a local variable - 'cycles'
    for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?');
    }

    执行结果:

    ??????????

    每个线程的内存栈里都创建了一个单独的变量cycle,所以输出是10个?
    如果是引用同一个对象的话,线程则共享这个数据:

    class ThreadTest {
    bool done;
    static void Main() {
    ThreadTest tt = new ThreadTest(); // Create a common instance
    new Thread (tt.Go).Start();
    tt.Go();
    }
    // Note that Go is now an instance method
    void Go() {
    if (!done) { done = true; Console.WriteLine ("Done"); }
    }
    }

    因为两个线程都调用Go(),它们共享done这个变量,所以done只输出一次:

    Done

    static变量提供一种不同的方式在线程中共享变量,这里是一个例子:

    class ThreadTest {
    static bool done; // Static fields are shared between all threads
    static void Main() {
    new Thread (Go).Start();
    Go();
    }
    static void Go() {
    if (!done) { done = true; Console.WriteLine ("Done"); }
    }
    }

    这里输出就不太确定了,看起来要输出两次done,其实不可能。我们交换一下Go的次序,

    class ThreadTest {
    static bool done; // Static fields are shared between all threads
    static void Main() {
    new Thread (Go).Start();
    Go();
    }
    static void Go() {
    if (!done) { Console.WriteLine ("Done"); done = true; }
    }
    }

    done就有可能输出两次。因为在一个线程计算if表达式然后执行Console.WriteLine ("Done");的时候,另一个线程可能有机会在done的值改变之前先输出done。
    其实在C#中可以用lock来达到这个目的:

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

    当两个线程同时竞争一个锁时,一个线程等待,或者说阻塞,直到锁空出来。这主要是保证同时只能有一个线程可以进入临界代码区域,"Done"只会被输出一次。
    代码是以这样的方式被保护的,来自于多线程上下文的不确定性,叫做线程安全。
    临时地暂停,或阻塞,是线程同步的基本功能。
    如果一个线程想要暂停,或者休眠一段时间,可以用:

    Thread.Sleep (TimeSpan.FromSeconds (30)); // 阻塞30秒

    一个线程可以通过调用Join等待另一个线程结束:

    Thread t = new Thread (Go); // 假设Go是静态函数。
    t.Start();
    Thread.Join (t); // 阻塞,只到线程t结束。

    线程如何工作:
    在内部,多线程是被线程调度器管理的,是CLR代替操作系统干的活。线程调度器要保证所有活跃线程合理分配执行时间,以及在等待中的线程(这些线程是不消耗CPU时间的)。
    在单核机器上,线程调度是以在活跃线程间快速切换时间片的方式工作的。就像就第一个例子,重复输出x或y的线程轮换得到时间片。在Windows XP下,一个时间片就是几十毫秒,这还是要比CPU在线程间切换能干更多事,一次线程切换也就几毫秒的事。
    在多核机器上,多线程的实现是结合了时间片轮换和并发,并发是不同的线程同时运行在不同的CPU上,因为机器要运行的线程数远远大于CPU的数量,所以还需要时间片切换。
    线程不能控制自己什么时候执行,完全由操作系统的时间片切换机制来控制。

    线程和进程:

    总是有面试官喜欢把线程和进程做比较,其实两者根本不是一个级别的东西。一个单独的应用程序内所有的线程都在逻辑或属于一个进程的。进程:一个运行应用程序的操作系统单元。线程与进程有些相似之处,比如:对于实例,进程和线程都是典型的时间片轮换的执行机制。关键的不同点在于进程间是相互独立的,而同一个应用程序里的线程间是共享堆内存的,这也是性能的用武之地:一个线程可以在后台运行,另一个线程可以显示得到的数据。

    什么时候应该用多线程:

    • 一个普通的多线程程序在后台运行耗时的任务时。主线程保持运行状态,工作线程干后台的活。在Windows Form程序里,如果主线程被长时间占用,键盘和鼠标的操作就不能处理了,然后程序就变成“无响应”了。所以,需要把耗时的任务放在后台运行,让主线程保证响应用户输入。
    • 在非UI程序中,比如Windows服务,多线程就特别有用了,当等待另一台机器(例如一个应用服务器,数据库服务器,客户端)的响应时,用一个工作线程来等待,让主线程保持畅通。
    • 多线程的另一个用处是在函数中有大量计算时,函数划成多个线程可以在多核的机器上执行更快(可以用Environment.ProcessorCount得到CPU核心数量)。
    • 一个C#程序可以通过两种方式成为多线程:显示地创建线程,或者使用.NET显示创建线程的功能(比如BackgroundWorker,线程池,定时器,远程服务器,WebSerivce或ASP.NET程序),在后面这些情况下,只能是多线程。单线程的web服务器肯定不行。在无状态的web服务器里,多线程是相当简单的。主要的问题是如果处理缓存数据的锁机制。

    什么时候不应该用多线程:

    多线程也有缺点,最大的问题是会让程序变得复杂,多线程本身并不复杂,复杂在于线程间的交互。能让开发周期变长,以及Bug变多。所以需要把多个线程间的交互设计的尽量简单,或者就别用多线程,除非你可以保证的很好。

    过多地在线程间切换和分配内存栈,也会带来CPU资源的消耗,通常,当硬盘IO很多时,只有一两个线程依次执行任务的程序性能要更好,而多个性能同时执行一个任务的性能不怎么样。后面会讨论生产者/消费者模型。


    创建和启动线程:

     可以用Thread类的构造函数创建线程,传递一个ThreadStart的代理作为参数,这个代理指向将要执行的函数,以下是这个代理的定义:

    public delegate void ThreadStart();

    执行Start()函数,线程即开始运行,在函数结束后线程会返回,下面是创建ThreadStart的C#语法:

    class ThreadTest {
    static void Main() {
    Thread t = new Thread (new ThreadStart (Go));
    t.Start(); // Run Go() on the new thread.
    Go(); // Simultaneously run Go() in the main thread.
    }
    static void Go() { Console.WriteLine ("hello!"); }

    线程t执行Go函数,同时主线程也调用Go,执行结果是:

    hello!
    hello!

    也可以用C#的语法糖:编译器会自动创建一个ThreadStart的代理。

    static void Main() {
    Thread t = new Thread (Go); // No need to explicitly use ThreadStart
    t.Start();
    ...
    }
    static void Go() { ... }

    还有更简单的匿名函数语法:

    static void Main() {
    Thread t = new Thread (delegate() { Console.WriteLine ("Hello!"); });
    t.Start();
    }

    给ThreadStart传递参数:这种形式只能传递一个参数

    public delegate void ParameterizedThreadStart (object obj);
    class ThreadTest {
    static void Main() {
    Thread t = new Thread (Go);
    t.Start (true); // == Go (true)
    Go (false);
    }
    static void Go (object upperCase) {
    bool upper = (bool) upperCase;
    Console.WriteLine (upper ? "HELLO!" : "hello!");
    }

    结果:

    hello!
    HELLO!

    如果用匿名函数方式:可以传递多个参数,且也不需要类型转换,

    static void Main() {
    Thread t = new Thread (delegate() { WriteText ("Hello"); });
    t.Start();
    }
    static void WriteText (string text) { Console.WriteLine (text); }

    还有一种传参的方式是传一个实例过去,而不是传一个静态函数:

    class ThreadTest {
    bool upper;
    static void Main() {
    ThreadTest instance1 = new ThreadTest();
    instance1.upper = true;
    Thread t = new Thread (instance1.Go);
    t.Start();
    ThreadTest instance2 = new ThreadTest();
    instance2.Go(); // Main thread – runs with upper=false
    }
    void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); }

    线程命名:线程有一个Name属性,在调试时很有用。

    class ThreadNaming {
    static void Main() {
    Thread.CurrentThread.Name = "main";
    Thread worker = new Thread (Go);
    worker.Name = "worker";
    worker.Start();
    Go();
    }
    static void Go() {
    Console.WriteLine ("Hello from " + Thread.CurrentThread.Name);
    }
    }

    输出:

    Hello from main
    Hello from worker

    前台线程和后台线程:

    默认情况下,线程都是前台线程,意味着任何一个前台线程正在运行,程序就是运行的。而后台线程在所有前台线程终止时也会立即终止。

    把线程从前台改为后台,线程在CPU调度器的优先级和状态是不会改变的。

    class PriorityTest {
    static void Main (string[] args) {
    Thread worker = new Thread (delegate() { Console.ReadLine(); });
    if (args.Length > 0) 
    worker.IsBackground = true; worker.Start(); } }

    如果这个程序执行时不带参数,worker线程默认是前台线程,并且会在ReadLine这一行等着用户输入。同时,主线程退出,但是程序会继续运行,因为ReadLine也是前台线程。如果传了一个参数给Main函数,worker线程的状态则被设置成后台状态,程序几乎会在主线程结束时立即退出--终于ReadLine。当后台线程以这种方式终止时,任何代码都不再执行了,这种代码是不推荐的,所以最好在程序退出前等待所有后台线程,可以用超时时间(Thread.Join)来做。如果因为某些原因worker线程一直不结束,也能终止这个线程,这种情况下最好记录一下日志来分析什么情况导致的。

    在Windows Form中被抛弃的前台线程是个潜在的危险,因为程序在主线程结束将要退出时,它还在继续运行。在Windows的任务管理器里,它在应用程序Tab里会消失,但在进程Tab里还在。除非用户显式地结束它。

    常见的程序退出失败的可能性就是忘记了前台线程。


    线程的优先级:线程的优先级决定了线程的执行时间。

    enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }

    线程的优先级为Highest时,并不意味着线程会实时运行,要想实时运行,进程的优先级也得是High。当你的进程的优先级是High时,如果程序进入了死循环,系统会死锁。这个时候就只有按电源键了。所以,慎用。

    最好将实时线程和UI分开在两个线程,并设置成不同的优先级,通过远程或共享内存通信,共享内存需要P/Invoking Win32 API(CreateFileMapping和MapViewOfFile)。


    线程的异常处理:线程一旦启动,任何在try/catch/finally范围内创建线程的代码块与try/catch/finally就没有什么关系了。

    public static void Main() {
    try {
    new Thread (Go).Start();
    }
    catch (Exception ex) {
    // We'll never get here!
    Console.WriteLine ("Exception!");
    }
    static void Go() { throw null; }
    }

    上例中的try/catch基本没用了,新创建的线程可能是未处理的空引用异常,最好在线程要执行的代码里加异常捕获:

    public static void Main() {
    new Thread (Go).Start();
    }
    static void Go() {
    try {
    ...
    throw null; // this exception will get caught below
    ...
    }
    catch (Exception ex) {
    Typically log the exception, and/or signal another thread
    that we've come unstuck
    ...
    }

    从.NET2.0开始,线程上任何未处理的异常会导致整个程序挂掉,意味着千万别忽略异常,在线程要执行的函数里,给每个可能异常的代码加上try/catch。这可能有点麻烦,所以,很多人这样处理,用全局的异常处理:

    using System;
    using System.Threading;
    using System.Windows.Forms;
    static class Program {
    static void Main() {
    Application.ThreadException += HandleError;
    Application.Run (new MainForm());
    }
    static void HandleError (object sender, ThreadExceptionEventArgs e) {
    Log exception, then either exit the app or continue...
    }
    }

    Application.ThreadException事件会在代码抛出异常时被触发,这样看起来很完美--可以捕获所有异常,但在worker线程上的异常可能捕获不了,在main函数里的窗体的构造函数,在Windows的消息循环之前就执行了。.NET提供了一个低层的事件捕获全局异常:AppDomain.UnhandledException,它才可以捕获所有异常(UI和非UI的)。虽然它提供了一个很好的方式捕获所有异常并记录异常日志,但是它没有办法阻止程序关系,也没有办法阻止.NET的异常对话框。

  • 相关阅读:
    Python学习笔记2——数据类型
    Python学习笔记1——环境配置
    Dagger2 探索记3——两大进阶组件(二)
    NLP 计算机视觉 cv 机器学习 ,入们基础
    Android studio 常用快捷键
    uiautomator 1使用简介
    在Android源码中如何吧so库打包编译进入apk, 集成第三方库(jar和so库)
    android框架Java API接口总注释/**@hide*/和internal API
    Google android开发者 中国官方文档开放了呀
    android aidl通信 RemoteCallbackList客户端注册回调
  • 原文地址:https://www.cnblogs.com/icuit/p/2800941.html
Copyright © 2020-2023  润新知