Skip to content

C# 多线程 01 线程基础

🏷️ 《C# 多线程》

创建线程

csharp
Thread t = new Thread(PrintNumbers);
t.Start();

暂停线程

csharp
Thread.Sleep(TimeSpan.FromSeconds(2));

线程等待

csharp
Thread t = new Thread(PrintNumbersWithDelay);
t.Start();
// 线程等待直到 t 执行结束,主线程才会继续执行
t.Join();

终止线程

csharp
Thread t = new Thread(PrintNumbersWithDelay);
t.Start();
Thread.Sleep(TimeSpan.FromSeconds(6));
// 终止线程
// 这给线程注入了 ThreadAbortException 方法,导致线程被终结。这非常危险,因为该异常可以在任何时刻发生并可能摧毁应用程序
// 另外,使用该技术也不一定总能终止线程。
// 目标线程可以通过处理该异常并调用 Thread.ResetAbort 方法来拒绝被终止。
// 因此并不推荐使用 Abort 方法来终止线程。
// 可优先使用一些其它方法,比如提供一个 CancellationToken 方法来取消线程的执行。
t.Abort();
Console.WriteLine("A thread has been aborted");

检测线程状态

csharp
Console.WriteLine(Thread.CurrentThread.ThreadState.ToString());

ThreadState

  • Unstarted 未开始时

  • Running 正在执行时

  • WaitSleepJoin 线程 Sleep 时

  • AbortRequested 线程 Abort 时

  • Stopped 正常结束时

线程优先级

csharp
threadOne.Priority = ThreadPriority.Highest;
threadTwo.Priority = ThreadPriority.Lowest;
csharp
Console.WriteLine($"Current thread priority: {Thread.CurrentThread.Priority}");
Console.WriteLine("Running on all cores available");
// 在所有可用的 CPU 核心上启动线程
// 如果拥有一个以上的计算核心,将在两秒内得到初步结果
// 最高优先级的线程通常会计算更多的迭代,但两个值应该很接近
RunThreads();
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine("Running on a single core");
// 让操作系统的所有线程运行在单个 CPU 核心(第一核心)上
// 结果会完全不同,计算耗时也会超过 2 秒
// 这是因为 CPU 核心大部分时间在运行高优先级的线程,只留给剩下的线程很少的时间来运行
Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
RunThreads();
点击查看执行结果
Current thread priority: Normal
Running on all cores available
ThreadTwo with      Lowest priority has a counte =   612,186,937
ThreadOne with     Highest priority has a counte =   714,994,619
Running on a single core
ThreadOne with     Highest priority has a counte = 6,758,568,718
ThreadTwo with      Lowest priority has a counte =    44,751,166

前台线程和后台线程

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

var threadOne = new Thread(sampleForeground.CountNumbers);
threadOne.Name = "ForegroundThread";
var threadTwo = new Thread(sampleBackground.CountNumbers);
threadTwo.Name = "BackgroundThread";
// 设置为后台线程
threadTwo.IsBackground = true;

threadOne.Start();
threadTwo.Start();
// 前台线程终止后,程序结束,并且后台线程被终结
// 进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。

向线程传递参数

csharp
// 通过类的实例化传递参数
var sample = new ThreadSample(10);
var threadOne = new Thread(sample.CountNumbers);
threadOne.Name = "ThreadOne";
threadOne.Start();
threadOne.Join();

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

// 通过 Start 方法传参
var threadTwo = new Thread(Count);
threadTwo.Name = "ThreadTwo";
threadTwo.Start(8);
threadTwo.Join();

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

// 通过 lambda 表达式  传参
var threadThree = new Thread(() => CountNumbers(12));
threadThree.Start();
threadThree.Join();

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

// 使用lambda表达式引用另一个C#对象的方法被称为闭包。
// 当在lambda表达式中使用任何局部变量时,C#会生成一个类,并将该变量作为该类的一个属性。
// 所以实际上该方法与threadOne中使用的一样,但是我们无需定义该类,C#编译器会自动帮我们实现
// 如果在多个 lambda 表达式中使用相同的变量,它们会共享该变量值。
int i = 10;
var threadFour = new Thread(() => PrintNumber(i));
i = 20;
var threadFive = new Thread(() => PrintNumber(i));
threadFour.Start(); // print 20
threadFive.Start(); // print 20
// 如果在线程启动后再更改 i 的值,则不会影响到已经启动的线程。

使用 C# 中的 lock 关键字

点击查看代码
csharp
/// <summary>
/// 使用C#的lock关键字
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
    Console.WriteLine("Incorrect counter");

    var c = new Counter();

    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();

    // 多个线程中存在竞争条件(race condition)
    // 由于++并不是一个线程安全的操作,所以输出结果一般都不为 0
    Console.WriteLine($"Total count: {c.Count}");
    Console.WriteLine("----------------------------------------");

    Console.WriteLine("Correct counter");

    var c1 = new CounterWithLock();

    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();
    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 Decrement()
    {
        Count++;
    }

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

class CounterWithLock : CounterBase
{
    // 如果锁定了一个对象,需要访问该对象的所有其他线程则会处于阻塞状态,并等待直到该对象解除锁定。
    // 这可能会导致严重的性能问题。
    private readonly object _ayncRoot = new object();

    public int Count { get; private set; }

    public override void Decrement()
    {
        lock(_ayncRoot)
        {
            Count++;
        }
    }

    public override void Increment()
    {
        lock (_ayncRoot)
        {
            Count--;
        }
    }
}

abstract class CounterBase
{
    public abstract void Increment();

    public abstract void Decrement();
}
点击查看输出结果
Incorrect counter
Total count: -2516
----------------------------------------
Correct counter
Total count: 0

使用 Monitor 类锁定资源

点击查看代码
csharp
/// <summary>
/// 使用 Monitor 类锁定资源
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
    object lock1 = new object();
    object lock2 = new object();

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

    lock (lock2)
    {
        Thread.Sleep(TimeSpan.FromSeconds(1));
        Console.WriteLine("Monitor.TyrEnter allows not to get stuck, returning false after a specified timeout is elapased");
        // Monitor.TyrEnter 方法接收一个超时参数。
        // 如果在能够获取被 lock 保护的资源之前,超时时间过期,则该方法会返回 false。
        if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(5)))
        {
            Console.WriteLine("Acquired a protected resource succesfully");
        } else
        {
            Console.WriteLine("Timeout acquiring a resource!");
        }
    }

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

    lock (lock2)
    {
        Console.WriteLine("This will be a deadlock");
        Thread.Sleep(TimeSpan.FromSeconds(1));
        lock (lock1)
        {
            Console.WriteLine("Acquired a protected resource succesfully");
        }
    }
}

static void LockTooMuch(object lock1, object lock2)
{
    lock(lock1)
    {
        Thread.Sleep(TimeSpan.FromSeconds(1));
        lock (lock2)
        {
        }
    }
}
点击查看输出结果
Monitor.TyrEnter allows not to get stuck, returning false after a specified timeout is elapased
Timeout acquiring a resource!
This will be a deadlock

处理异常

一般来说不要在线程中抛出异常,而是在线程代码中使用 try/catch 代码块。

点击查看代码
csharp
static void Main(string[] args)
{
    var t = new Thread(FaultyThread);
    t.Start();
    t.Join();

    try
    {
        t = new Thread(BadFaultyThread);
        t.Start();
        t.Join();
    }
    catch (Exception ex)
    {
        Console.WriteLine("We won't get here!");
    }
}

static void BadFaultyThread()
{
    Console.WriteLine("Starting a faulty thread...");
    Thread.Sleep(TimeSpan.FromSeconds(2));
    throw new Exception("Boom!");
}

static void FaultyThread()
{
    try
    {
        Console.WriteLine("Starting a faulty thread...");
        Thread.Sleep(TimeSpan.FromSeconds(1));
        throw new Exception("Boom!");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Exception handled: {ex.Message}");
    }
}
点击查看输出结果

线程外的 try/catch 并没有捕获到新线程中的异常,而且直接导致进程崩溃了

Starting a faulty thread...
Exception handled: Boom!
Starting a faulty thread...

未经处理的异常:  System.Exception: Boom!
   在 Recipe11.Program.BadFaultyThread() 位置 C:\OneDrive\Project\MultiThreading\Recipe11\Program.cs:行号 34
   在 System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   在 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   在 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   在 System.Threading.ThreadHelper.ThreadStart()