图片 10

你必须掌握的多线程编程,掌握多线程编程

Posted by

目录

你必须掌握的多线程编程,掌握多线程编程

1、多线程编程必备知识

    1.1 进程与线程的概念

       
 当我们打开一个应用程序后,操作系统就会为该应用程序分配一个进程ID,例如打开QQ,你将在任务管理器的进程选项卡看到QQ.exe进程,如下图:

         图片 1

         
进程可以理解为一块包含了某些资源的内存区域,操作系统通过进程这一方式把它的工作划分为不同的单元。一个应用程序可以对应于多个进程。

         
线程是进程中的独立执行单元,对于操作系统而言,它通过调度线程来使应用程序工作,一个进程中至少包含一个线程,我们把该线程成为主线程。线程与进程之间的关系可以理解为:线程是进程的执行单元,操作系统通过调度线程来使应用程序工作;而进程则是线程的容器,它由操作系统创建,又在具体的执行过程中创建了线程。

 

    1.2 线程的调度

       
 在操作系统的书中貌似有提过,“Windows是抢占式多线程操作系统”。之所以这么说它是抢占式的,是因为线程可以在任意时间里被抢占,来调度另一个线程。操作系统为每个线程分配了0-31中的某一级优先级,而且会把优先级高的线程优先分配给CPU执行。

         
Windows支持7个相对线程优先级:Idle、Lowest、BelowNormal、Normal、AboveNormal、Highest和Time-Critical。其中,Normal是默认的线程优先级。程序可以通过设置Thread的Priority属性来改变线程的优先级,该属性的类型为ThreadPriority枚举类型,其成员包括Lowest、BelowNormal、Normal、AboveNormal和Highest。CLR为自己保留了Idle和Time-Critical两个优先级。

 

    1.3 线程也分前后台

         
线程有前台线程和后台线程之分。在一个进程中,当所有前台线程停止运行后,CLR会强制结束所有仍在运行的后台线程,这些后台线程被直接终止,却不会抛出任何异常。主线程将一直是前台线程。我们可以使用Tread类来创建前台线程。

 1 using System;
 2 using System.Threading;
 3 
 4 namespace 多线程1
 5 {
 6     internal class Program
 7     {
 8         private static void Main(string[] args)
 9         {
10             var backThread = new Thread(Worker);
11             backThread.IsBackground = true;
12             backThread.Start();
13             Console.WriteLine("从主线程退出");
14             Console.ReadKey();
15         }
16 
17         private static void Worker()
18         {
19             Thread.Sleep(1000);
20             Console.WriteLine("从后台线程退出");
21         }
22     }
23 }

   
以上代码先通过Thread类创建了一个线程对象,然后通过设置IsBackground属性来指明该线程为后台线程。如果不设置这个属性,则默认为前台线程。接着调用了Start的方法,此时后台线程会执行Worker函数的代码。所以在这个程序中有两个线程,一个是运行Main函数的主线程,一个是运行Worker线程的后台线程。由于前台线程执行完毕后CLR会无条件地终止后台线程的运行,所以在前面的代码中,若启动了后台线程,则主线程将会继续运行。主线程执行完后,CLR发现主线程结束,会终止后台线程,然后使整个应用程序结束运行,所以Worker函数中的Console语句将不会执行。所以上面代码的结果是不会运行Worker函数中的Console语句的。

   
 可以使用Join函数的方法,确保主线程会在后台线程执行结束后才开始运行。

 1 using System;
 2 using System.Threading;
 3 
 4 namespace 多线程1
 5 {
 6     internal class Program
 7     {
 8         private static void Main(string[] args)
 9         {
10             var backThread = new Thread(Worker);
11             backThread.IsBackground = true;
12             backThread.Start();
13             backThread.Join();
14             Console.WriteLine("从主线程退出");
15             Console.ReadKey();
16         }
17 
18         private static void Worker()
19         {
20             Thread.Sleep(1000);
21             Console.WriteLine("从后台线程退出");
22         }
23     }
24 }

    以上代码调用Join函数来确保主线程会在后台线程结束后再运行。

    如果你线程执行的方法需要参数,则就需要使用new
Thread的重载构造函数Thread(ParameterizedThreadStart).

 1 using System;
 2 using System.Threading;
 3 
 4 namespace 多线程1
 5 {
 6     internal class Program
 7     {
 8         private static void Main(string[] args)
 9         {
10             var backThread = new Thread(new ParameterizedThreadStart(Worker));
11             backThread.IsBackground = true;
12             backThread.Start("Helius");
13             backThread.Join();
14             Console.WriteLine("从主线程退出");
15             Console.ReadKey();
16         }
17 
18         private static void Worker(object data)
19         {
20             Thread.Sleep(1000);
21             Console.WriteLine($"传入的参数为{data.ToString()}");
22         }
23     }
24 }

   
执行结果为:图片 2

 

2、线程的容器——线程池

   
前面我们都是通过Thead类来手动创建线程的,然而线程的创建和销毁会耗费大量时间,这样的手动操作将造成性能损失。因此,为了避免因通过Thread手动创建线程而造成的损失,.NET引入了线程池机制。

    2.1 线程池

       
 线程池是指用来存放应用程序中要使用的线程集合,可以将它理解为一个存放线程的地方,这种集中存放的方式有利于对线程进行管理。

       
 CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列,当应用程序想要执行一个异步操作时,需要调用QueueUserWorkItem方法来将对应的任务添加到线程池的请求队列中。线程池实现的代码会从队列中提取,并将其委派给线程池中的线程去执行。如果线程池没有空闲的线程,则线程池也会创建一个新线程去执行提取的任务。而当线程池线程完成某个任务时,线程不会被销毁,而是返回到线程池中,等待响应另一个请求。由于线程不会被销毁,所以也就避免了性能损失。记住,线程池里的线程都是后台线程,默认级别是Normal。

 

    2.2 通过线程池来实现多线程

         
要使用线程池的线程,需要调用静态方法ThreadPool.QueueUserWorkItem,以指定线程要调用的方法,该静态方法有两个重载版本:

          public static bool QueueUserWorkItem(WaitCallback callBack);

          public static bool QueueUserWorkItem(WaitCallback
callback,Object state)

         
这两个方法用于向线程池队列添加一个工作先以及一个可选的状态数据。然后,这两个方法就会立即返回。下面通过实例来演示如何使用线程池来实现多线程编程。

 1 using System;
 2 using System.Threading;
 3 
 4 namespace 多线程2
 5 {
 6     class Program
 7     {
 8         static void Main(string[] args)
 9         {
10             Console.WriteLine($"主线程ID={Thread.CurrentThread.ManagedThreadId}");
11             ThreadPool.QueueUserWorkItem(CallBackWorkItem);
12             ThreadPool.QueueUserWorkItem(CallBackWorkItem,"work");
13             Thread.Sleep(3000);
14             Console.WriteLine("主线程退出");
15             Console.ReadKey();
16         }
17 
18         private static void CallBackWorkItem(object state)
19         {
20             Console.WriteLine("线程池线程开始执行");
21             if (state != null)
22             {
23                 Console.WriteLine($"线程池线程ID={Thread.CurrentThread.ManagedThreadId},传入的参数为{state.ToString()}");
24             }
25             else
26             {
27                 Console.WriteLine($"线程池线程ID={Thread.CurrentThread.ManagedThreadId}");
28             }
29         }
30     }
31 }

结果为:图片 3

 

    2.3 协作式取消线程池线程

         .NET
Framework提供了取消操作的模式,这个模式是协作式的。为了取消一个操作,必须创建一个System.Threading.CancellationTokenSource对象。下面还是使用代码来演示一下:

using System;
using System.Threading;

namespace 多线程3
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Console.WriteLine("主线程运行");
            var cts = new CancellationTokenSource();
            ThreadPool.QueueUserWorkItem(Callback, cts.Token);
            Console.WriteLine("按下回车键来取消操作");
            Console.Read();
            cts.Cancel();
            Console.ReadKey();
        }

        private static void Callback(object state)
        {
            var token = (CancellationToken) state;
            Console.WriteLine("开始计数");
            Count(token, 1000);
        }

        private static void Count(CancellationToken token, int count)
        {
            for (var i = 0; i < count; i++)
            {
                if (token.IsCancellationRequested)
                {
                    Console.WriteLine("计数取消");
                    return;
                }
                Console.WriteLine($"计数为:{i}");
                Thread.Sleep(300);
            }
            Console.WriteLine("计数完成");
        }
    }
}

结果为:图片 4

 

3、线程同步

   
线程同步计数是指多线程程序中,为了保证后者线程,只有等待前者线程完成之后才能继续执行。这就好比生活中排队买票,在前面的人没买到票之前,后面的人必须等待。

    3.1 多线程程序中存在的隐患

         
多线程可能同时去访问一个共享资源,这将损坏资源中所保存的数据。这种情况下,只能采用线程同步技术。

    3.2 使用监视器对象实现线程同步

         
监视器对象(Monitor)能够确保线程拥有对共享资源的互斥访问权,C#通过lock关键字来提供简化的语法。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5 using System.Threading;
 6 using System.Threading.Tasks;
 7 
 8 namespace 线程同步
 9 {
10     class Program
11     {
12         private static int tickets = 100;
13         static object globalObj=new object();
14         static void Main(string[] args)
15         {
16             Thread thread1=new Thread(SaleTicketThread1);
17             Thread thread2=new Thread(SaleTicketThread2);
18             thread1.Start();
19             thread2.Start();
20             Console.ReadKey();
21         }
22 
23         private static void SaleTicketThread2()
24         {
25             while (true)
26             {
27                 try
28                 {
29                     Monitor.Enter(globalObj);
30                     Thread.Sleep(1);
31                     if (tickets > 0)
32                     {
33                         Console.WriteLine($"线程2出票:{tickets--}");
34                     }
35                     else
36                     {
37                         break;
38                     }
39                 }
40                 catch (Exception)
41                 {
42                     throw;
43                 }
44                 finally
45                 {
46                     Monitor.Exit(globalObj);
47                 }
48             }
49         }
50 
51         private static void SaleTicketThread1()
52         {
53             while (true)
54             {
55                 try
56                 {
57                     Monitor.Enter(globalObj);
58                     Thread.Sleep(1);
59                     if (tickets > 0)
60                     {
61                         Console.WriteLine($"线程1出票:{tickets--}");
62                     }
63                     else
64                     {
65                         break;
66                     }
67                 }
68                 catch (Exception)
69                 {
70                     throw;
71                 }
72                 finally
73                 {
74                     Monitor.Exit(globalObj);
75                 }
76             }
77         }
78     }
79 }

   
在以上代码中,首先额外定义了一个静态全局变量globalObj,并将其作为参数传递给Enter方法。使用了Monitor锁定的对象需要为引用类型,而不能为值类型。因为在将值类型传递给Enter时,它将被先装箱为一个单独的毒香,之后再传递给Enter方法;而在将变量传递给Exit方法时,也会创建一个单独的引用对象。此时,传递给Enter方法的对象和传递给Exit方法的对象不同,Monitor将会引发SynchronizationLockException异常。

  

    3.3 线程同步技术存在的问题

       
 (1)使用比较繁琐。要用额外的代码把多个线程同时访问的数据包围起来,还并不能遗漏。

       
 (2)使用线程同步会影响程序性能。因为获取和释放同步锁是需要时间的;并且决定那个线程先获得锁的时候,CPU也要进行协调。这些额外的工作都会对性能造成影响。

       
 (3)线程同步每次只允许一个线程访问资源,这会导致线程堵塞。继而系统会创建更多的线程,CPU也就要负担更繁重的调度工作。这个过程会对性能造成影响。

           下面就由代码来解释一下性能的差距:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Diagnostics;
 4 using System.Linq;
 5 using System.Text;
 6 using System.Threading;
 7 using System.Threading.Tasks;
 8 
 9 namespace 线程同步2
10 {
11     class Program
12     {
13         static void Main(string[] args)
14         {
15             int x = 0;
16             const int iterationNumber = 5000000;
17             Stopwatch stopwatch=Stopwatch.StartNew();
18             for (int i = 0; i < iterationNumber; i++)
19             {
20                 x++;
21             }
22             Console.WriteLine($"不使用锁的情况下花费的时间:{stopwatch.ElapsedMilliseconds}ms");
23             stopwatch.Restart();
24             for (int i = 0; i < iterationNumber; i++)
25             {
26                 Interlocked.Increment(ref x);
27             }
28             Console.WriteLine($"使用锁的情况下花费的时间:{stopwatch.ElapsedMilliseconds}ms");
29             Console.ReadKey();
30         }
31     }
32 }

   
执行结果:图片 5

    实践出结论。

1、多线程编程必备知识 1.1 进程与线程的概念
当我们打开一个应用程序后,操作系统就会为该…

  • C#多线程编程系列(二)-
    线程基础

    • 1.1
      简介
    • 1.2
      创建线程
    • 1.3
      暂停线程
    • 1.4
      线程等待
    • 1.5
      终止线程
    • 1.6
      检测线程状态
    • 1.7
      线程优先级
    • 1.8
      前台线程和后台线程
    • 1.9
      向线程传递参数
    • 1.10 C#
      Lock关键字的使用
    • 1.11
      使用Monitor类锁定资源
    • 1.12
      多线程中处理异常
  • 参考书籍
  • 笔者水平有限,如果错误欢迎各位批评指正!

C#多线程编程系列(二)- 线程基础


1.1 简介

线程基础主要包括线程创建、挂起、等待和终止线程。关于更多的线程的底层实现,CPU时间片轮转等等的知识,可以参考《深入理解计算机系统》一书中关于进程和线程的章节,本文不过多赘述。

1.2 创建线程

在C#语言中,创建线程是一件非常简单的事情;它只需要用到
System.Threading命名空间,其中主要使用Thread类来创建线程。

演示代码如下所示:

using System;
using System.Threading; // 创建线程需要用到的命名空间
namespace Recipe1
{
    class Program
    {
        static void Main(string[] args)
        {
            // 1.创建一个线程 PrintNumbers为该线程所需要执行的方法
            Thread t = new Thread(PrintNumbers);
            // 2.启动线程
            t.Start();

            // 主线程也运行PrintNumbers方法,方便对照
            PrintNumbers();
            // 暂停一下
            Console.ReadKey();
        }

        static void PrintNumbers()
        {
            // 使用Thread.CurrentThread.ManagedThreadId 可以获取当前运行线程的唯一标识,通过它来区别线程
            Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印...");
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i}");
            }
        }
    }
}

运行结果如下图所示,我们可以通过运行结果得知上面的代码创建了一个线程,然后主线程和创建的线程交叉输出结果,这说明PrintNumbers方法同时运行在主线程和另外一个线程中。

图片 6

1.3 暂停线程

暂停线程这里使用的方式是通过Thread.Sleep方法,如果线程执行Thread.Sleep方法,那么操作系统将在指定的时间内不为该线程分配任何时间片。如果Sleep时间100ms那么操作系统将至少让该线程睡眠100ms或者更长时间,所以Thread.Sleep方法不能作为高精度的计时器使用。

演示代码如下所示:

using System;
using System.Threading; // 创建线程需要用到的命名空间
namespace Recipe2
{
    class Program
    {
        static void Main(string[] args)
        {
            // 1.创建一个线程 PrintNumbers为该线程所需要执行的方法
            Thread t = new Thread(PrintNumbersWithDelay);
            // 2.启动线程
            t.Start();

            // 暂停一下
            Console.ReadKey();
        }

        static void PrintNumbersWithDelay()
        {
            Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印... 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}");
            for (int i = 0; i < 10; i++)
            {
                //3. 使用Thread.Sleep方法来使当前线程睡眠,TimeSpan.FromSeconds(2)表示时间为 2秒
                Thread.Sleep(TimeSpan.FromSeconds(2));
                Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}");
            }
        }
    }
}

运行结果如下图所示,通过下图可以确定上面的代码是有效的,通过Thread.Sleep方法,使线程休眠了2秒左右,但是并不是特别精确的2秒。验证了上面的说法,它的睡眠是至少让线程睡眠多长时间,而不是一定多长时间。

图片 7

1.4 线程等待

在本章中,线程等待使用的是Join方法,该方法将暂停执行当前线程,直到所等待的另一个线程终止。在简单的线程同步中会使用到,但它比较简单,不作过多介绍。

演示代码如下所示:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine($"-------开始执行 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------");

        // 1.创建一个线程 PrintNumbersWithDelay为该线程所需要执行的方法
        Thread t = new Thread(PrintNumbersWithDelay);
        // 2.启动线程
        t.Start();
        // 3.等待线程结束
        t.Join();

        Console.WriteLine($"-------执行完毕 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------");
        // 暂停一下
        Console.ReadKey();
    }

    static void PrintNumbersWithDelay()
    {
        Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印... 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}");
        for (int i = 0; i < 10; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}");
        }
    }
}

运行结果如下图所示,开始执行和执行完毕两条信息由主线程打印;根据其输出的顺序可见主线程是等待另外的线程结束后才输出执行完毕这条信息。

图片 8

1.5 终止线程

终止线程使用的方法是Abort方法,当该方法被执行时,将尝试销毁该线程。通过引发ThreadAbortException异常使线程被销毁。但一般不推荐使用该方法,原因有以下几点。

  1. 使用Abort方法只是尝试销毁该线程,但不一定能终止线程。
  2. 如果被终止的线程在执行lock内的代码,那么终止线程会造成线程不安全。
  3. 线程终止时,CLR会保证自己内部的数据结构不会损坏,但是BCL不能保证。

基于以上原因不推荐使用Abort方法,在实际项目中一般使用CancellationToken来终止线程。

演示代码如下所示:

static void Main(string[] args)
{
    Console.WriteLine($"-------开始执行 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------");

    // 1.创建一个线程 PrintNumbersWithDelay为该线程所需要执行的方法
    Thread t = new Thread(PrintNumbersWithDelay);
    // 2.启动线程
    t.Start();
    // 3.主线程休眠6秒
    Thread.Sleep(TimeSpan.FromSeconds(6));
    // 4.终止线程
    t.Abort();

    Console.WriteLine($"-------执行完毕 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}-------");
    // 暂停一下
    Console.ReadKey();
}

static void PrintNumbersWithDelay()
{
    Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 开始打印... 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}");
    for (int i = 0; i < 10; i++)
    {
        Thread.Sleep(TimeSpan.FromSeconds(2));
        Console.WriteLine($"线程:{Thread.CurrentThread.ManagedThreadId} 打印:{i} 现在时间{DateTime.Now.ToString("HH:mm:ss.ffff")}");
    }
}

运行结果如下图所示,启动所创建的线程3后,6秒钟主线程调用了Abort方法,线程3没有继续执行便结束了;与预期的结果一致。

图片 9

1.6 检测线程状态

线程的状态可通过访问ThreadState属性来检测,ThreadState是一个枚举类型,一共有10种状态,状态具体含义如下表所示。

成员名称 说明
Aborted 线程处于 Stopped 状态中。
AbortRequested 已对线程调用了 Thread.Abort 方法,但线程尚未收到试图终止它的挂起的 System.Threading.ThreadAbortException
Background 线程正作为后台线程执行(相对于前台线程而言)。此状态可以通过设置 Thread.IsBackground 属性来控制。
Running 线程已启动,它未被阻塞,并且没有挂起的 ThreadAbortException
Stopped 线程已停止。
StopRequested 正在请求线程停止。这仅用于内部。
Suspended 线程已挂起。
SuspendRequested 正在请求线程挂起。
Unstarted 尚未对线程调用 Thread.Start 方法。
WaitSleepJoin 由于调用 WaitSleepJoin,线程已被阻止。

下表列出导致状态更改的操作。

操作 ThreadState
在公共语言运行库中创建线程。 Unstarted
线程调用 Start Unstarted
线程开始运行。 Running
线程调用 Sleep WaitSleepJoin
线程对其他对象调用 Wait WaitSleepJoin
线程对其他线程调用 Join WaitSleepJoin
另一个线程调用 Interrupt Running
另一个线程调用 Suspend SuspendRequested
线程响应 Suspend 请求。 Suspended
另一个线程调用 Resume Running
另一个线程调用 Abort AbortRequested
线程响应 Abort 请求。 Stopped
线程被终止。 Stopped

演示代码如下所示:

static void Main(string[] args)
{
    Console.WriteLine("开始执行...");

    Thread t = new Thread(PrintNumbersWithStatus);
    Thread t2 = new Thread(DoNothing);

    // 使用ThreadState查看线程状态 此时线程未启动,应为Unstarted
    Console.WriteLine($"Check 1 :{t.ThreadState}");

    t2.Start();
    t.Start();

    // 线程启动, 状态应为 Running
    Console.WriteLine($"Check 2 :{t.ThreadState}");

    // 由于PrintNumberWithStatus方法开始执行,状态为Running
    // 但是经接着会执行Thread.Sleep方法 状态会转为 WaitSleepJoin
    for (int i = 1; i < 30; i++)
    {
        Console.WriteLine($"Check 3 : {t.ThreadState}");
    }

    // 延时一段时间,方便查看状态
    Thread.Sleep(TimeSpan.FromSeconds(6));

    // 终止线程
    t.Abort();

    Console.WriteLine("t线程被终止");

    // 由于该线程是被Abort方法终止 所以状态为 Aborted或AbortRequested
    Console.WriteLine($"Check 4 : {t.ThreadState}");
    // 该线程正常执行结束 所以状态为Stopped
    Console.WriteLine($"Check 5 : {t2.ThreadState}");

    Console.ReadKey();
}

static void DoNothing()
{
    Thread.Sleep(TimeSpan.FromSeconds(2));
}

static void PrintNumbersWithStatus()
{
    Console.WriteLine("t线程开始执行...");

    // 在线程内部,可通过Thread.CurrentThread拿到当前线程Thread对象
    Console.WriteLine($"Check 6 : {Thread.CurrentThread.ThreadState}");
    for (int i = 1; i < 10; i++)
    {
        Thread.Sleep(TimeSpan.FromSeconds(2));
        Console.WriteLine($"t线程输出 :{i}");
    }
}

运行结果如下图所示,与预期的结果一致。

图片 10

相关文章

Leave a Reply

电子邮件地址不会被公开。 必填项已用*标注