C#線程


C#的線程(一)

初識線程

線程是一個獨立的運行單元,每個進程內部都有多個線程,每個線程都可以各自同時執行指令。每個線程都有自己獨立的棧,但是與進程內的其他線程共享內存。但是對於.NET的客戶端程序(Console,WPF,WinForms)是由CLR創建的單線程(主線程,且只創建一個線程)來啟動。在該線程上可以創建其他線程。

圖:

線程工作方式

多線程由內部線程調度程序管理,線程調度器通常是CLR委派給操作系統的函數。線程調度程序確保所有活動線程都被分配到合適的執行時間,線程在等待或阻止時 (例如,在一個獨占鎖或用戶輸入) 不會消耗 CPU 時間。
在單處理器計算機上,線程調度程序是執行時間切片 — 迅速切換每個活動線程。在 Windows 中, 一個時間片是通常數十毫秒為單位的區域 — — 相比來說 線程間相互切換比CPU更消耗資源。在多處理器計算機上,多線程用一種混合的時間切片和真正的並發性來實現,不同的線程會在不同的cpu運行代碼。

創建線程

如:

using System;
using System.Threading;

class ThreadTest
{
  static void Main()
  {
    Thread t = new Thread (Write2);          // 創建線程t
    t.Start();                               // 執行 Write2()
 
    // 同時執行主線程上的該方法
    for (int i = 0; i < 1000; i++) Console.Write ("1");
  }
 
  static void Write2()
  {
    for (int i = 0; i < 1000; i++) Console.Write ("2");
  }
}

//輸出:
//111122221122221212122221212......

在主線程上創建了一個新的線程,該新線程執行WrWrite2方法,在調用t.Start()時,主線程並行,輸出“1”。

圖:

線程Start()之后,線程的IsAlive屬性就為true,直到該線程結束(當線程傳入的方法結束時,該線程就結束)。


CLR使每個線程都有自己獨立的內存棧,所以每個線程的本地變量都相互獨立。

如:

static void Main() 
{
  new Thread (Go).Start();      // 創建一個新線程,並調用Go方法
  Go();                         // 在主線程上調用Go方法
}
 
static void Go()
{
  // 聲明一個本地局部變量 cycles
  for (int cycles = 0; cycles < 5; cycles++) Console.Write ('N');
}
//輸出:
//NNNNNNNNNN (共輸出10個N)

在新線程和主線程上調用Go方法時分別創建了變量cycles,這時cycles在不同的線程棧上,所以相互獨立不受影響。

圖:

如果不同線程指向同一個實例的引用,那么不同的線程共享該實例。

如:

class ThreadTest
{
  //全局變量
  int i;
 
  static void Main()
  {
    ThreadTest tt = new ThreadTest();   // 創建一個ThreadTest類的實例
    new Thread (tt.Go).Start();
    tt.Go();
  }
 
  // Go方法屬於ThreadTest的實例
  void Go() 
  {
     if (i==1) { ++i; Console.WriteLine (i); }
  }
}
//輸出:
//2

新線程和主線程上調用了同一個實例的Go方法,所以變量i共享。

靜態變量也可以被多線程共享

class ThreadTest 
{
  static int i;    // 靜態變量可以被線程共享
 
  static void Main()
  {
    new Thread (Go).Start();
    Go();
  }
 
  static void Go()
  {
    if (i==1) { ++i; Console.WriteLine (i); }
  }
}

//輸出:
//2

如果將Go方法的代碼位置互換

 static void Go()
  {
    if (i==1) {  Console.WriteLine (i);++i;}
  }

//輸出:
//1
//1(有時輸出一個,有時輸出兩個)

如果新線程在Write之后,done=true之前,主線程也執行到了write那么就會有兩個done。

不同線程在讀寫共享字段時會出現不可控的輸出,這就是多線程的線程安全問題。

解決方法: 使用排它鎖來解決這個問題--lock

class ThreadSafe 
{
  static bool done;
  static readonly object locker = new object();
 
  static void Main()
  {
    new Thread (Go).Start();
    Go();
  }
 
  static void Go()
  {
    //使用lock,確保一次只有一個線程執行該代碼
    lock (locker)
    {
      if (!done) { Console.WriteLine ("Done"); done = true; }
    }
  }
}

當多個線程都在爭取這個排它鎖時,一個線程獲取該鎖,其他線程會處於blocked狀態(該狀態時不消耗cpu),等待另一個線程釋放鎖時,捕獲該鎖。這就保證了一次
只有一個線程執行該代碼。


Join和Sleep

Join可以實現暫停另一個線程,直到調用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");
}

//輸出:
//yyyyyy..... Thread t has ended!

線程t調用Join方法,阻塞主線程,直到t線程執行結束,再執行主線程。

Sleep:暫停該線程一段時間

Thread.Sleep (TimeSpan.FromHours (1));  // 暫停一個小時
Thread.Sleep (500);                     // 暫停500毫秒

Join是暫停別的線程,Sleep是暫停自己線程。

上面的例子是使用Thread類的構造函數,給構造函數傳入一個ThreadStart委托。來實現的。

public delegate void ThreadStart();

然后調用Start方法,來執行該線程。委托執行完該線程也結束。

如:

class ThreadTest
{
  static void Main() 
  {
    Thread t = new Thread (new ThreadStart (Go));
 
    t.Start();   // 執行Go方法
    Go();        // 同時在主線程上執行Go方法
  }
 
  static void Go()
  {
    Console.WriteLine ("hello!");
  }
}

多數情況下,可以不用new ThreadStart委托。直接在構造函數里傳入void類型的方法。

Thread t = new Thread (Go); 

使用lambda表達式

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

Foreground線程和Background線程

默認情況下創建的線程都是Foreground,只要有一個Foregournd線程在執行,應用程序就不會關閉。
Background線程則不是。一旦Foreground線程執行完,應用程序結束,background就會強制結束。
可以用IsBackground來查看該線程是什么類型的線程。


線程異常捕獲

public static void Main()
{
  try
  {
    new Thread (Go).Start();
  }
  catch (Exception ex)
  {
    // 不能捕獲異常
    Console.WriteLine ("Exception!");
  }
}
 
static void Go() { throw null; }   //拋出 Null異常

此時並不能在Main方法里捕獲線程Go方法的異常,如果是Thread自身的異常可以捕獲。

正確捕獲方式:

public static void Main()
{
   new Thread (Go).Start();
}
 
static void Go()
{
  try
  {
    // ...
    throw null;    // 這個異常會被下面捕獲
    // ...
  }
  catch (Exception ex)
  {
     // ...
  }
}


線程池

當創建一個線程時,就會消耗幾百毫秒cpu,創建一些新的私有局部變量棧。每個線程還消耗(默認)約1 MB的內存。線程池通過共享和回收線程,允許在不影響性能的情況下啟用多線程。
每個.NET程序都有一個線程池,線程池維護着一定數量的工作線程,這些線程等待着執行分配下來的任務。

線程池線程注意點:

1 線程池的線程不能設置名字(導致線程調試困難)。
2 線程池的線程都是background線程
3 阻塞一個線程池的線程,會導致延遲。
4 可以隨意設置線程池的優先級,在回到線程池時改線程就會被重置。

通過Thread.CurrentThread.IsThreadPoolThread.可以查看該線程是否是線程池的線程。

使用線程池創建線程的方法:

  • Task
  • ThreadPool.QueueUserWorkItem
  • Asynchronous delegates
  • BackgroundWorker

TPL

Framework4.0下可以使用Task來創建線程池線程。調用Task.Factory.StartNew(),傳遞一個委托

  • Task.Factory.StartNew

static void Main() 
{
  Task.Factory.StartNew (Go);
}
 
static void Go()
{
  Console.WriteLine ("Hello from the thread pool!");
}

Task.Factory.StartNew 返回一個Task對象。可以調用該Task對象的Wait來等待該線程結束,調用Wait時會阻塞調用者的線程。

  • Task構造函數
    給Task構造函數傳遞Action委托,或對應的方法,調用start方法,啟動任務
static void Main() 
{
  Task t=new Task(Go);
  t.Start();
}
 
static void Go()
{
  Console.WriteLine ("Hello from the thread pool!");
}

  • Task.Run

直接調用Task.Run傳入方法,執行。


static void Main() 
{
  Task.Run(() => Go());
}
 
static void Go()
{
  Console.WriteLine ("Hello from the thread pool!");
}


QueueUserWorkItem

QueueUserWorkItem沒有返回值。使用 QueueUserWorkItem,只需傳遞相應委托的方法就行。

static void Main()
{
  //Go方法的參數data此時為空
  ThreadPool.QueueUserWorkItem (Go);
  //Go方法的參數data此時為123
  ThreadPool.QueueUserWorkItem (Go, 123);
  Console.ReadLine();
}
 
static void Go (object data) 
{
  Console.WriteLine ("Hello from the thread pool! " + data);
}

委托異步

委托異步可以返回任意類型個數的值。
使用委托異步的方式:

  1. 聲明一個和方法匹配的委托
  2. 調用該委托的BeginInvoke方法,獲取返回類型為IAsyncResult的值
  3. 調用EndInvoke方法傳遞IAsyncResulte類型的值獲取最終結果

如:

static void Main()
{
  Func<string, int> method = Work;
  IAsyncResult cookie = method.BeginInvoke ("test", null, null);
  //
  // ... 此時可以同步處理其他事情
  //
  int result = method.EndInvoke (cookie);
  Console.WriteLine ("String length is: " + result);
}
 
static int Work (string s) { return s.Length; }

使用回調函數來簡化委托的異步調用,回調函數參數為IAsyncResult類型

static void Main()
{
  Func<string, int> method = Work;
  method.BeginInvoke ("test", Done, method);
  // ...
  //並行其他事情
}
 
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);
}

使用匿名方法

 Func<string, int> f = s => { return s.Length; };
  f.BeginInvoke("hello", arg =>
  {
      var target = (Func<string, int>)arg.AsyncState;
      int result = target.EndInvoke(arg);
      Console.WriteLine("String length is: " + result);
  }, f);

線程傳參和線程返回值

Thread

Thread構造函數傳遞方法有兩種方式:

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

所以Thread可以傳遞零個或一個參數,但是沒有返回值。

  • 使用lambda表達式直接傳入參數。
static void Main()
{
  Thread t = new Thread ( () => Print ("Hello from t!") );
  t.Start();
}
 
static void Print (string message) 
{
  Console.WriteLine (message);
}
  • 調用Start方法時傳入參數
static void Main()
{
  Thread t = new Thread (Print);
  t.Start ("Hello from t!");
}
 
static void Print (object messageObj)
{
  string message = (string) messageObj;   
  Console.WriteLine (message);
}

Lambda簡潔高效,但是在捕獲變量的時候要注意,捕獲的變量是否共享。
如:

for (int i = 0; i < 10; i++)
  new Thread (() => Console.Write (i)).Start();

//輸出:
//0223447899

因為每次循環中的i都是同一個i,是共享變量,在輸出的過程中,i的值會發生變化。

解決方法-局部域變量

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

這時每個線程都指向新的域變量temp(此時每個線程都有屬於自己的花括號的域變量)在該線程中temp不受其他線程影響。


委托

委托可以有任意個傳入和輸出參數。以Action,Func來舉例。

  • Action 有零個或多個傳入參數,但是沒有返回值。
  • Func 有零個或多個傳入參數,和一個返回值。
  Func<string, int> method = Work;
  IAsyncResult cookie = method.BeginInvoke("test", null, null);
  //
  // ... 此時可以同步處理其他事情
  //
  int result = method.EndInvoke(cookie);
  Console.WriteLine("String length is: " + result);        

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

使用回調函數獲取返回值

static void Main()
{
  Func<string, int> method = Work;
  method.BeginInvoke ("test", Done, null);
  // ...
  //並行其他事情
}
 
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);
}

EndInvoke做了三件事情:

  1. 等待委托異步的結束。
  2. 獲取返回值。
  3. 拋出未處理異常給調用線程。

Task

Task 泛型允許有返回值。

如:

static void Main()
{
  // 創建Task並執行
  Task<string> task = Task.Factory.StartNew<string>
    ( () => DownloadString ("http://www.baidu.com") ); 
  // 同時執行其他方法
  Console.WriteLine("begin");
  //等待獲取返回值,並且不會阻塞主線程
  Console.WriteLine(task.Result);
  Console.WriteLine("end");
} 
static string DownloadString (string uri)
{
  using (var wc = new System.Net.WebClient())
    return wc.DownloadString (uri);
}


參考:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM