C#线程(一)

本篇作为自己简要的笔记和整理,自己英语略渣  就不翻译了 

参考http://www.albahari.com/threading

这里有翻译的http://www.cnblogs.com/miniwiki/archive/2010/06/18/1760540.html

这个是后面发现翻译的相当好的http://blog.gkarch.com/threading/part1.html

C#支持多线程并行执行代码,但是有一个主线程是被CLR和操作系统自动创建的。  在没有创建其他线程之前,所有代码都在主线程中进行;

在使用线程前   请记得引用以下两个命名空间

using System;

using System.Threading; 

 启动一个新的线程

class Program
    {
        static void Main()
        {
            Thread t = new Thread(WriteY);          //启动一个新的线程
            t.Start();                              //运行WriteY()

            for (int i = 0; i < 100; i++)
            {
                Console.Write("x");
            }
            Console.ReadKey();
        }

        static void WriteY()
        {
            for (int i = 0; i < 100; i++)
            {
                Console.Write("y");
            }
        }
    }

 

多运行几次会发现每次的输出都是不一样的    这也说明了多条线程同行运行的情况下 是没有特定的执行顺序的  是并行的

 

 

CLR分配每个线程到它自己的内存堆栈上,来保证局部变量的分离运行

static void Main()
        {
            new Thread(Go).Start();      //在新线程中调用Go
            Go();                 //在主线程中调用Go
            Console.ReadKey();
        }

        static void Go()
        {
        //声明并使用局部变量 cycles for (int cycles = 0; cycles < 5; cycles++) { Console.Write("?"); } }

 变量cycles的副本分别在各自的内存堆栈中创建

当线程们引用了一些公用的目标实例的时候,他们会共享数据。

class ThreadTest
    {
        bool done;
        static void Main()
        {
            ThreadTest tt = new ThreadTest();       //创建一个实例
            new Thread(tt.Go).Start();              //开启新线程调用实例中的方法
            tt.Go();                                //直接通过主线程调用实例中的方法
            Console.ReadKey();
        }

        void Go()                   //作为一个实例方法
        {
            if (!done) { done = true; Console.WriteLine("Done"); }
        }
    }

只输出了一次   实例tt同时被新线程与主线程调用    在同一个实例中的数据是共享的   在新线程调用后done就被赋值成了true 所以在主线程调用的时候不满足条件所以没有进行第二次打印

静态字段在所有线程间共享数据

 class ThreadTest
    {
        static bool done;           //静态字段
        static void Main()
        {
            new Thread(Go).Start();
            Go();
            Console.ReadKey();
        }

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

 同样只输出了一次

将Go改一下

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

是不是很大的概率输出了两次    那么问题就来了  为什么只是换了下输出与赋值的顺序就不一样了

想一下这样的情况 : 一个线程在判断if块的时候,正好另一个线程正在执行WriteLine语句——在它将done设置为true之前。是不是就输出了两次了呢;

这样就引出了一个关键概念, 那就是线程安全

线程安全

补救措施是当读写公共字段的时候,提供一个独占锁,C#提供了lock语句来达到这个目的

    class ThreadTest
    {
        static bool done;
        static readonly object locker = new object();
        static void Main()
        {
            new Thread(Go).Start();
            Go();
            Console.ReadKey();
        }

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

 当两个线程执行时遇到同一个锁的时候,一个线程被阻止,然后等待,直到锁变为可用状态

这种情况确保了只有一个线程能进入关键部分的代码      代码以如此方式在不确定的多线程环境中被叫做线程安全

一个线程被锁住的时候,不消耗CPU的资源;

Join and Sleep

 你可以通过调用Join方法来等待一个线程结束

Join还有两个带参数的方法     public bool Join(int millisecondsTimeout);   等待多少毫秒后退出

public bool Join(TimeSpan timeout); 类型为System.TimeSpan的时间间隔  

两个带参数的方法都带有返回值 ,如果线程进终止,则返回true, 如果线程事经过了多少时间量后终止的 则返回false

Thread.Sleep  在指定时间内暂停当前线程

Thread.Sleep(TimeSpan.FromSeconds(1));
Thread.Sleep(500);

 Sleep 或者 Join 阻塞的线程都不消耗CPU资源

Thread.Sleep(0) 立即放弃线程的当前时间片,自愿交出其他线程的CPU  在Framework 4.0中  新Thread . yield()方法做同样的事

创建和使用新线程

调用Start方法后,线程开始运行,线程一直到它所调用的方法返回后结束

        static void Main()
        {
            Thread t = new Thread(new ThreadStart(Go));
            t.Start();
            Go();
            Console.ReadKey();
        }

        static void Go()
        {
            Console.WriteLine("Hello!");
        }

 一个线程可以更方便地通过指定一个方法

Thread t = new Thread(Go);      //不需要显示的调用ThreadStart

 在这种情况,ThreadStart被编译器自动推断出来,另一个快捷的方式是使用匿名方法或者lambda表达式来启动线程:

static void Main()
{
  Thread t = new Thread(delegate() { Console.WriteLine("Hello!"); });    //匿名方法
  Thread t = new Thread(() => { Console.WriteLine("Hello!"); });         //lambda表达式
  t.Start();
  Console.ReadKey();
}    

 

 将数据传入ThreadStart中

1.最简单的将参数传入线程的方法是执行一个lambda表达式 或者通过delegate匿名方法

        static void Main()
        {
            Thread t = new Thread(() => { Console.WriteLine("Hello!"); });      //lambda表达式
            t.Start();
            Console.ReadKey();
        }   

 使用这种方法可以将任意数量的参数传递给方法

        static void Main()
        {
            new Thread(() => 
            { 
                Console.WriteLine("I'm running on another thread!");
                Console.WriteLine("This is so easy!");
            }).Start();      //lambda表达式
            Console.ReadKey();
        }    

 

2.另一种方法是通过一个线程的参数

        static void Main()
        {
       Thread t = new Thread(new ParameterizedThreadStart(Print)); Thread t = new Thread(Print);          //不显示调用ParameterizedThreadStart t.Start("Hello from t!"); Console.ReadKey(); } static void Print(object messageObj) { string message = (string)messageObj; Console.WriteLine(message); }

 这样可以运行时因为在Thread的构造函数重载接受两种委托

public delegate void ThreadStart();
public delegate void ParameterizedThreadStart (object obj);

需要注意的是ParameterizedThreadStart仅接收一个参数,并且参数的类型是object

Lambda 表达式和获取数据

Lambda表达式在将数据传递给线程的时候是非常强大的,然而必须小心意外修改变量的值,因为这些变量都是共享的

static void Main()
{
  for (int i = 0; i < 10; i++)
  {
    new Thread(() => Console.Write(i)).Start();
  }
  Console.ReadKey();
}  

输出的结果是完全不正确的     

解决方案是使用一个零时变量

        static void Main()
        {
            for (int i = 0; i < 10; i++)
            {
                int temp = i;
                new Thread(() => Console.WriteLine(temp)).Start();
            }
            Console.ReadKey();
        }

 此处输出并不是一定是按照顺序输出    但是不会出现重复的输出

 

        static void Main()
        {
            string text = "t1";
            Thread t1 = new Thread(() => Console.WriteLine(text));
            text = "t2";
            Thread t2 = new Thread(() => Console.WriteLine(text));
            t1.Start();
            t2.Start();

            Console.ReadKey();
        }

因为lambda表达式捕获相同的文本变量 所以打印两次t2

试着改成这样

        static void Main()
        {
            string text = "t1";
            Thread t1 = new Thread(() => Console.WriteLine(text));
            t1.Start();
            text = "t2";
            Thread t2 = new Thread(() => Console.WriteLine(text));
            t2.Start();

            Console.ReadKey();
        }

 同样是输出了两个t2  再改一下

        static void Main()
        {
            ........
            t1.Start();
            Thread.Sleep(10);
            text = "t2";
            ........
        }

 输出一个t1一个t2上面输出两个是因为t1线程启动的时候text已经被更改为了t2  这里可以用Thread.Sleep(10)等一会 或者用lock就可以输出不一样的结果了

命名线程

线程的名字可以在被任何时间设置——但只能设置一次,重命名会引发异常

程序的主线程也可以被命名,下面例子里主线程通过CurrentThread命名:

        static void Main()
        {
            Thread.CurrentThread.Name = "main";
            Thread worker = new Thread(Go);
            worker.Name = "worker";
            worker.Start();
            Go();
            Console.ReadKey();
        }

        static void Go()
        {
            Console.WriteLine("Hello from  " + Thread.CurrentThread.Name + "    "+DateTime.Now.Millisecond);
        }

 输出的顺序是不一定的哦

前台线程和后台线程

默认情况下,线程创建显式前台线程。前台线程保持应用程序的活着,只要其中任何一个正在运行,而后台线程则不是。所有前台线程完成后,应用程序结束,任何后台线程的运行将被终止

一个线程事前台线程/后台线程与优先级或分配执行时间没有关系

你可以查询或者改变线程的前后线程状态,用IsBackground属性 for example:

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

如果程序被调用的时候没有任何参数,工作线程为前台线程,并且将等待ReadLine语句来等待用户的触发回车,这期间,主线程退出,但是程序保持运行,因为一个前台线程仍然活着 

线程优先级

线程的优先级属性决定了执行时间

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

这个只有在多线程同时活跃的时候

提升一个线程的优先级——它可能导致问题,如资源为其他线程饥饿。

提升一个线程的优先级不让它能够执行实时工作,因为它仍然是由应用程序进行节流进程优先级。执行实时工作,您还必须提升进程优先级 通过Syste.Diagnostics空间下的Process类

        static void Main(string[] args)
        {
            using (Process p = Process.GetCurrentProcess())
            {
                p.PriorityClass = ProcessPriorityClass.High;  //提升进程优先级
            } 
        }

 异常处理

任何线程创建范围内try/catch/finally块,当线程开始执行便不再与其有任何关系  考虑下面的程序:

    class ThreadTest
    {
        static void Main(string[] args)
        {
            try
            {
                new Thread(Go).Start();
            }catch(Exception ex)
            {
                Console.WriteLine("Exception!  " + ex);     //不会在这得到异常
            }
            Console.ReadKey();
        }

        static void Go()
        {
            throw null;
        }//将得到NullReferenceException
    }

运行时,将会报错        这里try/catch语句一点用也没有,新创建的线程将引发NullReferenceException异常。当你考虑到每个线程有独立的执行路径的时候,便知道这行为是有道理的,

补救方法是在线程处理的方法内加入他们自己的异常处理

        static void Main(string[] args)
        {
            new Thread(Go).Start();
        }

        static void Go()
        {
            try
            {
                //.....
                throw null;     //这个异常在下面会被捕捉到
                //.....
            }
            catch(Exception ex)
            {
                //记录异常日志,并且或通知另一个线程
                //我们发生错误
            }
        }

 从.NET 2.0开始,任何线程内的未处理的异常都将导致整个程序关闭,这意味着忽略异常不再是一个选项了。因此为了避免由未处理异常引起的程序崩溃,try/catch块需要出现在每个线程进入的方法内,至少要在产品程序中应该如此。对于经常使用“全局”

然而在某些情况下,可以不必处理工作线程上的异常,因为 .NET Framework 会为你处理。这些会在接下来的内容中讲到:

异步委托

BackgroundWorker

任务并行库(TPL)

线程池

当启动一个线程时,会有几百毫秒的时间花费在准备一些额外的资源上,例如一个新的私有局部变量栈这样的事情。每个线程会占用(默认情况下)1MB 内存。线程池(thread pool)可以通过共享与回收线程来减轻这些开销,允许多线程应用在很小的粒度上而没有性能损失。在多核心处理器以分治(divide-and- conquer)的风格执行计算密集代码时将会十分有用。

线程池会限制其同时运行的工作线程总数。太多的活动线程会加重操作系统的管理负担,也会降低 CPU 缓存的效果。一旦达到数量限制,任务就会进行排队,等待一个任务完成后才会启动另一个。这使得程序任意并发成为可能,例如 web 服务器。(异步方法模式(asynchronous method pattern)是进一步高效利用线程池的高级技术

有多种方法可以使用线程池:

通过任务并行库(TPL)(Framework 4.0 中加入)

调用ThreadPool.QueueUserWorkItem

通过异步委托

通过BackgroundWorker

以下构造会间接使用线程池:

  • WCF、Remoting、ASP.NET 和 ASMX 网络服务应用
  • System.Timers.TimerSystem.Threading.Timer
  • .NET Framework 中名字以 Async 结尾的方法,例如WebClient上的方法(使用基于事件的异步模式,EAP),和大部分BeginXXX方法(异步编程模型模式,APM)
  • PLINQ

在使用线程池线程时有几点需要小心:

  • 无法设置线程池线程的Name属性,这会令调试更为困难(当然,调试时也可以在 Visual Studio 的线程窗口中给线程附加备注)。
  • 线程池线程永远是后台线程(一般不是问题)。
  • 阻塞线程池线程可能会在程序早期带来额外的延迟,除非调用了ThreadPool.SetMinThreads(见优化线程池)。

  可以改变线程池线程的优先级,当它用完后返回线程池时会被恢复到默认状态。

可以通过Thread.CurrentThread.IsThreadPoolThread属性来查询当前是否运行在线程池线程上。

3.1通过 TPL 使用线程池

TPL 具有更多的特性,非常适合于利用多核处理器

可以很容易的使用任务并行库(Task Parallel Library,TPL)中的Task类来使用线程池。Task类在 Framework 4.0 时被加入:如果你熟悉旧式的构造,可以将非泛型的Task类看作ThreadPool.QueueUserWorkItem的替代,而泛型的Task<TResult>看作异步委托的替代。比起旧式的构造,新式的构造会更快速,更方便,并且更灵活。

要使用非泛型的Task类,调用Task.Factory.StartNew,并传递目标方法的委托:

        static void Main(string[] args)
        {
            Task.Factory.StartNew(Go);
        }

        static void Go()
        {
            Console.WriteLine("Hellow from the thread pool!");
        }

当调用TaskWait方法时,所有未处理的异常会在宿主线程被重新抛出。(如果不调用Wait而是丢弃不管,未处理的异常会像普通的线程那样结束程序。(译者注:在 .NET 4.5 中,为了支持基于async / await的异步模式,Task中这种“未观察”的异常默认会被忽略,而不会导致进程结束

泛型的Task<TResult>类是非泛型Task的子类。它可以使你在其完成执行后得到一个返回值。在下面的例子中,我们使用Task<TResult>来下载一个网页:

        static void Main(string[] args)
        {
            //启动task
            Task<string> task = Task.Factory.StartNew<string>(() => DownloadString("https://www.baidu.com"));

            RunSomeOtherMethod();           //执行其他工作,它会和task并行执行

            //通过Result属性获取返回值
            //如果仍在执行中,当前进程会阻塞等待到task结束
            string result = task.Result;
            Console.ReadKey();
        }

        static string DownloadString(string url)
        {
            using (var wc = new System.Net.WebClient()) {
                return wc.DownloadString(url);
            }
        }

        static void RunSomeOtherMethod()
        {
            Console.WriteLine("RunSomeOtherMethod!");
        }

 

(这里的<string> 类型参数是为了示例的清晰,它可以被省略,让编译器推断。)

查询task的Result属性时,未处理的异常会被封装在AggregateException中自动重新抛出。然而,如果没有查询Result属性(并且也没有调用Wait),未处理的异常会令程序结束。

Task.Factory.StartNew返回一个Task对象,可以用来监视任务,例如通过调用Wait方法来等待其结束。

不通过TPL使用线程

如果是使用 .NET Framework 4.0 以前的版本,则不能使用任务并行库。你必须通过一种旧的构造使用线程池:ThreadPool.QueueUserWorkItem与异步委托。这两者之间的不同在于异步委托可以让你从线程中返回数据,同时异步委托还可以将异常封送回调用方

要使用QueueUserWorkItem,仅需要使用希望在线程池线程上运行的委托来调用该方法:

    class ThreadTest
    {
        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(Go);       //没有参数data为null
            ThreadPool.QueueUserWorkItem(Go, 123);

            Console.ReadKey();
        }

        static void Go(object data)
        {
            Console.WriteLine("Hello from the thread pool!  " + data);
        }
    }

目标方法Go,必须接受单一一个object参数(来满足WaitCallback委托)。这提供了一种向方法传递数据的便捷方式,就像ParameterizedThreadStart一样。与Task不同,QueueUserWorkItem并不会返回一个对象来帮助我们在后续管理其执行。并且,你必须在目标代码中显式处理异常,未处理的异常会令程序结束。

异步委托

ThreadPool.QueueUserWorkItem并没有提供 在线程执行结束之后从线程中返回值的简单机制。异步委托调用(asynchronous delegate invocations )解决了这一问题,可以允许双向传递任意数量的参数。而且,异步委托上的未处理异常可以方便的原线程上重新抛出(更确切的说,在调用EndInvoke的线程上),所以它们不需要显式处理。

不要混淆异步委托和异步方法(asynchronous methods ,以 BeginEnd 开始的方法,比如File.BeginRead/File.EndRead)。异步方法表面上看使用了相似的方式,但是其实是为了解决更困难的问题。

下面是如何通过异步委托启动一个工作线程:

  1. 创建目标方法的委托(通常是一个Func类型的委托)。
  2. 在该委托上调用BeginInvoke,保存其IAsyncResult类型的返回值。

    BeginInvokde会立即返回。当线程池线程正在工作时,你可以执行其它的动作。

  3. 当需要结果时,在委托上调用EndInvoke,传递所保存的IAsyncResult对象。

接下来的例子中,我们使用异步委托调用来和主线程中并行运行一个返回字行串长度的简单方法:

        static void Main(string[] args)
        {
            Func<string, int> method = Work;
            IAsyncResult cookie = method.BeginInvoke("test", null, null);

            int result = method.EndInvoke(cookie);
            Console.WriteLine("String length is: " + result);

            Console.ReadKey();
        }

        static int Work(string s)
        {
            return s.Length;
        }

 

EndInvoke会做三件事:

  1. 如果异步委托还没有结束,它会等待异步委托完成执行。
  2. 它会接收返回值(也包括refout方式的参数)。
  3. 它会向调用线程抛出未处理的异常。

如果使用异步委托调用的方法没有返回值,技术上你仍然需要调用EndInvoke。在实践中,这里存在争论,因为不调用EndInvoke也不会有什么损失。然而如果你选择不调用它,就需要考虑目标方法中的异常处理来避免错误无法察觉。(无论您使用何种方法,都要调用 EndInvoke 来完成异步调用。)

 

当调用BeginInvoke时也可以指定一个回调委托。这是一个在完成时会被自动调用的、接受IAsyncResult对象的方法。这样可以在后面的代码中“忘记”异步委托,但是需要在回调方法里做点其它工作:

        static void Main(string[] args)
        {
            Func<string, int> method = Work;
            method.BeginInvoke("test", Done, method);

            Console.ReadKey();
        }

        static int Work(string s) { return s.Length; }

        static void Done(IAsyncResult cookie)
        {
            var target = (Func<string, int>)cookie.AsyncState;
            int result = target.EndInvoke(cookie);
            Console.WriteLine("String length is: " + result);
        }

BeginInvoke的最后一个参数是一个用户状态对象,用于设置IAsyncResultAsyncState属性。它可以是需要的任何东西,在这个例子中,我们用它向回调方法传递method委托,这样才能够在它上面调用EndInvoke

优化线程池

 

线程池初始时其池内只有一个线程。随着任务的分配,线程池管理器就会向池内“注入”新线程来满足工作负荷的需要,直到最大数量的限制。在足够的非活动时间之后,线程池管理器在认为“回收”一些线程能够带来更好的吞吐量时进行线程回收。

可以通过调用ThreadPool.SetMaxThreads方法来设置线程池可以创建的线程上限;默认如下:

  • Framework 4.0,32位环境下:1023
  • Framework 4.0,64位环境下:32768
  • Framework 3.5:每个核心 250
  • Framework 2.0:每个核心 25

(这些数字可能根据硬件和操作系统不同而有差异。)数量这么多是因为要确定阻塞(等待一些条件,比如远程计算机的相应)的线程的条件是否被满足。

也可以通过ThreadPool.SetMinThreads设置线程数量下限。下限的作用比较奇妙:它是一种高级的优化技术,用来指示线程池管理器在达到下限之前不要延迟线程的分配。当存在阻塞线程时,提高下限可以改善程序并发性。

默认下限数量是 CPU 核心数,也就是能充分利用 CPU 的最小数值。在服务器环境下(比如 IIS 中的 ASP.NET),下限数量一般要高的多,差不多 50 或者更高。

 

 

  将线程池的最小线程数设置为 x 并不是立即创建至少 x 个线程,而是线程会根据需要来创建。这个数值是指示线程池管理器当需要的时候,立即 创建 x 个线程。那么问题是为什么线程池在其它情况下会延迟创建线程?

  答案是为了防止短生命周期的任务导致线程数量短暂高峰,使程序的内存足迹(memory footprint)快速膨胀。为了描述这个问题,考虑在一个 4 核的计算机上运行一个客户端程序,它一次发起了 40 个任务请求。如果每个任务都是一个 10ms 的计算,假设它们平均分配在 4 个核心上,总共的开销就是 100ms 多。理想情况下,我们希望这 40 个任务运行在 4 个线程上:

  • 如果线程数量更少,就无法充分利用 4 个核心。
  • 如果线程数量更多,会浪费内存和 CPU 时间去创建不必要的线程。

  线程池就是以这种方式工作的。让线程数量和 CPU 核心数量匹配,就能够既保持小的内存足迹,又不损失性能。当然这也需要线程都能被充分使用(在这个例子中满足该条件)。

  但是,现在来假设任务不是进行 10ms 的计算,而是请求互联网,使用半秒钟等待响应,此时本地 CPU 是空闲状态。线程池管理器的线程经济策略(译者注:指上面说的线程数量匹配核心数)这时就不灵了,应该创建更多的线程,让所有的请求同时进行。

  幸运的是,线程池管理器还有一个后备方案。如果在半秒内没有能够响应请求队列,就会再创建一个新的线程,以此类推,直到线程数量上限。

  半秒的等待时间是一把双刃剑。一方面它意味着一次性的短暂任务不会使程序快速消耗不必要的40MB(或者更多)的内存。另一方面,在线程池线程被阻塞时,比如在请求数据库或者调用WebClient.DownloadFile,就进行不必要的等待。因为这种原因,你可以通过调用SetMinThreads来让线程池管理器在分配最初的 x 个线程时不要等待,例如:

ThreadPool.SetMinThreads (50, 50);

  (第二个参数是表示多少个线程分配给 I/O 完成端口(I/O completion ports,IOCP),来被APM使用,这会在C# 4.0 in a Nutshell 的第 23 章描述。)

  最小线程数量的默认值是 CPU 核心数。

posted @ 2015-11-09 10:58  我爱吃橙子  阅读(305)  评论(0编辑  收藏  举报