c#知识总结-线程基础

目录

简介

创建线程

暂停线程

等待线程

终止线程

检测线程状态

线程优先级

前台线程和后台线程

向线程传递参数

 Lock关键字的使用

使用Monitor类锁定资源

多线程中处理异常


简介

线程基础主要包含线程的创建、挂起、等待、终止线程。

创建线程

主要用Thread类来创建线程。示例:

// 1.创建一个线程 PrintNumbers为该线程所需要执行的方法
Thread t = new Thread(PrintNumbers);
// 2.启动线程
t.Start();

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

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

从运行结果来说,主线程和创建的线程交叉输出结果,说明PrintNumbers方法同时运行在主线程和另外一个线程中。

暂停线程

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

示例:

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

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

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秒。验证了上面的说法,它的睡眠是至少让线程睡眠多长时间,而不是一定多长时间。

等待线程

线程等待使用的是Join方法,该方法将暂停执行当前线程,直到所等待的另一个线程终止

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

// 3.等待线程结束
t.Join();

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

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")}");
    }
}

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

终止线程

终止线程使用的方法是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没有继续执行便结束了;与预期的结果一致。

注:.net5及以后, abort过时,不支持调用

检测线程状态

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

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

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

操作ThreadState
在公共语言运行库中创建线程。Unstarted
线程调用 StartUnstarted
线程开始运行。Running
线程调用 SleepWaitSleepJoin
线程对其他对象调用 WaitWaitSleepJoin
线程对其他线程调用 JoinWaitSleepJoin
另一个线程调用 InterruptRunning
另一个线程调用 SuspendSuspendRequested
线程响应 Suspend 请求。Suspended
另一个线程调用 ResumeRunning
另一个线程调用 AbortAbortRequested
线程响应 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}");
    }
}

线程优先级

Windows操作系统为抢占式多线程(Preemptive multithreaded)操作系统,是因为线程可在任何时间停止(被枪占)并调度另一个线程。

Windows操作系统中线程有0(最低) ~ 31(最高)的优先级,而优先级越高所能占用的CPU时间就越多,确定某个线程所处的优先级需要考虑进程优先级相对线程优先级两个优先级。

  1. 进程优先级:Windows支持6个进程优先级,分别是Idle、Below Normal、Normal、Above normal、High 和Realtime。默认为Normal
  2. 相对线程优先级:相对线程优先级是相对于进程优先级的,因为进程包含了线程。Windows支持7个相对线程优先级,分别是Idle、Lowest、Below Normal、Normal、Above Normal、Highest 和 Time-Critical.默认为Normal

下表总结了进程的优先级线程的相对优先级优先级(0~31)的映射关系。粗体为相对线程优先级,斜体为进程优先级

IdleBelow NormalNormalAbove NormalHighRealtime
Time-Critical151515151531
Highest6810121526
Above Normal579111425
Normal468101324
Below Normal35791223
Lowest24681122
Idle1111116


WaitSleepJoin
线程对其他线程调用 JoinWaitSleepJoin
另一个线程调用 InterruptRunning
另一个线程调用 SuspendSuspendRequested
线程响应 Suspend 请求。Suspended
另一个线程调用 ResumeRunning
另一个线程调用 AbortAbortRequested
线程响应 Abort 请求。Stopped
线程被终止。Stopped

演示代码如下所示:

Console.WriteLine($"当前线程优先级: {Thread.CurrentThread.Priority} \r\n");

// 第一次测试,在所有核心上运行
Console.WriteLine("运行在所有空闲的核心上");
RunThreads();
Thread.Sleep(TimeSpan.FromSeconds(2));

// 第二次测试,在单个核心上运行
Console.WriteLine("\r\n运行在单个核心上");
// 设置在单个核心上运行
System.Diagnostics.Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
RunThreads();

Console.ReadLine();

 void RunThreads()
{
    var sample = new ThreadSample();

    var threadOne = new Thread(sample.CountNumbers);
    threadOne.Name = "线程一";
    var threadTwo = new Thread(sample.CountNumbers);
    threadTwo.Name = "线程二";

    // 设置优先级和启动线程
    threadOne.Priority = ThreadPriority.Highest;
    threadTwo.Priority = ThreadPriority.Lowest;
    threadOne.Start();
    threadTwo.Start();

    // 延时2秒 查看结果
    Thread.Sleep(TimeSpan.FromSeconds(2));
    sample.Stop();
}


class ThreadSample
{
    private bool _isStopped = false;

    public void Stop()
    {
        _isStopped = true;
    }

    public void CountNumbers()
    {
        long counter = 0;

        while (!_isStopped)
        {
            counter++;
        }

        Console.WriteLine($"{Thread.CurrentThread.Name} 优先级为 {Thread.CurrentThread.Priority,11} 计数为 = {counter,13:N0}");
    }
}

运行结果如图所示。Highest占用的CPU时间明显多于Lowest。当程序运行在所有核心上时,线程可以在不同核心同时运行,所以HighestLowest差距会小一些。

前台线程和后台线程

在CLR中,线程要么是前台线程,要么就是后台线程。当一个进程的所有前台线程停止运行时,CLR将强制终止仍在运行的任何后台线程,不会抛出异常。

在C#中可通过Thread类中的IsBackground属性来指定是否为后台线程。在线程生命周期中,任何时候都可从前台线程变为后台线程。线程池中的线程默认为后台线程


var sampleForeground = new ThreadSample(10);
var sampleBackground = new ThreadSample(20);
var threadPoolBackground = new ThreadSample(20);

// 默认创建为前台线程
var threadOne = new Thread(sampleForeground.CountNumbers);
threadOne.Name = "前台线程";
threadOne.IsBackground = false;

var threadTwo = new Thread(sampleBackground.CountNumbers);
threadTwo.Name = "后台线程";
// 设置IsBackground属性为 true 表示后台线程
threadTwo.IsBackground = true;

// 线程池内的线程默认为 后台线程
ThreadPool.QueueUserWorkItem((obj) =>
{
    Thread.CurrentThread.Name = "线程池线程";
    threadPoolBackground.CountNumbers();
});

// 启动线程 
threadOne.Start();
threadTwo.Start();


class ThreadSample
{
    private readonly int _iterations;

    public ThreadSample(int iterations)
    {
        _iterations = iterations;
    }
    public void CountNumbers()
    {
        for (int i = 0; i < _iterations; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
            Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}");
        }
    }
}

运行结果如图所示。当前台线程10次循环结束以后,创建的后台线程和线程池线程都会被CLR强制结束。

向线程传递参数

向线程中传递参数常用的有三种方法,构造函数传值、Start方法传值和Lambda表达式传值,一般常用Start方法来传值。

演示代码如下所示,通过三种方式来传递参数,告诉线程中的循环最终需要循环几次。


// 第一种方法 通过构造函数传值
var sample = new ThreadSample(10);

var threadOne = new Thread(sample.CountNumbers);
threadOne.Name = "ThreadOne";
threadOne.Start();
threadOne.Join();

Console.WriteLine("--------------------------");

// 第二种方法 使用Start方法传值 
// Count方法 接收一个Object类型参数
var threadTwo = new Thread(Count);
threadTwo.Name = "ThreadTwo";
// Start方法中传入的值 会传递到 Count方法 Object参数上
threadTwo.Start(8);
threadTwo.Join();

Console.WriteLine("--------------------------");

// 第三种方法 Lambda表达式传值
// 实际上是构建了一个匿名函数 通过函数闭包来传值
var threadThree = new Thread(() => CountNumbers(12));
threadThree.Name = "ThreadThree";
threadThree.Start();
threadThree.Join();
Console.WriteLine("--------------------------");

// Lambda表达式传值 会共享变量值
int i = 10;
var threadFour = new Thread(() => PrintNumber(i));
threadFour.Name = "threadFour";
i = 20;
var threadFive = new Thread(() => PrintNumber(i));
threadFive.Name = "threadFive";
threadFour.Start();
threadFive.Start();


static void Count(object iterations)
{
    CountNumbers((int)iterations);
}

static void CountNumbers(int iterations)
{
    for (int i = 1; i <= iterations; i++)
    {
        Thread.Sleep(TimeSpan.FromSeconds(0.5));
        Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}");
    }
}

static void PrintNumber(int number)
{
    Console.WriteLine(number);
}

class ThreadSample
{
    private readonly int _iterations;

    public ThreadSample(int iterations)
    {
        _iterations = iterations;
    }
    public void CountNumbers()
    {
        for (int i = 1; i <= _iterations; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
            Console.WriteLine($"{Thread.CurrentThread.Name} prints {i}");
        }
    }
}

 Lock关键字的使用

在多线程的系统中,由于CPU的时间片轮转等线程调度算法的使用,容易出现线程安全问题。

在C#中lock关键字是一个语法糖,它将Monitor封装,给object加上一个互斥锁,从而实现代码的线程安全,Monitor会在下一节中介绍。

对于lock关键字还是Monitor锁定的对象,都必须小心选择,不恰当的选择可能会造成严重的性能问题甚至发生死锁。以下有几条关于选择锁定对象的建议。

  1. 同步锁定的对象不能是值类型。因为使用值类型时会有装箱的问题,装箱后的就成了一个新的实例,会导致Monitor.Enter()Monitor.Exit()接收到不同的实例而失去关联性
  2. 避免锁定this、typeof(type)和stringthistypeof(type)锁定可能在其它不相干的代码中会有相同的定义,导致多个同步块互相阻塞。string需要考虑字符串拘留的问题,如果同一个字符串常量在多个地方出现,可能引用的会是同一个实例。
  3. 对象的选择作用域尽可能刚好达到要求,使用静态的、私有的变量。

以下演示代码实现了多线程情况下的计数功能,一种实现是线程不安全的,会导致结果与预期不相符,但也有可能正确。另外一种使用了lock关键字进行线程同步,所以它结果是一定的。


Console.WriteLine("错误的多线程计数方式");

var c = new Counter();
// 开启3个线程,使用没有同步块的计数方式对其进行计数
var t1 = new Thread(() => TestCounter(c));
var t2 = new Thread(() => TestCounter(c));
var t3 = new Thread(() => TestCounter(c));
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();

// 因为多线程 线程抢占等原因 其结果是不一定的  碰巧可能为0
Console.WriteLine($"Total count: {c.Count}");
Console.WriteLine("--------------------------");

Console.WriteLine("正确的多线程计数方式");

var c1 = new CounterWithLock();
// 开启3个线程,使用带有lock同步块的方式对其进行计数
t1 = new Thread(() => TestCounter(c1));
t2 = new Thread(() => TestCounter(c1));
t3 = new Thread(() => TestCounter(c1));
t1.Start();
t2.Start();
t3.Start();
t1.Join();
t2.Join();
t3.Join();

// 其结果是一定的 为0
Console.WriteLine($"Total count: {c1.Count}");

Console.ReadLine();


static void TestCounter(CounterBase c)
{
    for (int i = 0; i < 100000; i++)
    {
        c.Increment();
        c.Decrement();
    }
}

// 线程不安全的计数
class Counter : CounterBase
{
    public int Count { get; private set; }

    public override void Increment()
    {
        Count++;
    }

    public override void Decrement()
    {
        Count--;
    }
}

// 线程安全的计数
class CounterWithLock : CounterBase
{
    private readonly object _syncRoot = new Object();

    public int Count { get; private set; }

    public override void Increment()
    {
        // 使用Lock关键字 锁定私有变量
        lock (_syncRoot)
        {
            // 同步块
            Count++;
        }
    }

    public override void Decrement()
    {
        lock (_syncRoot)
        {
            Count--;
        }
    }
}

abstract class CounterBase
{
    public abstract void Increment();

    public abstract void Decrement();
}

使用Monitor类锁定资源

Monitor类主要用于线程同步中, lock关键字是对Monitor类的一个封装,其封装结构如下代码所示。

try
{
    Monitor.Enter(obj);
    dosomething();
}
catch(Exception ex)
{  
}
finally
{
    Monitor.Exit(obj);
}

以下代码演示了使用Monitor.TyeEnter()方法避免资源死锁和使用lock发生资源死锁的场景。


object lock1 = new object();
object lock2 = new object();

new Thread(() => LockTooMuch(lock1, lock2)).Start();

lock (lock2)
{
    Thread.Sleep(1000);
    Console.WriteLine("Monitor.TryEnter可以不被阻塞, 在超过指定时间后返回false");
    // 如果5S不能进入同步块,那么返回。
    // 因为前面的lock锁定了 lock2变量  而LockTooMuch()一开始锁定了lock1 所以这个同步块无法获取 lock1 而LockTooMuch方法内也不能获取lock2
    // 只能等待TryEnter超时 释放 lock2 LockTooMuch()才会是释放 lock1
    if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5)))
    {
        Console.WriteLine("获取保护资源成功");
    }
    else
    {
        Console.WriteLine("获取资源超时");
    }
}

new Thread(() => LockTooMuch(lock1, lock2)).Start();

Console.WriteLine("----------------------------------");
lock (lock2)
{
    Console.WriteLine("这里会发生资源死锁");
    Thread.Sleep(1000);
    // 这里必然会发生死锁  
    // 本同步块 锁定了 lock2 无法得到 lock1
    // 而 LockTooMuch 锁定了 lock1 无法得到 lock2
    lock (lock1)
    {
        // 该语句永远都不会执行
        Console.WriteLine("获取保护资源成功");
    }
}


static void LockTooMuch(object lock1, object lock2)
{
    lock (lock1)
    {
        Console.WriteLine("LockTooMuch lock1");
        Thread.Sleep(1000);
        lock (lock2)
        {
            Console.WriteLine("LockTooMuch lock2");
        }
    }
}

运行结果如图所示,因为使用Monitor.TryEnter()方法在超时以后会返回,不会阻塞线程,所以没有发生死锁。而第二段代码中lock没有超时返回的功能,导致资源死锁,同步块中的代码永远不会被执行。

多线程中处理异常

在多线程中处理异常应当使用就近原则,在哪个线程发生异常那么所在的代码块一定要有相应的异常处理。否则可能会导致程序崩溃、数据丢失。

主线程中使用try/catch语句是不能捕获创建线程中的异常。但是万一遇到不可预料的异常,可通过监听AppDomain.CurrentDomain.UnhandledException事件来进行捕获和异常处理。

演示代码如下所示,异常处理 1 和 异常处理 2 能正常被执行,而异常处理 3 是无效的。


// 启动线程,线程代码中进行异常处理
var t = new Thread(FaultyThread);
t.Start();
t.Join();

// 捕获全局异常
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
t = new Thread(BadFaultyThread);
t.Start();
t.Join();

// 线程代码中不进行异常处理,尝试在主线程中捕获
AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException;
try
{
    t = new Thread(BadFaultyThread);
    t.Start();
}
catch (Exception ex)
{
    // 永远不会运行
    Console.WriteLine($"异常处理 3 : {ex.Message}");
}


void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    Console.WriteLine($"异常处理 2 :{(e.ExceptionObject as Exception).Message}");
}

void BadFaultyThread()
{
    Console.WriteLine("有异常的线程已启动...");
    Thread.Sleep(TimeSpan.FromSeconds(2));
    throw new Exception("Boom!");
}

void FaultyThread()
{
    try
    {
        Console.WriteLine("有异常的线程已启动...");
        Thread.Sleep(TimeSpan.FromSeconds(1));
        throw new Exception("Boom!");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"异常处理 1 : {ex.Message}");
    }
}

  • 24
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值