zoukankan      html  css  js  c++  java
  • C# 多线程总结

    1 创建线程

     

    1.1 异步委托方式

    使用异步委托创建的线程,都是由.Net线程池维护的。
    线程池中的线程总是后台线程。
    为了方便起见,接下来使用的共通委托方法如下1

    static int TakesAWhile(int data, int ms)
    {
        Console.WriteLine("TakesAWhile started");
        Thread.Sleep(ms);
        Console.WriteLine("TakesAWhile completed");
        return ++data;
    }
    

    1.1.1 IAsyncResult.IsCompleted

    根据IAsyncResult.IsCompleted判断异步委托是否执行完成。
    EndInvoke获取返回值。

    TakesAWhileDelegate dl = TakesAWhile;
    IAsyncResult ar = dl.BeginInvoke(1, 3000, null, null);
    while (!ar.IsCompleted)
    {
        Console.Write(".");
        Thread.Sleep(50);
    }
    int result = dl.EndInvoke(ar);
    Console.WriteLine("result: {0}", result);
    

    1.1.2 IAsyncResult.AsyncWaitHandle

    使用WaitHandle,可指定异步调用的超时时间进行后续处理。

    TakesAWhileDelegate dl = TakesAWhile;
    IAsyncResult ar = dl.BeginInvoke(1, 3000, null, null);
    if (!ar.AsyncWaitHandle.WaitOne(200, false))
    {
        Console.WriteLine("Thread not invoked.");
    }
    if (ar.AsyncWaitHandle.WaitOne(3000, false))
    {
        int result = dl.EndInvoke(ar);
        Console.WriteLine("result: {0}", result);
    }
    

    1.1.3 AsyncCallBack

    通过传入回调函数,进行后续处理

    • 分支一:单独定义回调方法
      static void Main(string[] args)
      {
          TakesAWhileDelegate dl = TakesAWhile;
          dl.BeginInvoke(1, 3000, TakesAWhileCompleted, dl);
          //必须程序主线程一直存在才会执行回调方法,所以使用了如下for循环(说明了异步委托所创建的线程是一个后台线程)
          for (int i = 0; i < 100; i++ )
          {
              Console.Write(".");
              Thread.Sleep(50);
          }
      }
      
      //定义回调方法
      static void TakesAWhileCompleted(IAsyncResult ar)
      {
          if (ar == null)
          {
              throw new ArgumentNullException("ar");
          }
          TakesAWhileDelegate dl = ar.AsyncState as TakesAWhileDelegate;
          Trace.Assert(dl != null, "Invalid object type");
          int result = dl.EndInvoke(ar);
          Console.WriteLine("result: {0}", result);
      }
      
    • 分支二:使用lambada表达式
      TakesAWhileDelegate dl = TakesAWhile;
      dl.BeginInvoke(1, 3000,
                     //这是个回调函数,使用lambada表达式的话,代码不够清晰。
                     ar =>
          {
              int result = dl.EndInvoke(ar);//lambda表达式可使用该作用域外部的变量dl
              Console.WriteLine("result: {0}", result);
          },
                     null);
      //必须程序主线程一直存在才会执行回调方法
      for (int i = 0; i < 100; i++)
      {
          Console.Write(".");
          Thread.Sleep(50);
      }
      

    1.2 Thread类

     

    1.2.1 无参数线程方法

    var t1 = new Thread(() => Console.WriteLine("running in a thread, id {0}", Thread.CurrentThread.ManagedThreadId));
    t1.Start();
    Console.WriteLine("This is a main thread, id {0}", Thread.CurrentThread.ManagedThreadId);
    

    1.2.2 有参数线程方法

    public struct Data
    {
        public string Message;
    }
    
    static int TakesAWhile(int data, int ms)
    {
        var d = new Data { Message = "Info" };
        var t2 = new Thread((object obj) =>
            {
                Data data = (Data)obj;
                Console.WriteLine("running in a thread, id {0}, Data {1}", Thread.CurrentThread.ManagedThreadId, data.Message);
            });
        t2.Start(d);
        Console.WriteLine("This is a main thread, id {0}", Thread.CurrentThread.ManagedThreadId);
    }
    

    1.2.3 后台线程

    Thread类默认创建的是前台线程,设定IsBackground属性可转为后台线程

    var t1 = new Thread(
        () => 
        {
            Console.WriteLine("branch thread Start, id {0}", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(3000);
            Console.WriteLine("branch thread End");
        }) 
    { Name = "NewBKThread", IsBackground = true };
    t1.Start();
    Thread.Sleep(50);//为了使后台线程的情况下,能打出branch thread Start, id
    Console.WriteLine("This is a main thread, id {0}", Thread.CurrentThread.ManagedThreadId);
    
    • IsBackground = true 结果:

    branch thread Start, id 3
    This is a main thread, id 1

    • IsBackground = false 结果:

    branch thread Start, id 3
    This is a main thread, id 1
    branch thread End

    1.2.4 关于线程优先级

    可以通过Thread.Priority属性调整线程的 基本 优先级。实际线程调度器会动态调整优先级
    频繁使用CPU的线程的优先级会动态调低,等待资源(等待磁盘IO完成等)的线程会动态调高优先级。
    以便在下次等待结束时获得CPU资源。2

    1.2.5 线程状态

    通过属性Thread.ThreadState获取当前线程状态

    运行Thread.Start()后,状态为Unstarted。
    系统线程调度器选择了运行该线程后,状态为Running。
    调用Thread.Sleep(),状态为WaitSleepJoin。

    停止另一个线程,调用Thread.Abort()。接到中止命令的线程中会抛出ThreadAbortException。3
    涉及的状态有AbortRequested、Aborted。
    继续停止的线程,调用Thread.ResetAbort()。线程将会在抛出ThreadAbortException后的语句后继续进行。

    等待线程的结束,调用ThreadInstance.Join()。
    该调用会停止 当前 线程,当前线程状态设为WaitSleepJoin。
    等待加入的线程处理完成,再继续当前线程的处理。

    1.3 线程池

    超出最大线程数时,QueueUserWorkItem会等待获取线程资源时再调用。

    static void Main(string[] args)
    {
        ThreadPool.SetMinThreads(3, 3);//创建线程池时启动的最小线程数
        ThreadPool.SetMaxThreads(10, 10);//最大线程数
        for (int i = 0; i < 5; i++ )
        {
            ThreadPool.QueueUserWorkItem(JobForAThread);
        }
        Thread.Sleep(3000);//由于是后台线程,需要使主线程等一会,否则程序直接退出
    }
    
    static void JobForAThread(object state)
    {
        for (int i = 0; i < 3; i++)
        {
            Console.WriteLine("loop {0}, running inside pooled thread {1}", i, Thread.CurrentThread.ManagedThreadId);
        }
    }
    

    使用线程池的限制:

    • 其中的所有线程只能是后台线程。
    • 无法设置线程的优先级或名称。
    • 关键点 适用于耗时较短的任务。长期运行的线程,应使用Thread类创建。

    2 同步问题

     

    2.1 lock关键字

    只能锁定引用类型,锁定值类型等于锁定了一个副本,没有意义,编译器也不允许你这么做。
    使用锁定需要时间,并不总是必须。可以创建类的两个版本,一个同步版本,一个异步版本。

    2.1.1 将实例成员设为线程安全的

    lock(this)
    {
        //一次只有一个线程能访问相同实例的该语句块
    }
    

    因为该实例对象也可用于外部访问,这样做会导致外部访问时也得等待该同步语句块执行完成。正确的做法:

    private object syncRoot = new object();
    public void DoSomething()
    {
        lock (object)
        {
            //Do something
        }
    }
    
    private static object syncRoot = new object();//可用于锁定类静态成员
    

    2.1.2 lock关键字由编译器解析为Monitor类

    lock (obj) {  };
    

    等价于:

    Monitor.Enter(obj);
    try
    {
    }
    finally
    {
        Monitor.Exit(obj);
    }
    

    与lock关键字的区别:

    • 可添加一个等待解锁的超时时间,使用TryEnter传递超时值。
    bool lockTaken = Monitor.TryEnter(obj, 500);
    if (lockTaken)
    {
        try
        {
    
        }
        finally
        {
            Monitor.Exit(obj);
        }
    }
    else
    {
        //didn't get the lock, do something else
    }
    

    2.1.3 更快速的Interlocked类

    仅用于简单的针对变量赋值的同步问题

    lock(this)
    {
        if (someState == null)
        {
            someState = newState;
        }
    }
    

    等价于(可用于单件模式的GetInstance):

    Interlocked.CompareExchange<SomeState>(ref someState, newState, null);//第一个参数和第三个参数比较,如果相等,替换为第二个参数的值
    
    public int State
    { 
        get 
        {
            lock (this)
            {
                return ++state;
            }
        }
    }
    

    等价于:

    public int State
    {
        get
        {
            return Interlocked.Increment(ref state);
        }
    }
    

    2.2 WaitHandle

    WaitHandle是一个抽象基类。用于等待某个信号量。
    Mutex、EventWaitHandle、Semaphore类都从WaitHandle派生。

    2.3 Mutex类

    提供进程之间的同步访问。创建一个进程之间能共享的以字符串命名的互斥锁。
    构造函数的一种形式如下:

    bool created;
    Mutex mutex = new Mutex(false, "IFFileMutex", out created);
    

    其中,第一个参数定义了该互斥体的所有权是否应属于调用线程。
    第二个参数是互斥体名字,操作系统能识别该字符串,以此实现各进程之间的同步。
    第三个参数,如果系统中已存在该命名的互斥体返回false,否则返回true。

    Mutex mutex = Mutex.OpenExisting("IFFileMutex");//打开系统中已存在的互斥体
    if(mutex.WaitOne(500))//500为等待超时时间
    {
        try
        {
            //synchronized region
        }
        finally
        {
            mutex.ReleaseMutex();
        }
    }
    

    2.4 Semaphore类

    信号量可以同时由多个线程使用,是计数的互斥体。一般用于受数量限制的访问资源(如DB连接资源)。

    2.5 Event类

    系统级的资源同步方式,比之Mutex,多了个Reset方法,
    重置nonsignaled的状态(等同于互斥体的锁定状态),释放所有等待的线程。

    Set方法:将事件设为signaled状态,使其他等待的线程得以继续,类似锁的Release方法。
    Waitone方法:等待事件被设为signaled状态。
    Reset方法:将事件设为nonsignaled状态,并且阻塞所有等待的线程。

    2.5.1 AutoResetEvent

    Reset方法会在某一线程Waitone成功后,自动重置为nonsignaled。
    达到的效果:一次只能一个线程继续处理。

    2.5.2 ManualResetEvent

    需手动调用Reset方法重置为nonsignaled。
    达到的效果:多个线程都能继续进行处理。

    2.6 ReaderWriterLockSlim类(.Net 3.5引入)

    如果没有Writer锁定资源,就允许多个Reader访问资源,但只能有一个
    Writer锁定该资源(所有访问中的Reader都必须先释放锁)。
    比之.Net 1.0版本 ReaderWriterLock类,重新设计为防止死锁,提供更好的性能。

    • EnterReadLock 进入锁定,另一个方法TryEnterReadLock允许指定一个超时时间。ExitReadLock释放锁定
    • EnterUpgradableReadLock 用于读取锁定需要改为写入锁定的情况。
    • EnterWriteLock 获得多资源的写入锁定。仅一个线程能获取锁定,在这之前还必须释放所有的读取锁定。

    3 Timer类

    .Net提供了几个Timer类,比较如下:

     
    命名空间说明
    System.Threading 提供了Timer的核心功能,在构造函数中传入回调的委托。
    System.Timer 继承Component,可在设计界面拖入,提供了基于事件的机制(非委托)。
    System.Windows.Forms 为单线程环境设计的(创建和回调在同一个线程中执行),执行回调方法时UI会假死,不宜执行耗时较长的代码。该Timer时间精度55ms。
    System.Web.UI 是一个AJAX扩展,可以用于Web页面

    4 总结

     
    目的参考开销4是否跨进程?
    lock(Monitor) 保证单个进程内只有一个线程能够获取同步资源 20ns No
    Mutex 保证只有一个线程能够获取同步资源 1000ns Yes
    Semaphore 可指定可获取同步资源的线程数 1000ns Yes
    ReaderWriterLock 允许多个Reader访问同步资源 100ns No
    AutoResetEvent 当信号被设为signaled状态时,允许单个线程进入同步资源块 1000ns Yes
    ManualResetEvent 当信号被设为signaled状态时,允许所有等待线程进入同步资源块 1000ns Yes
    ReaderWriterLockSlim 可锁定多个Reader访问资源以及单个Writer修改资源 40ns No
    • 注:一些Slim类(如ManualResetEventSlim),比之旧版本,通常拥有更好的性能。参考 MSDN

    几条规则:

    1. 尽量使同步要求最低,尝试避免共享状态。
    2. 类的静态成员应是线程安全的。
    3. 实例成员不需要是线程安全的。为了最佳性能,最好在类的外部处理同步问题。

    完整代码示例:MultiThreadDemo.rar

  • 相关阅读:
    View使用总结
    IPC机制总结
    Activity 启动模式总结
    StrictMode 严格模式
    dumpsys, traceView调试命令
    Monkey命令及调试
    Android Studio使用tips
    Java常用数据结构Set, Map, List
    JavaScript String 小球重力弹回
    JavaScript Array 数组方法汇总
  • 原文地址:https://www.cnblogs.com/ywsoftware/p/3103579.html
Copyright © 2011-2022 走看看