亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

C#多線程用法詳解

 更新時(shí)間:2021年12月18日 15:30:09   作者:.NET開發(fā)菜鳥  
本文詳細(xì)講解了C#多線程用法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下

一、基本概念

1、進(jìn)程

首先打開任務(wù)管理器,查看當(dāng)前運(yùn)行的進(jìn)程:

從任務(wù)管理器里面可以看到當(dāng)前所有正在運(yùn)行的進(jìn)程。那么究竟什么是進(jìn)程呢?

進(jìn)程(Process)是Windows系統(tǒng)中的一個(gè)基本概念,它包含著一個(gè)運(yùn)行程序所需要的資源。一個(gè)正在運(yùn)行的應(yīng)用程序在操作系統(tǒng)中被視為一個(gè)進(jìn)程,進(jìn)程可以包括一個(gè)或多個(gè)線程。線程是操作系統(tǒng)分配處理器時(shí)間的基本單元,在進(jìn)程中可以有多個(gè)線程同時(shí)執(zhí)行代碼。進(jìn)程之間是相對獨(dú)立的,一個(gè)進(jìn)程無法訪問另一個(gè)進(jìn)程的數(shù)據(jù)(除非利用分布式計(jì)算方式),一個(gè)進(jìn)程運(yùn)行的失敗也不會影響其他進(jìn)程的運(yùn)行,Windows系統(tǒng)就是利用進(jìn)程把工作劃分為多個(gè)獨(dú)立的區(qū)域的。進(jìn)程可以理解為一個(gè)程序的基本邊界。是應(yīng)用程序的一個(gè)運(yùn)行例程,是應(yīng)用程序的一次動態(tài)執(zhí)行過程。

2、線程

在任務(wù)管理器里面查詢當(dāng)前總共運(yùn)行的線程數(shù):

線程(Thread)是進(jìn)程中的基本執(zhí)行單元,是操作系統(tǒng)分配CPU時(shí)間的基本單位,一個(gè)進(jìn)程可以包含若干個(gè)線程,在進(jìn)程入口執(zhí)行的第一個(gè)線程被視為這個(gè)進(jìn)程的主線程。在.NET應(yīng)用程序中,都是以Main()方法作為入口的,當(dāng)調(diào)用此方法時(shí)系統(tǒng)就會自動創(chuàng)建一個(gè)主線程。線程主要是由CPU寄存器、調(diào)用棧和線程本地存儲器(Thread Local Storage,TLS)組成的。CPU寄存器主要記錄當(dāng)前所執(zhí)行線程的狀態(tài),調(diào)用棧主要用于維護(hù)線程所調(diào)用到的內(nèi)存與數(shù)據(jù),TLS主要用于存放線程的狀態(tài)信息。

二、多線程

多線程的優(yōu)點(diǎn):可以同時(shí)完成多個(gè)任務(wù);可以使程序的響應(yīng)速度更快;可以讓占用大量處理時(shí)間的任務(wù)或當(dāng)前沒有進(jìn)行處理的任務(wù)定期將處理時(shí)間讓給別的任務(wù);可以隨時(shí)停止任務(wù);可以設(shè)置每個(gè)任務(wù)的優(yōu)先級以優(yōu)化程序性能。

那么可能有人會問:為什么可以多線程執(zhí)行呢?總結(jié)起來有下面兩方面的原因:

1、CPU運(yùn)行速度太快,硬件處理速度跟不上,所以操作系統(tǒng)進(jìn)行分時(shí)間片管理。這樣,從宏觀角度來說是多線程并發(fā)的,因?yàn)镃PU速度太快,察覺不到,看起來是同一時(shí)刻執(zhí)行了不同的操作。但是從微觀角度來講,同一時(shí)刻只能有一個(gè)線程在處理。

2、目前電腦都是多核多CPU的,一個(gè)CPU在同一時(shí)刻只能運(yùn)行一個(gè)線程,但是多個(gè)CPU在同一時(shí)刻就可以運(yùn)行多個(gè)線程。

然而,多線程雖然有很多優(yōu)點(diǎn),但是也必須認(rèn)識到多線程可能存在影響系統(tǒng)性能的不利方面,才能正確使用線程。不利方面主要有如下幾點(diǎn):

  • (1)線程也是程序,所以線程需要占用內(nèi)存,線程越多,占用內(nèi)存也越多。
  • (2)多線程需要協(xié)調(diào)和管理,所以需要占用CPU時(shí)間以便跟蹤線程。
  • (3)線程之間對共享資源的訪問會相互影響,必須解決爭用共享資源的問題。
  • (4)線程太多會導(dǎo)致控制太復(fù)雜,最終可能造成很多程序缺陷。

當(dāng)啟動一個(gè)可執(zhí)行程序時(shí),將創(chuàng)建一個(gè)主線程。在默認(rèn)的情況下,C#程序具有一個(gè)線程,此線程執(zhí)行程序中以Main方法開始和結(jié)束的代碼,Main()方法直接或間接執(zhí)行的每一個(gè)命令都有默認(rèn)線程(主線程)執(zhí)行,當(dāng)Main()方法返回時(shí)此線程也將終止。

一個(gè)進(jìn)程可以創(chuàng)建一個(gè)或多個(gè)線程以執(zhí)行與該進(jìn)程關(guān)聯(lián)的部分程序代碼。在C#中,線程是使用Thread類處理的,該類在System.Threading命名空間中。使用Thread類創(chuàng)建線程時(shí),只需要提供線程入口,線程入口告訴程序讓這個(gè)線程做什么。通過實(shí)例化一個(gè)Thread類的對象就可以創(chuàng)建一個(gè)線程。創(chuàng)建新的Thread對象時(shí),將創(chuàng)建新的托管線程。Thread類接收一個(gè)ThreadStart委托或ParameterizedThreadStart委托的構(gòu)造函數(shù),該委托包裝了調(diào)用Start方法時(shí)由新線程調(diào)用的方法,示例代碼如下:

Thread thread=new Thread(new ThreadStart(method));//創(chuàng)建線程
thread.Start(); //啟動線程

上面代碼實(shí)例化了一個(gè)Thread對象,并指明將要調(diào)用的方法method(),然后啟動線程。ThreadStart委托中作為參數(shù)的方法不需要參數(shù),并且沒有返回值。ParameterizedThreadStart委托一個(gè)對象作為參數(shù),利用這個(gè)參數(shù)可以很方便地向線程傳遞參數(shù),示例代碼如下:

Thread thread=new Thread(new ParameterizedThreadStart(method));//創(chuàng)建線程

thread.Start(3); //啟動線程

創(chuàng)建多線程的步驟:

  • 1、編寫線程所要執(zhí)行的方法
  • 2、實(shí)例化Thread類,并傳入一個(gè)指向線程所要執(zhí)行方法的委托。(這時(shí)線程已經(jīng)產(chǎn)生,但還沒有運(yùn)行)
  • 3、調(diào)用Thread實(shí)例的Start方法,標(biāo)記該線程可以被CPU執(zhí)行了,但具體執(zhí)行時(shí)間由CPU決定

2.1 System.Threading.Thread類

Thread類是是控制線程的基礎(chǔ)類,位于System.Threading命名空間下,具有4個(gè)重載的構(gòu)造函數(shù):

名稱 說明
Thread(ParameterizedThreadStart) 初始化 Thread 類的新實(shí)例,指定允許對象在線程啟動時(shí)傳遞給線程的委托。要執(zhí)行的方法是有參的。
Thread(ParameterizedThreadStart,?Int32) 初始化 Thread 類的新實(shí)例,指定允許對象在線程啟動時(shí)傳遞給線程的委托,并指定線程的最大堆棧大小
Thread(ThreadStart) 初始化 Thread 類的新實(shí)例。要執(zhí)行的方法是無參的。
Thread(ThreadStart,?Int32) 初始化 Thread 類的新實(shí)例,指定線程的最大堆棧大小。

ThreadStart是一個(gè)無參的、返回值為void的委托。委托定義如下:

public delegate void ThreadStart()

通過ThreadStart委托創(chuàng)建并運(yùn)行一個(gè)線程:

class Program
    {
        static void Main(string[] args)
        {
            //創(chuàng)建無參的線程
            Thread thread1 = new Thread(new ThreadStart(Thread1));
            //調(diào)用Start方法執(zhí)行線程
            thread1.Start();

            Console.ReadKey();
        }

        /// <summary>
        /// 創(chuàng)建無參的方法
        /// </summary>
        static void Thread1()
        {
            Console.WriteLine("這是無參的方法");
        }
    }

運(yùn)行結(jié)果

除了可以運(yùn)行靜態(tài)的方法,還可以運(yùn)行實(shí)例方法

class Program
    {
        static void Main(string[] args)
        {
            //創(chuàng)建ThreadTest類的一個(gè)實(shí)例
            ThreadTest test=new ThreadTest();
            //調(diào)用test實(shí)例的MyThread方法
            Thread thread = new Thread(new ThreadStart(test.MyThread));
            //啟動線程
            thread.Start();
            Console.ReadKey();
        }
    }

    class ThreadTest
    {
        public void MyThread()
        {
            Console.WriteLine("這是一個(gè)實(shí)例方法");
        }
    }

運(yùn)行結(jié)果:

如果為了簡單,也可以通過匿名委托或Lambda表達(dá)式來為Thread的構(gòu)造方法賦值

static void Main(string[] args)
 {
       //通過匿名委托創(chuàng)建
       Thread thread1 = new Thread(delegate() { Console.WriteLine("我是通過匿名委托創(chuàng)建的線程"); });
       thread1.Start();
       //通過Lambda表達(dá)式創(chuàng)建
       Thread thread2 = new Thread(() => Console.WriteLine("我是通過Lambda表達(dá)式創(chuàng)建的委托"));
       thread2.Start();
       Console.ReadKey();
 }

運(yùn)行結(jié)果:

ParameterizedThreadStart是一個(gè)有參的、返回值為void的委托,定義如下:

public delegate void ParameterizedThreadStart(Object obj)

class Program
    {
        static void Main(string[] args)
        {
            //通過ParameterizedThreadStart創(chuàng)建線程
            Thread thread = new Thread(new ParameterizedThreadStart(Thread1));
            //給方法傳值
            thread.Start("這是一個(gè)有參數(shù)的委托");
            Console.ReadKey();
        }

        /// <summary>
        /// 創(chuàng)建有參的方法
        /// 注意:方法里面的參數(shù)類型必須是Object類型
        /// </summary>
        /// <param name="obj"></param>
        static void Thread1(object obj)
        {
            Console.WriteLine(obj);
        }
    }

注意:ParameterizedThreadStart委托的參數(shù)類型必須是Object的。如果使用的是不帶參數(shù)的委托,不能使用帶參數(shù)的Start方法運(yùn)行線程,否則系統(tǒng)會拋出異常。但使用帶參數(shù)的委托,可以使用thread.Start()來運(yùn)行線程,這時(shí)所傳遞的參數(shù)值為null。

2.2線程的常用屬性

屬性名稱 說明
CurrentContext 獲取線程正在其中執(zhí)行的當(dāng)前上下文。
CurrentThread 獲取當(dāng)前正在運(yùn)行的線程。
ExecutionContext 獲取一個(gè) ExecutionContext 對象,該對象包含有關(guān)當(dāng)前線程的各種上下文的信息。
IsAlive 獲取一個(gè)值,該值指示當(dāng)前線程的執(zhí)行狀態(tài)。
IsBackground 獲取或設(shè)置一個(gè)值,該值指示某個(gè)線程是否為后臺線程。
IsThreadPoolThread 獲取一個(gè)值,該值指示線程是否屬于托管線程池。
ManagedThreadId 獲取當(dāng)前托管線程的唯一標(biāo)識符。
Name 獲取或設(shè)置線程的名稱。
Priority 獲取或設(shè)置一個(gè)值,該值指示線程的調(diào)度優(yōu)先級。
ThreadState 獲取一個(gè)值,該值包含當(dāng)前線程的狀態(tài)。

2.2.1 線程的標(biāo)識符

ManagedThreadId是確認(rèn)線程的唯一標(biāo)識符,程序在大部分情況下都是通過Thread.ManagedThreadId來辨別線程的。而Name是一個(gè)可變值,在默認(rèn)時(shí)候,Name為一個(gè)空值 Null,開發(fā)人員可以通過程序設(shè)置線程的名稱,但這只是一個(gè)輔助功能。

2.2.2 線程的優(yōu)先級別

當(dāng)線程之間爭奪CPU時(shí)間時(shí),CPU按照線程的優(yōu)先級給予服務(wù)。高優(yōu)先級的線程可以完全阻止低優(yōu)先級的線程執(zhí)行。.NET為線程設(shè)置了Priority屬性來定義線程執(zhí)行的優(yōu)先級別,里面包含5個(gè)選項(xiàng),其中Normal是默認(rèn)值。除非系統(tǒng)有特殊要求,否則不應(yīng)該隨便設(shè)置線程的優(yōu)先級別。

成員名稱 說明
Lowest 可以將 Thread 安排在具有任何其他優(yōu)先級的線程之后。
BelowNormal 可以將 Thread 安排在具有 Normal 優(yōu)先級的線程之后,在具有 Lowest 優(yōu)先級的線程之前。
Normal 默認(rèn)選擇??梢詫?Thread 安排在具有 AboveNormal 優(yōu)先級的線程之后,在具有 BelowNormal 優(yōu)先級的線程之前。
AboveNormal 可以將 Thread 安排在具有 Highest 優(yōu)先級的線程之后,在具有 Normal 優(yōu)先級的線程之前。
Highest 可以將 Thread 安排在具有任何其他優(yōu)先級的線程之前。

2.2.3 線程的狀態(tài)

通過ThreadState可以檢測線程是處于Unstarted、Sleeping、Running 等等狀態(tài),它比 IsAlive 屬性能提供更多的特定信息。

前面說過,一個(gè)應(yīng)用程序域中可能包括多個(gè)上下文,而通過CurrentContext可以獲取線程當(dāng)前的上下文。

CurrentThread是最常用的一個(gè)屬性,它是用于獲取當(dāng)前運(yùn)行的線程。

2.2.4 System.Threading.Thread的方法

Thread 中包括了多個(gè)方法來控制線程的創(chuàng)建、掛起、停止、銷毀,以后來的例子中會經(jīng)常使用。

方法名稱 說明
Abort() 終止本線程。
GetDomain() 返回當(dāng)前線程正在其中運(yùn)行的當(dāng)前域。
GetDomainId() 返回當(dāng)前線程正在其中運(yùn)行的當(dāng)前域Id。
Interrupt() 中斷處于 WaitSleepJoin 線程狀態(tài)的線程。
Join() 已重載。 阻塞調(diào)用線程,直到某個(gè)線程終止時(shí)為止。
Resume() 繼續(xù)運(yùn)行已掛起的線程。
Start() 執(zhí)行本線程。
Suspend() 掛起當(dāng)前線程,如果當(dāng)前線程已屬于掛起狀態(tài)則此不起作用
Sleep() 把正在運(yùn)行的線程掛起一段時(shí)間。

線程示例

static void Main(string[] args)
        {
            //獲取正在運(yùn)行的線程
            Thread thread = Thread.CurrentThread;
            //設(shè)置線程的名字
            thread.Name = "主線程";
            //獲取當(dāng)前線程的唯一標(biāo)識符
            int id = thread.ManagedThreadId;
            //獲取當(dāng)前線程的狀態(tài)
            ThreadState state= thread.ThreadState;
            //獲取當(dāng)前線程的優(yōu)先級
            ThreadPriority priority= thread.Priority;
            string strMsg = string.Format("Thread ID:{0}\n" + "Thread Name:{1}\n" +
                "Thread State:{2}\n" + "Thread Priority:{3}\n", id, thread.Name,
                state, priority);

            Console.WriteLine(strMsg);

            Console.ReadKey();
        }

運(yùn)行結(jié)果:

2.3 前臺線程和后臺線程

前臺線程:只有所有的前臺線程都結(jié)束,應(yīng)用程序才能結(jié)束。默認(rèn)情況下創(chuàng)建的線程都是前臺線程。

后臺線程:只要所有的前臺線程結(jié)束,后臺線程自動結(jié)束。通過Thread.IsBackground設(shè)置后臺線程。必須在調(diào)用Start方法之前設(shè)置線程的類型,否則一旦線程運(yùn)行,將無法改變其類型。

通過BeginXXX方法運(yùn)行的線程都是后臺線程。

class Program
    {
        static void Main(string[] args)
        {
            //演示前臺、后臺線程
            BackGroundTest background = new BackGroundTest(10);
            //創(chuàng)建前臺線程
            Thread fThread = new Thread(new ThreadStart(background.RunLoop));
            //給線程命名
            fThread.Name = "前臺線程";


            BackGroundTest background1 = new BackGroundTest(20);
            //創(chuàng)建后臺線程
            Thread bThread = new Thread(new ThreadStart(background1.RunLoop));
            bThread.Name = "后臺線程";
            //設(shè)置為后臺線程
            bThread.IsBackground = true;

            //啟動線程
            fThread.Start();
            bThread.Start();
        }
    }

    class BackGroundTest
    {
        private int Count;
        public BackGroundTest(int count)
        {
            this.Count = count;
        }
        public void RunLoop()
        {
            //獲取當(dāng)前線程的名稱
            string threadName = Thread.CurrentThread.Name;
            for (int i = 0; i < Count; i++)
            {
                Console.WriteLine("{0}計(jì)數(shù):{1}",threadName,i.ToString());
                //線程休眠500毫秒
                Thread.Sleep(1000);
            }
            Console.WriteLine("{0}完成計(jì)數(shù)",threadName);

        }
    }

運(yùn)行結(jié)果:前臺線程執(zhí)行完,后臺線程未執(zhí)行完,程序自動結(jié)束。

bThread.IsBackground = true注釋掉,運(yùn)行結(jié)果:主線程執(zhí)行完畢后(Main函數(shù)),程序并未結(jié)束,而是要等所有的前臺線程結(jié)束以后才會結(jié)束。

后臺線程一般用于處理不重要的事情,應(yīng)用程序結(jié)束時(shí),后臺線程是否執(zhí)行完成對整個(gè)應(yīng)用程序沒有影響。如果要執(zhí)行的事情很重要,需要將線程設(shè)置為前臺線程。

2.4 線程同步

所謂同步:是指在某一時(shí)刻只有一個(gè)線程可以訪問變量。

如果不能確保對變量的訪問是同步的,就會產(chǎn)生錯誤。

c#為同步訪問變量提供了一個(gè)非常簡單的方式,即使用c#語言的關(guān)鍵字Lock,它可以把一段代碼定義為互斥段,互斥段在一個(gè)時(shí)刻內(nèi)只允許一個(gè)線程進(jìn)入執(zhí)行,而其他線程必須等待。在c#中,關(guān)鍵字Lock定義如下:

Lock(expression)
{
statement_block
}

expression代表你希望跟蹤的對象:

如果你想保護(hù)一個(gè)類的實(shí)例,一般地,你可以使用this;

如果你想保護(hù)一個(gè)靜態(tài)變量(如互斥代碼段在一個(gè)靜態(tài)方法內(nèi)部),一般使用類名就可以了

而statement_block就算互斥段的代碼,這段代碼在一個(gè)時(shí)刻內(nèi)只可能被一個(gè)線程執(zhí)行。

以書店賣書為例

class Program
    {
        static void Main(string[] args)
        {
            BookShop book = new BookShop();
            //創(chuàng)建兩個(gè)線程同時(shí)訪問Sale方法
            Thread t1 = new Thread(new ThreadStart(book.Sale));
            Thread t2 = new Thread(new ThreadStart(book.Sale));
            //啟動線程
            t1.Start();
            t2.Start();
            Console.ReadKey();
        }
    }



    class BookShop
    {
        //剩余圖書數(shù)量
        public int num = 1;
        public void Sale()
        {
            int tmp = num;
            if (tmp > 0)//判斷是否有書,如果有就可以賣
            {
                Thread.Sleep(1000);
                num -= 1;
                Console.WriteLine("售出一本圖書,還剩余{0}本", num);
            }
            else
            {
                Console.WriteLine("沒有了");
            }
        }
    }

運(yùn)行結(jié)果:

從運(yùn)行結(jié)果可以看出,兩個(gè)線程同步訪問共享資源,沒有考慮同步的問題,結(jié)果不正確。

考慮線程同步,改進(jìn)后的代碼:

class Program
    {
        static void Main(string[] args)
        {
            BookShop book = new BookShop();
            //創(chuàng)建兩個(gè)線程同時(shí)訪問Sale方法
            Thread t1 = new Thread(new ThreadStart(book.Sale));
            Thread t2 = new Thread(new ThreadStart(book.Sale));
            //啟動線程
            t1.Start();
            t2.Start();
            Console.ReadKey();
        }
    }



    class BookShop
    {
        //剩余圖書數(shù)量
        public int num = 1;
        public void Sale()
        {
            //使用lock關(guān)鍵字解決線程同步問題
            lock (this)
            {
                int tmp = num;
                if (tmp > 0)//判斷是否有書,如果有就可以賣
                {
                    Thread.Sleep(1000);
                    num -= 1;
                    Console.WriteLine("售出一本圖書,還剩余{0}本", num);
                }
                else
                {
                    Console.WriteLine("沒有了");
                }
            }
        }
    }

運(yùn)行結(jié)果:

2.5 跨線程訪問

點(diǎn)擊“測試”,創(chuàng)建一個(gè)線程,從0循環(huán)到10000給文本框賦值,代碼如下:

private void btn_Test_Click(object sender, EventArgs e)
        {
            //創(chuàng)建一個(gè)線程去執(zhí)行這個(gè)方法:創(chuàng)建的線程默認(rèn)是前臺線程
            Thread thread = new Thread(new ThreadStart(Test));
            //Start方法標(biāo)記這個(gè)線程就緒了,可以隨時(shí)被執(zhí)行,具體什么時(shí)候執(zhí)行這個(gè)線程,由CPU決定
            //將線程設(shè)置為后臺線程
            thread.IsBackground = true;
            thread.Start();
        }

        private void Test()
        {
            for (int i = 0; i < 10000; i++)
            {
                this.textBox1.Text = i.ToString();
            }
        }

運(yùn)行結(jié)果:

產(chǎn)生錯誤的原因:textBox1是由主線程創(chuàng)建的,thread線程是另外創(chuàng)建的一個(gè)線程,在.NET上執(zhí)行的是托管代碼,C#強(qiáng)制要求這些代碼必須是線程安全的,即不允許跨線程訪問Windows窗體的控件。

解決方案:

1、在窗體的加載事件中,將C#內(nèi)置控件(Control)類的CheckForIllegalCrossThreadCalls屬性設(shè)置為false,屏蔽掉C#編譯器對跨線程調(diào)用的檢查。

 private void Form1_Load(object sender, EventArgs e)
 {
        //取消跨線程的訪問
        Control.CheckForIllegalCrossThreadCalls = false;
 }

使用上述的方法雖然可以保證程序正常運(yùn)行并實(shí)現(xiàn)應(yīng)用的功能,但是在實(shí)際的軟件開發(fā)中,做如此設(shè)置是不安全的(不符合.NET的安全規(guī)范),在產(chǎn)品軟件的開發(fā)中,此類情況是不允許的。如果要在遵守.NET安全標(biāo)準(zhǔn)的前提下,實(shí)現(xiàn)從一個(gè)線程成功地訪問另一個(gè)線程創(chuàng)建的空間,要使用C#的方法回調(diào)機(jī)制。

2、使用回調(diào)函數(shù)

回調(diào)實(shí)現(xiàn)的一般過程:

C#的方法回調(diào)機(jī)制,也是建立在委托基礎(chǔ)上的,下面給出它的典型實(shí)現(xiàn)過程。

(1)、定義、聲明回調(diào)。

//定義回調(diào)
private delegate void DoSomeCallBack(Type para);
//聲明回調(diào)
DoSomeCallBack doSomaCallBack;

可以看出,這里定義聲明的“回調(diào)”(doSomaCallBack)其實(shí)就是一個(gè)委托。

(2)、初始化回調(diào)方法。

doSomeCallBack=new DoSomeCallBack(DoSomeMethod);

所謂“初始化回調(diào)方法”實(shí)際上就是實(shí)例化剛剛定義了的委托,這里作為參數(shù)的DoSomeMethod稱為“回調(diào)方法”,它封裝了對另一個(gè)線程中目標(biāo)對象(窗體控件或其他類)的操作代碼。

(3)、觸發(fā)對象動作

Opt obj.Invoke(doSomeCallBack,arg);

其中Opt obj為目標(biāo)操作對象,在此假設(shè)它是某控件,故調(diào)用其Invoke方法。Invoke方法簽名為:

object Control.Invoke(Delegate method,params object[] args);

它的第一個(gè)參數(shù)為委托類型,可見“觸發(fā)對象動作”的本質(zhì),就是把委托doSomeCallBack作為參數(shù)傳遞給控件的Invoke方法,這與委托的使用方式是一模一樣的。

最終作用于對象Opt obj的代碼是置于回調(diào)方法體DoSomeMethod()中的,如下所示:

private void DoSomeMethod(type para)
{
//方法體
Opt obj.someMethod(para);
}

如果不用回調(diào),而是直接在程序中使用“Opt obj.someMethod(para);”,則當(dāng)對象Opt obj不在本線程(跨線程訪問)時(shí)就會發(fā)生上面所示的錯誤。

從以上回調(diào)實(shí)現(xiàn)的一般過程可知:C#的回調(diào)機(jī)制,實(shí)質(zhì)上是委托的一種應(yīng)用。在C#網(wǎng)絡(luò)編程中,回調(diào)的應(yīng)用是非常普遍的,有了方法回調(diào),就可以在.NET上寫出線程安全的代碼了。

使用方法回調(diào),實(shí)現(xiàn)給文本框賦值:

namespace MultiThreadDemo
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        //定義回調(diào)
        private delegate void setTextValueCallBack(int value);
        //聲明回調(diào)
        private setTextValueCallBack setCallBack;

        private void btn_Test_Click(object sender, EventArgs e)
        {
            //實(shí)例化回調(diào)
            setCallBack = new setTextValueCallBack(SetValue);
            //創(chuàng)建一個(gè)線程去執(zhí)行這個(gè)方法:創(chuàng)建的線程默認(rèn)是前臺線程
            Thread thread = new Thread(new ThreadStart(Test));
            //Start方法標(biāo)記這個(gè)線程就緒了,可以隨時(shí)被執(zhí)行,具體什么時(shí)候執(zhí)行這個(gè)線程,由CPU決定
            //將線程設(shè)置為后臺線程
            thread.IsBackground = true;
            thread.Start();
        }

        private void Test()
        {
            for (int i = 0; i < 10000; i++)
            {
                //使用回調(diào)
                textBox1.Invoke(setCallBack, i);
            }
        }

        /// <summary>
        /// 定義回調(diào)使用的方法
        /// </summary>
        /// <param name="value"></param>
        private void SetValue(int value)
        {
            this.textBox1.Text = value.ToString();
        }
    }
}

2.6 終止線程

若想終止正在運(yùn)行的線程,可以使用Abort()方法。

三、同步和異步

同步和異步是對方法執(zhí)行順序的描述。

同步:等待上一行完成計(jì)算之后,才會進(jìn)入下一行。

例如:請同事吃飯,同事說很忙,然后就等著同事忙完,然后一起去吃飯。

異步:不會等待方法的完成,會直接進(jìn)入下一行,是非阻塞的。

例如:請同事吃飯,同事說很忙,那同事先忙,自己去吃飯,同事忙完了他自己去吃飯。

下面通過一個(gè)例子講解同步和異步的區(qū)別

1、新建一個(gè)winform程序,上面有兩個(gè)按鈕,一個(gè)同步方法、一個(gè)異步方法,在屬性里面把輸出類型改成控制臺應(yīng)用程序,這樣可以看到輸出結(jié)果,代碼如下:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace MyAsyncThreadDemo
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 異步方法
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnAsync_Click(object sender, EventArgs e)
        {
            Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}");
            Action<string> action = this.DoSomethingLong;
            // 調(diào)用委托(同步調(diào)用)
            action.Invoke("btnAsync_Click_1");
            // 異步調(diào)用委托
            action.BeginInvoke("btnAsync_Click_2",null,null);
            Console.WriteLine($"***************btnAsync_Click End    {Thread.CurrentThread.ManagedThreadId}");
        }

        /// <summary>
        /// 同步方法
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSync_Click(object sender, EventArgs e)
        {
            Console.WriteLine($"****************btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
            int j = 3;
            int k = 5;
            int m = j + k;
            for (int i = 0; i < 5; i++)
            {
                string name = string.Format($"btnSync_Click_{i}");
                this.DoSomethingLong(name);
            }
        }


        private void DoSomethingLong(string name)
        {
            Console.WriteLine($"****************DoSomethingLong {name} Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
            long lResult = 0;
            for (int i = 0; i < 1000000000; i++)
            {
                lResult += i;
            }
            Console.WriteLine($"****************DoSomethingLong {name}   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
        }
    }
}

2、啟動程序,點(diǎn)擊同步,結(jié)果如下:

從上面的截圖中能夠很清晰的看出:同步方法是等待上一行代碼執(zhí)行完畢之后才會執(zhí)行下一行代碼。

點(diǎn)擊異步,結(jié)果如下:

從上面的截圖中看出:當(dāng)執(zhí)行到action.BeginInvoke("btnAsync_Click_2",null,null);這句代碼的時(shí)候,程序并沒有等待這段代碼執(zhí)行完就執(zhí)行了下面的End,沒有阻塞程序的執(zhí)行。

在剛才的測試中,如果點(diǎn)擊同步,這時(shí)winform界面不能拖到,界面卡住了,是因?yàn)橹骶€程(即UI線程)在忙于計(jì)算。

點(diǎn)擊異步的時(shí)候,界面不會卡住,這是因?yàn)橹骶€程已經(jīng)結(jié)束,計(jì)算任務(wù)交給子線程去做。

在仔細(xì)檢查上面兩個(gè)截圖,可以看出異步的執(zhí)行速度比同步執(zhí)行速度要快。同步方法執(zhí)行完將近16秒,異步方法執(zhí)行完將近6秒。

在看下面的一個(gè)例子,修改異步的方法,也和同步方法一樣執(zhí)行循環(huán),修改后的代碼如下:

private void btnAsync_Click(object sender, EventArgs e)
{
      Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}");
      //Action<string> action = this.DoSomethingLong;
      //// 調(diào)用委托(同步調(diào)用)
      //action.Invoke("btnAsync_Click_1");
      //// 異步調(diào)用委托
      //action.BeginInvoke("btnAsync_Click_2",null,null);
      Action<string> action = this.DoSomethingLong;
      for (int i = 0; i < 5; i++)
      {
           //Thread.Sleep(5);
           string name = string.Format($"btnAsync_Click_{i}");
           action.BeginInvoke(name, null, null);
      }
      Console.WriteLine($"***************btnAsync_Click End    {Thread.CurrentThread.ManagedThreadId}");
}

結(jié)果如下:

從截圖中能夠看出:同步方法執(zhí)行是有序的,異步方法執(zhí)行是無序的。異步方法無序包括啟動無序和結(jié)束無序。啟動無序是因?yàn)橥粫r(shí)刻向操作系統(tǒng)申請線程,操作系統(tǒng)收到申請以后,返回執(zhí)行的順序是無序的,所以啟動是無序的。結(jié)束無序是因?yàn)殡m然線程執(zhí)行的是同樣的操作,但是每個(gè)線程的耗時(shí)是不同的,所以結(jié)束的時(shí)候不一定是先啟動的線程就先結(jié)束。從上面同步方法中可以清晰的看出:btnSync_Click_0執(zhí)行時(shí)間耗時(shí)不到3秒,而btnSync_Click_1執(zhí)行時(shí)間耗時(shí)超過了3秒。可以想象體育比賽中的跑步,每位運(yùn)動員聽到發(fā)令槍起跑的順序不同,每位運(yùn)動員花費(fèi)的時(shí)間不同,最終到達(dá)終點(diǎn)的順序也不同。

總結(jié)一下同步方法和異步方法的區(qū)別:

  • 1、同步方法由于主線程忙于計(jì)算,所以會卡住界面。
  • 異步方法由于主線程執(zhí)行完了,其他計(jì)算任務(wù)交給子線程去執(zhí)行,所以不會卡住界面,用戶體驗(yàn)性好。
  • 2、同步方法由于只有一個(gè)線程在計(jì)算,所以執(zhí)行速度慢。
  • 異步方法由多個(gè)線程并發(fā)運(yùn)算,所以執(zhí)行速度快,但并不是線性增長的(資源可能不夠)。多線程也不是越多越好,只有多個(gè)獨(dú)立的任務(wù)同時(shí)運(yùn)行,才能加快速度。
  • 3、同步方法是有序的。
  • 異步多線程是無序的:啟動無序,執(zhí)行時(shí)間不確定,所以結(jié)束也是無序的。一定不要通過等待幾毫秒的形式來控制線程啟動/執(zhí)行時(shí)間/結(jié)束。

四、回調(diào)

先來看看異步多線程無序的例子:

在界面上新增一個(gè)按鈕,實(shí)現(xiàn)代碼如下:

private void btnAsyncAdvanced_Click(object sender, EventArgs e)
{
      Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
      Action<string> action = this.DoSomethingLong;
      action.BeginInvoke("btnAsyncAdvanced_Click", null, null);
      // 需求:異步多線程執(zhí)行完之后再打印出下面這句
      Console.WriteLine($"到這里計(jì)算已經(jīng)完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
      Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
}

運(yùn)行結(jié)果:

從上面的截圖中看出,最終的效果并不是我們想要的效果,而且打印輸出的還是主線程。

既然異步多線程是無序的,那我們有沒有什么辦法可以解決無序的問題呢?辦法當(dāng)然是有的,那就是使用回調(diào),.NET框架已經(jīng)幫我們實(shí)現(xiàn)了回調(diào):

BeginInvoke的第二個(gè)參數(shù)就是一個(gè)回調(diào),那么AsyncCallback究竟是什么呢?F12查看AsyncCallback的定義:

發(fā)現(xiàn)AsyncCallback就是一個(gè)委托,參數(shù)類型是IAsyncResult,明白了AsyncCallback是什么以后,將上面的代碼進(jìn)行如下的改造:

private void btnAsyncAdvanced_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    Action<string> action = this.DoSomethingLong;
    // 定義一個(gè)回調(diào)
    AsyncCallback callback = p =>
    {
       Console.WriteLine($"到這里計(jì)算已經(jīng)完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
    };
    // 回調(diào)作為參數(shù)
    action.BeginInvoke("btnAsyncAdvanced_Click", callback, null);
    Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
 }

運(yùn)行結(jié)果:

上面的截圖中可以看出,這就是我們想要的效果,而且打印是子線程輸出的,但是程序究竟是怎么實(shí)現(xiàn)的呢?我們可以進(jìn)行如下的猜想:

程序執(zhí)行到BeginInvoke的時(shí)候,會申請一個(gè)基于線程池的線程,這個(gè)線程會完成委托的執(zhí)行(在這里就是執(zhí)行DoSomethingLong()方法),在委托執(zhí)行完以后,這個(gè)線程又會去執(zhí)行callback回調(diào)的委托,執(zhí)行callback委托需要一個(gè)IAsyncResult類型的參數(shù),這個(gè)IAsyncResult類型的參數(shù)是如何來的呢?鼠標(biāo)右鍵放到BeginInvoke上面,查看返回值:

發(fā)現(xiàn)BeginInvoke的返回值就是IAsyncResult類型的。那么這個(gè)返回值是不是就是callback委托的參數(shù)呢?將代碼進(jìn)行如下的修改:

private void btnAsyncAdvanced_Click(object sender, EventArgs e)
{
            // 需求:異步多線程執(zhí)行完之后再打印出下面這句
            Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
            Action<string> action = this.DoSomethingLong;
            // 無序的
            //action.BeginInvoke("btnAsyncAdvanced_Click", null, null);

            IAsyncResult asyncResult = null;
            // 定義一個(gè)回調(diào)
            AsyncCallback callback = p =>
            {
                // 比較兩個(gè)變量是否是同一個(gè)
                Console.WriteLine(object.ReferenceEquals(p,asyncResult));
                Console.WriteLine($"到這里計(jì)算已經(jīng)完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
            };
            // 回調(diào)作為參數(shù)
            asyncResult= action.BeginInvoke("btnAsyncAdvanced_Click", callback, null);
            Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
}

結(jié)果:

這里可以看出BeginInvoke的返回值就是callback委托的參數(shù)。

現(xiàn)在我們可以使用回調(diào)解決異步多線程無序的問題了。

2、獲取委托異步調(diào)用的返回值

使用EndInvoke可以獲取委托異步調(diào)用的返回值,請看下面的例子:

private void btnAsyncReturnVlaue_Click(object sender, EventArgs e)
{
       // 定義一個(gè)無參數(shù)、int類型返回值的委托
       Func<int> func = () =>
       {
             Thread.Sleep(2000);
             return DateTime.Now.Day;
       };
       // 輸出委托同步調(diào)用的返回值
       Console.WriteLine($"func.Invoke()={func.Invoke()}");
       // 委托的異步調(diào)用
       IAsyncResult asyncResult = func.BeginInvoke(p =>
       {
            Console.WriteLine(p.AsyncState);
       },"異步調(diào)用返回值");
       // 輸出委托異步調(diào)用的返回值
       Console.WriteLine($"func.EndInvoke(asyncResult)={func.EndInvoke(asyncResult)}");
}

結(jié)果:

以上所述是小編給大家介紹的C#多線程用法詳解,希望對大家有所幫助。在此也非常感謝大家對腳本之家網(wǎng)站的支持!

相關(guān)文章

  • C#新特性之可空引用類型

    C#新特性之可空引用類型

    本文詳細(xì)講解了C#新特性之可空引用類型,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-02-02
  • C#中的Dialog對話框

    C#中的Dialog對話框

    這篇文章介紹了C#中的Dialog對話框,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-05-05
  • 常用類之TCP連接類-socket編程

    常用類之TCP連接類-socket編程

    常用類之TCP連接類-socket編程...
    2007-03-03
  • C# 三種序列化方法分享

    C# 三種序列化方法分享

    這篇文章主要介紹了C# 三種序列化方法,需要的朋友可以參考下
    2014-02-02
  • WPF實(shí)現(xiàn)窗體中的懸浮按鈕

    WPF實(shí)現(xiàn)窗體中的懸浮按鈕

    這篇文章主要為大家詳細(xì)介紹了WPF實(shí)現(xiàn)窗體中的懸浮按鈕,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-11-11
  • c# 如何用組合替代繼承

    c# 如何用組合替代繼承

    這篇文章主要介紹了c# 如何用組合替代繼承,幫助大家更好的理解和學(xué)習(xí)使用c#,感興趣的朋友可以了解下
    2021-02-02
  • C#中Timer使用及解決重入問題

    C#中Timer使用及解決重入問題

    本文主要介紹了C#中Timer使用及解決重入問題的相關(guān)知識。具有很好的參考價(jià)值,下面跟著小編一起來看下吧
    2017-02-02
  • C#實(shí)現(xiàn)判斷當(dāng)前操作用戶管理角色的方法

    C#實(shí)現(xiàn)判斷當(dāng)前操作用戶管理角色的方法

    這篇文章主要介紹了C#實(shí)現(xiàn)判斷當(dāng)前操作用戶管理角色的方法,涉及C#針對系統(tǒng)用戶判斷的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-08-08
  • C#調(diào)用Python的URL接口的示例

    C#調(diào)用Python的URL接口的示例

    這篇文章主要介紹了C#調(diào)用Python的URL接口的示例,幫助大家更好的理解和學(xué)習(xí)c#,感興趣的朋友可以了解下
    2020-10-10
  • C#實(shí)現(xiàn)數(shù)據(jù)導(dǎo)出任一Word圖表的通用呈現(xiàn)方法

    C#實(shí)現(xiàn)數(shù)據(jù)導(dǎo)出任一Word圖表的通用呈現(xiàn)方法

    應(yīng)人才測評產(chǎn)品的需求,導(dǎo)出測評報(bào)告是其中一個(gè)重要的環(huán)節(jié),報(bào)告的文件類型也多種多樣,其中WORD輸出也扮演了一個(gè)重要的角色,本文給大家介紹了C#實(shí)現(xiàn)數(shù)據(jù)導(dǎo)出任一Word圖表的通用呈現(xiàn)方法及一些體會,需要的朋友可以參考下
    2023-10-10

最新評論