.NET多线程


一、基本概念

1.什么是线程?

线程是操作系统中能够独立运行的最小单位,也是程序中可以并发执行的一段指令序列
线程是进程的一部分,一个进程可以包含多个线程
进程有入口线程,也可以创建更多线程

2.多线程的使用场景

批量重复任务希望同时进行
多个不同任务需要同时进行,互不干扰
总结:提高程序运行效率

3.什么是线程池?

一组预先创建好的线程

备注:因为一个线程的创建和销毁是占用系统开销的,用线程池里提前创建好的线程,可以避免频分的创建和销毁新线程,可以提高系统效率。小的且不会阻塞的任务可以用线程池里的线程,其他的时候还是自己创建新线程吧。
异步编程默认用的线程池里的线程。

4.什么是线程安全?

多个线程访问同一个资源,不会导致共享资源数据不一致或不可预期的结果就称为线程安全

5.如何保证线程安全?

同步机制:协调和控制多个线程之间的执行顺序、互斥访问共享资源
原子操作:在执行过程中不会被中断的操作。不可分割,要么完全执行,要么完全不执行,没有中间状态。使用原子操作得借助于.NET的System.Threading.Interlocked类,这里面提供了一些原子操作的方法。

5.1.协调和控制多个线程之间的执行顺序

可以使用的手法:

  • Thread.Join
  • ManualResetEvent 和 AutoResetEvent
  • SemaphoreSlim
  • Task 和 await

Thread.Join

Thread.Join 方法可让一个线程等待另一个线程执行完毕后再继续执行,常用于简单的线程顺序控制。

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread thread1 = new Thread(Task1);
        Thread thread2 = new Thread(Task2);

        thread1.Start();
        // 主线程等待 thread1 执行完毕
        thread1.Join();

        thread2.Start();
        // 主线程等待 thread2 执行完毕
        thread2.Join();

        Console.WriteLine("All tasks are completed.");
    }

    static void Task1()
    {
        Console.WriteLine("Task 1 is starting.");
        Thread.Sleep(1000); 
        Console.WriteLine("Task 1 is completed.");
    }

    static void Task2()
    {
        Console.WriteLine("Task 2 is starting.");
        Thread.Sleep(1000); 
        Console.WriteLine("Task 2 is completed.");
    }
}

在上述代码中,thread1.Join() 会使主线程阻塞,直到 thread1 执行完毕;接着 thread2.Start() 启动 thread2,thread2.Join() 又会使主线程等待 thread2 执行完毕,从而保证了 Task1 先于 Task2 执行。


ManualResetEvent 和 AutoResetEvent

ManualResetEvent 和 AutoResetEvent是用于线程间同步的事件类,前者需要手动重置事件状态,后者在信号被接收后会自动重置。

using System;
using System.Threading;

class Program
{
    static ManualResetEvent event1 = new ManualResetEvent(false);
    static ManualResetEvent event2 = new ManualResetEvent(false);

    static void Main()
    {
        Thread thread1 = new Thread(Task1);
        Thread thread2 = new Thread(Task2);

        thread1.Start();
        thread2.Start();

        // 主线程等待两个任务完成
        event2.WaitOne();
        Console.WriteLine("All tasks are completed.");
    }

    static void Task1()
    {
        Console.WriteLine("Task 1 is starting.");
        Thread.Sleep(1000); 
        Console.WriteLine("Task 1 is completed.");
        // 通知 Task2 可以开始执行
        event1.Set();
    }

    static void Task2()
    {
        // 等待 Task1 完成
        event1.WaitOne();
        Console.WriteLine("Task 2 is starting.");
        Thread.Sleep(1000); 
        Console.WriteLine("Task 2 is completed.");
        // 通知主线程任务完成
        event2.Set();
    }
}

在这个例子中,event1 用于控制 Task2 等待 Task1 完成,event2 用于通知主线程所有任务已完成。event1.WaitOne() 会使 Task2 线程阻塞,直到 event1.Set() 被调用,从而实现了线程执行顺序的控制。


SemaphoreSlim
SemaphoreSlim 是一个轻量级的信号量,可用于限制同时访问某个资源或执行某个代码段的线程数量,也可用于线程顺序控制。

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static SemaphoreSlim semaphore = new SemaphoreSlim(0, 1);

    static async Task Main()
    {
        Task task1 = Task.Run(Task1);
        Task task2 = Task.Run(Task2);

        await Task.WhenAll(task1, task2);
        Console.WriteLine("All tasks are completed.");
    }

    static async Task Task1()
    {
        Console.WriteLine("Task 1 is starting.");
        await Task.Delay(1000); 
        Console.WriteLine("Task 1 is completed.");
        // 释放信号量,允许 Task2 执行
        semaphore.Release();
    }

    static async Task Task2()
    {
        // 等待信号量
        await semaphore.WaitAsync();
        Console.WriteLine("Task 2 is starting.");
        await Task.Delay(1000); 
        Console.WriteLine("Task 2 is completed.");
    }
}

SemaphoreSlim 初始计数为 0,意味着 Task2 会在 semaphore.WaitAsync() 处阻塞,直到 Task1 调用 semaphore.Release() 释放信号量,从而保证了 Task1 先于 Task2 执行。


Task 和 await
在异步编程中,可以使用 Task 和 await 关键字来协调任务的执行顺序。

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        await Task1();
        await Task2();
        Console.WriteLine("All tasks are completed.");
    }

    static async Task Task1()
    {
        Console.WriteLine("Task 1 is starting.");
        await Task.Delay(1000); 
        Console.WriteLine("Task 1 is completed.");
    }

    static async Task Task2()
    {
        Console.WriteLine("Task 2 is starting.");
        await Task.Delay(1000); 
        Console.WriteLine("Task 2 is completed.");
    }
}

await 关键字会使 Main 方法异步等待 Task1 执行完毕后再继续执行 Task2,保证了任务的顺序执行,且不会阻塞主线程。


5.2 互斥访问共享资源

互斥指的一个线程访问这个共享资源的时候,不允许其他线程访问

  • lock 语句是最常用的线程同步方式,简单易用,适用于大多数场景。
  • Monitor 类提供了更灵活的锁控制,与 lock 语句功能类似。
  • Mutex 类可用于跨进程的线程同步,性能相对较低,适用于需要在多个进程之间共享锁的场景。

LOCK
lock 语句是 .NET 中最常用的线程同步机制之一,它基于 Monitor 类实现,用于确保在同一时间只有一个线程可以执行被锁定的代码块。

using System;
using System.Threading;

class Program
{
    private static readonly object _lockObject = new object();
    private static int _sharedResource = 0;

    static void Main()
    {
        // 创建两个线程来访问共享资源
        Thread thread1 = new Thread(IncrementSharedResource);
        Thread thread2 = new Thread(IncrementSharedResource);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine($"Final value of shared resource: {_sharedResource}");
    }

    static void IncrementSharedResource()
    {
        for (int i = 0; i < 100000; i++)
        {
            // 使用 lock 语句确保同一时间只有一个线程可以访问共享资源
            lock (_lockObject)
            {
                _sharedResource++;
            }
        }
    }
}
  • _lockObject 是一个用于锁定的对象,通常使用 private static readonly 修饰,以确保所有线程都使用同一个锁对象。
  • lock (_lockObject)语句块确保在同一时间只有一个线程可以进入该代码块,从而避免多个线程同时修改 _sharedResource 导致的数据竞争问题。

Monitor
Monitor 类提供了更灵活的线程同步机制,与 lock 语句类似,但可以更精细地控制锁的获取和释放。

using System;
using System.Threading;

class Program
{
    private static readonly object _lockObject = new object();
    private static int _sharedResource = 0;

    static void Main()
    {
        Thread thread1 = new Thread(IncrementSharedResource);
        Thread thread2 = new Thread(IncrementSharedResource);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine($"Final value of shared resource: {_sharedResource}");
    }

    static void IncrementSharedResource()
    {
        for (int i = 0; i < 100000; i++)
        {
            // 获取锁
            Monitor.Enter(_lockObject);
            try
            {
                _sharedResource++;
            }
            finally
            {
                // 释放锁
                Monitor.Exit(_lockObject);
            }
        }
    }
}
  • Monitor.Enter(_lockObject) 用于获取锁,如果锁已经被其他线程持有,则当前线程会被阻塞。
  • Monitor.Exit(_lockObject) 用于释放锁,确保在 try 块中的代码执行完毕后,无论是否发生异常,锁都会被释放。

Mutex
Mutex 是一种互斥锁,可用于在多个线程或多个进程之间实现线程同步。

using System;
using System.Threading;

class Program
{
    private static readonly Mutex _mutex = new Mutex();
    private static int _sharedResource = 0;

    static void Main()
    {
        Thread thread1 = new Thread(IncrementSharedResource);
        Thread thread2 = new Thread(IncrementSharedResource);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine($"Final value of shared resource: {_sharedResource}");
    }

    static void IncrementSharedResource()
    {
        for (int i = 0; i < 100000; i++)
        {
            // 请求获取互斥锁
            _mutex.WaitOne();
            try
            {
                _sharedResource++;
            }
            finally
            {
                // 释放互斥锁
                _mutex.ReleaseMutex();
            }
        }
    }
}
  • _mutex.WaitOne() 用于请求获取互斥锁,如果锁已经被其他线程持有,则当前线程会被阻塞。
  • _mutex.ReleaseMutex() 用于释放互斥锁,确保在 try 块中的代码执行完毕后,无论是否发生异常,锁都会被释放。

5.3 原子操作

说明:在执行过程中不会被中断的操作。不可分割,要么完全执行,要么完全不执行,没有中间状态。.NET底层提供了一些原子操作的方法,只能用这些方法进行原子操作。

.NET 在 System.Threading.Interlocked 类中提供了一系列用于执行原子操作的静态方法,常见的如下:
1.Increment 和 Decrement
用于对整数类型(int、long 等)进行原子的自增和自减操作。
如 Interlocked.Increment(ref num) 会将 num 的值原子地增加 1,Interlocked.Decrement(ref num) 会将 num 的值原子地减少 1。
2.Exchange
用于原子地将一个变量的值替换为另一个值,并返回该变量的原始值。
例如 Interlocked.Exchange(ref num, 10) 会将 num 的值原子地替换为 10,并返回 num 原来的值。
3.CompareExchange
用于原子地比较一个变量的值与一个期望值,如果相等,则将该变量的值替换为一个新值,并返回该变量的原始值;如果不相等,则不进行替换,直接返回该变量的当前值。
例如 Interlocked.CompareExchange(ref num, 20, 10) 会比较 num 的值是否为 10,如果是,则将 num 的值替换为 20,并返回 10;如果不是,则不进行替换,直接返回 num 的当前值。

6.NET自带的一些多线程方法

Parallel 是一个非常有用的类,翻译中文为“并行的”,它位于 System.Threading.Tasks 命名空间下,主要用于简化并行编程,让开发者可以更轻松地利用多核处理器的计算能力,提高程序的执行效率。
比如使用Parallel 的For来执行循环,它会将循环的迭代任务分配给多个线程同时执行。Parallel还有Foreach、Invoke等方法。


AsParallel 是一个扩展方法,它属于 PLINQ(Parallel Language Integrated Query,并行语言集成查询)的一部分,位于 System.Linq 命名空间下。AsParallel 方法的主要作用是将一个普通的顺序查询转换为并行查询,从而利用多核处理器的并行计算能力,提高查询操作的执行效率。
AsParallel 可以应用于任何实现了 IEnumerable 接口的集合,将其转换为可并行处理的查询。

using System;
using System.Linq;

class Program
{
    static void Main()
    {
        // 创建一个包含 1 到 100 的整数集合
        var numbers = Enumerable.Range(1, 100);

        // 使用 AsParallel 将顺序查询转换为并行查询
        var parallelResult = numbers.AsParallel()
                                    .Where(n => n % 2 == 0)
                                    .Select(n => n * n);

        // 输出结果
        foreach (var result in parallelResult)
        {
            Console.WriteLine(result);
        }
    }
}

在上述代码中,AsParallel 方法将 numbers 集合转换为并行查询,后续的 Where 和 Select 操作会并行执行,从而利用多核处理器的优势。


在使用 AsParallel 方法将顺序查询转换为并行查询后,查询结果的顺序可能会被打乱,因为并行处理时各个任务的完成顺序是不确定的。AsOrdered 方法的作用就是确保并行查询的结果保持与源序列相同的顺序。

二、线程的创建

1.线程的创建

不带参数的:

  // 创建一个新的线程,传入要执行的方法
  Thread newThread = new Thread(DoWork); //这里DoWork是一个没有参数的方法
  // 启动线程
  newThread.Start();

带参数的:

// 创建一个新的线程,传入带参数的方法
Thread newThread = new Thread(DoWorkWithParameter);

// 启动线程并传递参数
newThread.Start("Hello from parameter!");

static void DoWorkWithParameter(object parameter)//只能是一个参数,如果想传递多个参数的话,传一个类吧
{
   string message = (string)parameter;
   Console.WriteLine(message);
}

用线程池里的线程:

  // 将方法排队到线程池队列中
  ThreadPool.QueueUserWorkItem(DoWorkInThreadPool);

一般不推荐直接使用线程池里的线程,因为线程池里的线程进程自己还要干自己的事,比如web应用中线程池中的线程用于处理客户端的请求

1.1 前台线程与后台线程

前台线程:
只要有一个前台线程在运行,应用程序就不会退出。也就是说,前台线程会阻止应用程序的终止,它会保证自身执行完毕后才可能让程序结束。通常,创建的普通线程默认就是前台线程。
后台线程:
后台线程不会阻止应用程序的退出。当所有前台线程都执行完毕后,无论后台线程是否执行完,应用程序都会立即退出,后台线程也会随之终止。

前台线程、后台线程
前台线程创建的后台线程会跟着前台线程一起关闭

2.线程的终止

Thread.Join (详见第一章5.1)

说明:Thread.Join 方法可让一个线程等待另一个线程执行完毕后再继续执行,常用于简单的线程顺序控制。

Thread.Interrupt()

说明:Interrupt() 方法用于中断处于 WaitSleepJoin 状态(即线程正在等待、睡眠或加入)的线程。当调用 Interrupt() 方法时,会在目标线程中抛出 ThreadInterruptedException 异常。备注:如果线程里正在执行死循环,Interrupt()停不掉他。得写个Thread.Sleep(0)让他等待一下。

3.线程的挂起与恢复

3.1 Mutex(互斥体)

Mutex 是一种线程同步原语,用于确保在同一时间只有一个线程可以访问共享资源。它是跨进程的,可以在不同进程之间实现互斥访问。

3.2 Semaphore(信号量)

Semaphore 用于控制对有限资源的并发访问。它维护一个计数器,该计数器表示可用资源的数量。当线程请求访问资源时,计数器会减 1;当线程释放资源时,计数器会加 1。如果计数器为 0,则请求线程会被阻塞,直到有其他线程释放资源。

3.3 WaitHandle

WaitHandle 是一个抽象基类,Mutex、Semaphore 和 EventWaitHandle 等类都继承自它。它提供了一组方法,用于等待一个或多个同步对象变为有信号状态。

3.4 ReaderWriterLock

ReaderWriterLock 用于实现读写锁,允许多个线程同时进行读操作,但在写操作时会独占资源,确保数据的一致性。读操作可以并发执行,提高了并发性能;而写操作则需要独占资源,避免数据冲突。

4.不要自己造轮子

4.1 Lazy

Lazy 是一个非常有用的泛型类,它用于实现延迟初始化。延迟初始化是指在需要使用某个对象时才进行初始化,而不是在对象创建时就立即初始化,这样可以提高程序的性能和资源利用率,特别是对于那些初始化开销较大或者不一定会被使用的对象。

4.2 concurrentBag等

ConcurrentBag、ConcurrentStack、ConcurrentQueue 和 ConcurrentDictionary<TKey, TValue> 都是 .NET 中 System.Collections.Concurrent 命名空间下的线程安全集合类,它们用于在多线程环境中高效地存储和操作数据。

ConcurrentBag:允许多个线程同时进行添加和移除操作,无需额外的同步机制。
ConcurrentStack:遵循后进先出(LIFO)原则,多线程环境下可安全操作。
ConcurrentQueue:遵循先进先出(FIFO)原则,多线程环境下可安全操作。
ConcurrentDictionary:用于存储键值对,多线程环境下可安全进行添加、删除、查找等操作。

4.3 BlockingCollection

BlockingCollection本质上是一个支持阻塞操作的集合容器,它可以作为生产者线程和消费者线程之间的桥梁。生产者线程负责向集合中添加元素,而消费者线程则从集合中取出元素进行处理。当集合为空时,消费者线程会被阻塞,直到有新元素被添加进来;当集合达到最大容量时,生产者线程会被阻塞,直到有元素被消费者取出。

4.4 Channel

Channel 是一种用于在不同线程或异步操作之间进行高效、安全的数据传递的机制,类似于一个管道,一端可以发送数据,另一端可以接收数据。它在 System.Threading.Channels 命名空间下,是 .NET Core 3.0 及更高版本引入的功能,常用于构建异步数据流和实现生产者 - 消费者模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙套大人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值