• C#多线程


    一、多线程实现方式
            1. 使⽤Thread类: System.Threading.Thread 类是C#中最基本的多线程编程⼯具。

            2. 使⽤ThreadPool: 线程池是⼀个管理和重⽤线程的机制,它可以在应⽤程序中创建和使 ⽤多个线程,⽽⽆需显式地管理线程的⽣命周期。你可以使⽤ ThreadPool.QueueUserWorkItem ⽅法将⼯作项添加到线程池中执⾏

            3. 使⽤Task类(推荐): System.Threading.Tasks.Task 类是.NET Framework 4.0引⼊的并⾏编程⼯具,它提供了更⾼级别的抽象,简化了多线程编程。使⽤ Task.Run ⽅ 法可以很⽅便地创建并启动新线程

    二、.C# 5 引⼊的 async/await 关键字是⽤来做什么的?它与传统 的多线程编程有什么不同?         async/await 是C# 5 中引⼊的⼀种异步编程模式,⽤于简化异步操作的编写和管理。它可 以帮助开发者编写更清晰、更易读的异步代码,同时避免了传统多线程编程中可能出现的⼀些 问题。 async/await 并不是创建新线程的⽅式,⽽是⼀种对异步操作的任务管理机制。 异步编程和多线程的区别:

            1. 可读性: async/await 的代码结构更加清晰易读。传统的多线程编程可能会涉及显 式地创建、启动和管理线程,⽽ async/await 让你可以将异步操作以类似于同步代 码的⽅式进⾏编写,不需要关⼼底层线程的管理。

            2. 阻塞和⾮阻塞: 使⽤ async/await 可以避免阻塞主线程。在传统的多线程编程中, 如果主线程需要等待⼀个操作完成,可能需要使⽤阻塞⽅式等待。⽽ async/await 允许主线程在等待异步操作的同时保持⾮阻塞状态,提⾼了程序的响应性。

            3. 上下⽂切换: 传统的多线程编程可能涉及线程切换的开销,⽽ async/await 不会直 接引⼊线程切换。它使⽤了异步任务的调度器来管理任务的执⾏,这可能会在需要的时候 重⽤线程,减少上下⽂切换的成本。

            4. 异常处理: async/await 更好地处理了异常。异步操作中的异常会在 await 表 达式中正确地捕获,使得异常处理更加简单和可靠。

             5. 资源管理: 传统多线程编程中需要⼿动管理资源的释放,⽽ async/await 通常能够 更好地管理资源的⽣命周期。 总之, async/await 是⼀种更现代、更简洁的异步编程⽅式,相较于传统的多线程编程,它 能够提供更好的可读性、更好的性能和更少的错误。

    三、线程安全

    常见的线程安全问题

            竞争条件(Race Condition):当多个线程并发访问共享资源时,可能会导致竞争条件。例如,当多个线程通过递增操作改变一个共享变量的值时,可能会导致值的不确定性。

            死锁(Deadlock):当多个线程相互等待彼此释放某些资源时,可能会导致死锁。在死锁状态下,程序停止响应,无法正常运行。

            内存泄漏(Memory Leak):内存泄漏是指程序运行时不断分配内存,但不及时释放,导致内存使用过多。这可能会影响程序的性能和可靠性。

            线程干扰(Thread Interference):线程干扰是指在线程间共享数据时,未正确同步数据所导致的问题。这可能导致数据丢失或不一致的情况。

    解决方法
    以下是一些解决线程安全问题的方法:

            互斥锁:互斥锁是一种常用的线程同步机制,它能够保护共享资源,确保多个线程访问资源时不会产生冲突。在C#中,可使用lock关键字来实现互斥锁。

            原子操作:原子操作是指在CPU执行某个操作时,该操作不会中断或被其他线程所干扰。通过使用原子操作,我们可以避免竞争条件的问题。

            并发集合(Concurrent Collections):并发集合是一种特殊的集合类型,它是线程安全的。在C#中,ConcurrentQueue、ConcurrentStack和ConcurrentDictionary等类就是并发集合。

            线程安全的类型(Thread-Safe Types):线程安全的类型是指可以安全地访问和修改数据的类型。在C#中,有一些类型(如StringBuilder、DateTime和String等)是线程安全的。
    四、锁

            1、lock关键字

            如果说c#中的锁,那么首当其冲的就是lock关键字了。给lock关键字指定一个引用对象,然后上锁,保证同一时间只能有一个线程在锁里。这应该是最我们最常用的场景了。注意:我们说的是一把锁里同时只能有一个线程,至于这把锁用在了几个地方,那就不确定了。比如:object lockobj=new object(),这把锁可以锁一个代码块,也可以锁多个代码块,但无论锁多少个代码块,同一时间只能有一个线程打开这把锁进去,所以会有人建议,不要用lock(typeof(Program))或lock(this)这种锁,因为这把锁是所有人能看到的,别人可以用这把锁锁住自己的代码,这样就会出现一把锁锁住多个代码块的情况了,但现实使用中,一般没人会这么干,所以即使我们在阅读开源工程的源码时也能常常见到lock(typeof(Program))这种写法,不过还是建议用私有字段做锁,下面给出锁的几中应用场景:

    1. class Program
    2. {
    3. private readonly object lockObj = new object();
    4. private object obj = null;
    5. public void TryInit()
    6. {
    7. if (obj == null)
    8. {
    9. lock (lockObj)
    10. {
    11. if (obj == null)
    12. {
    13. obj = new object();
    14. }
    15. }
    16. }
    17. }
    18. }

    自动编号

    1. class DemoService
    2. {
    3. private static int id;
    4. private static readonly object lockObj = new object();
    5. public void Action()
    6. {
    7. //do something
    8. int newid;
    9. lock (lockObj)
    10. {
    11. newid = id + 1;
    12. id = newid;
    13. }
    14. //use newid...
    15. }
    16. }

     最后: 需要说明的是,lock关键字只不过是Monitor的语法糖,也就是说下面的代码:

    1. lock (typeof(Program))
    2. {
    3. int i = 0;
    4. //do something
    5. }

    被编译成IL后就变成了:

    1. try
    2. {
    3. Monitor.Enter(typeof(Program));
    4. int i = 0;
    5. //do something
    6. }
    7. finally
    8. {
    9. Monitor.Exit(typeof(Program));
    10. }
    注意:lock关键字不能跨线程使用,因为它是针对线程上的锁。下面的代码是不被允许的(异步代码可能在await前后切换线程):想实现异步锁,参照后面的:《SemaphoreSlim》
    

            2.Monitor

            上面说了lock关键字是Monitor的语法糖,那么肯定Monitor功能是lock的超集,所以这里讲讲Monitor除了lock的功能外还有什么:

            Monitor.Wait(lockObj):让自己休眠并让出锁给其他线程用(其实就是发生了阻塞),直到其他在锁内的线程发出脉冲(Pulse/PulseAll)后才可从休眠中醒来开始竞争锁。Monitor.Wait(lockObj,2000)则可以指定最大的休眠时间,如果时间到还没有被唤醒那么就自己醒。注意: Monitor.Wait有返回值,当自己醒的时候返回false,当其他线程唤醒的时候返回true,这主要是用来防止线程锁死,返回值可以用来判断是否向后执行或者是重新发起Monitor.Wait(lockObj)
    Monitor.Pulse或Monitor.PulseAll:唤醒由于Monitor.Wait休眠的线程,让他们醒来参与竞争锁。不同的是:Pulse只能唤醒一个,PulseAll是全部唤醒。这里顺便提一下:在多生产者、多消费者的情况下,我们更希望去唤醒消费者或者是生产者,而不是谁都唤醒,在java中我们可以使用lock的condition来解决这个问题,在c#中我们可以使用下面介绍的ManaualResetEvent或AutoResetEvent

     

    1. System.Object obj = (System.Object)x;
    2. System.Threading.Monitor.Enter(obj);
    3. try
    4. {
    5. DoSomething();
    6. }
    7. finally
    8. {
    9. System.Threading.Monitor.Exit(obj);
    10. }

    3、ReaderWriteLock[Slim]

            我们知道,Monitor实现的是在读写两种情况的临界区中只可以让一个线程访问,那么如果业务中存在”读取密集型“操作,就好比数据库一样,读取的操作永远比写入的操作多。针对这种情况,我们使用Monitor的话很吃亏,不过没关系,ReadWriterLock[Slim]就很牛X,因为实现了”写入串行“,”读取并行“。
    ReaderWriteLock[Slim]中主要用3组方法:

    <1> AcquireWriterLock[TryEnterReadLock]: 获取写入锁。
    ReleaseWriterLock:释放写入锁。

    <2> AcquireReaderLock: 获取读锁。
    ReleaseReaderLock:释放读锁。

    <3> UpgradeToWriterLock:将读锁转为写锁。
    DowngradeFromWriterLock:将写锁还原为读锁。
     

     并行读

    1. using System;
    2. using System.Threading;
    3. class Program
    4. {
    5. //static ReaderWriterLock readerWriterLock = new ReaderWriterLock();
    6. static ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();
    7. public static void Main(string[] args)
    8. {
    9. var thread = new Thread(() =>
    10. {
    11. Console.WriteLine("thread1 start...");
    12. //readerWriterLock.AcquireReaderLock(3000);
    13. readerWriterLock.TryEnterReadLock(3000);
    14. int index = 0;
    15. while (true)
    16. {
    17. index++;
    18. Console.WriteLine("du...");
    19. Thread.Sleep(1000);
    20. if (index > 6) break;
    21. }
    22. //readerWriterLock.ReleaseReaderLock();
    23. readerWriterLock.ExitReadLock();
    24. });
    25. thread.Start();
    26. var thread2 = new Thread(() =>
    27. {
    28. Console.WriteLine("thread2 start...");
    29. //readerWriterLock.AcquireReaderLock(3000);
    30. readerWriterLock.TryEnterReadLock(3000);
    31. int index = 0;
    32. while (true)
    33. {
    34. index++;
    35. Console.WriteLine("读...");
    36. Thread.Sleep(1000);
    37. if (index > 6) break;
    38. }
    39. //readerWriterLock.ReleaseReaderLock();
    40. readerWriterLock.ExitReadLock();
    41. });
    42. thread2.Start();
    43. Console.ReadLine();
    44. }
    45. }

    串行写

    1. using System;
    2. using System.Threading;
    3. class Program
    4. {
    5. //static ReaderWriterLock readerWriterLock = new ReaderWriterLock();
    6. static ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();
    7. public static void Main(string[] args)
    8. {
    9. var thread = new Thread(() =>
    10. {
    11. Console.WriteLine("thread1 start...");
    12. //readerWriterLock.AcquireWriterLock(1000);
    13. readerWriterLock.TryEnterWriteLock(1000);
    14. Console.WriteLine("写...");
    15. Thread.Sleep(5000);
    16. Console.WriteLine("写完了...");
    17. //readerWriterLock.ReleaseReaderLock();
    18. readerWriterLock.ExitWriteLock();
    19. });
    20. thread.Start();
    21. var thread2 = new Thread(() =>
    22. {
    23. Console.WriteLine("thread2 start...");
    24. try
    25. {
    26. //readerWriterLock.AcquireReaderLock(2000);
    27. readerWriterLock.TryEnterReadLock(2000);
    28. Console.WriteLine("du...");
    29. //readerWriterLock.ReleaseReaderLock();
    30. readerWriterLock.ExitReadLock();
    31. Console.WriteLine("du wan...");
    32. }
    33. catch (Exception ex)
    34. {
    35. Console.WriteLine(ex.Message);
    36. }
    37. });
    38. Thread.Sleep(100);
    39. thread2.Start();
    40. Console.ReadLine();
    41. }
    42. }

    从上面的试验可以看出,“读“和“写”锁是不能并行的,他们之间相互竞争,同一时间,里面可以有一批“读”锁或一个“写”锁 ,其他的则不允许。

    另外,我们在程序中应该尽量使用ReaderWriterLockSlim,而不是ReaderWriterLock,关于这点,可以看官方文档描述:

    4.mutex

            Mutex的实现是调用操作系统层的功能,所以Mutex的性能要略慢一些,而它所能锁住的范围更大(它能跨进程上锁),但是它的功能也就相当于lock关键字(因为没有类似Monitor.Wait和Monitor.Pulse的方法)。
    Mutex分为命名的Mutex和未命名的Mutex,命名的Mutex可用来跨进程加锁,未命名的相当于lock。
    所以说:在一个进程中使用它的场景真的不多。它的比较常用场景如:限制一个程序在一个计算机上只能允许运行一次:

    1. class Program
    2. {
    3. private static Mutex mutex = null;
    4. static void Main()
    5. {
    6. bool firstInstance;
    7. mutex = new Mutex(true, @"Global\MutexSampleApp", out firstInstance);
    8. try
    9. {
    10. if (!firstInstance)
    11. {
    12. Console.WriteLine("已有实例运行,输入回车退出……");
    13. Console.ReadLine();
    14. return;
    15. }
    16. else
    17. {
    18. Console.WriteLine("我们是第一个实例!");
    19. for (int i = 60; i > 0; --i)
    20. {
    21. Console.WriteLine(i);
    22. Thread.Sleep(1000);
    23. }
    24. }
    25. }
    26. finally
    27. {
    28. if (firstInstance)
    29. {
    30. mutex.ReleaseMutex();
    31. }
    32. mutex.Close();
    33. mutex = null;
    34. }
    35. }
    36. }

    需要注意的地方:

    new Mutex(true, @"Global\MutexSampleApp", out firstInstance)代码不会阻塞当前线程(即使第一个参数为true),在多进程协作的时候最后一个参数firstInstance很重要,要善于运用。
    mutex.WaitOne(30*1000)代码,当前进程正在等待获取锁的时候,已占用了这个命名锁的进程意外退出了,此时当前线程并不会直接获得锁然后向后执行,而是抛出异常AbandonedMutexException,所以在等待获取锁的时候要记得加上try catch。可以参照下面的代码:
     

    1. class Program
    2. {
    3. private static Mutex mutex = null;
    4. static void Main()
    5. {
    6. mutex = new Mutex(false, @"Global\MutexSampleApp");
    7. while (true)
    8. {
    9. try
    10. {
    11. Console.WriteLine("start wating...");
    12. mutex.WaitOne(20 * 1000);
    13. Console.WriteLine("enter success");
    14. Thread.Sleep(20 * 1000);
    15. break;
    16. }
    17. catch (AbandonedMutexException ex)
    18. {
    19. Console.WriteLine(ex.Message);
    20. continue;
    21. }
    22. }
    23. //do something
    24. mutex.ReleaseMutex();
    25. Console.WriteLine("Released");
    26. Console.WriteLine("ok");
    27. Console.ReadKey();
    28. }
    29. }

    5、并发集合

            C#中的并发集合包括ConcurrentQueue、ConcurrentStack、ConcurrentBag、ConcurrentDictionary和BlockingCollection等。这些集合不仅提供了线程安全的访问,而且还具有高效的并发性能。

    ConcurrentQueue是一个线程安全的队列,支持并发添加和删除元素。ConcurrentStack类似于ConcurrentQueue,不同之处在于它是一个栈而不是队列。ConcurrentBag则类似于一个集合,可以并发添加和删除元素,但不保证元素的顺序。ConcurrentDictionary是一个线程安全的字典,支持并发添加、删除和更新键值对。

    另外一个比较有用的并发集合是BlockingCollection,它是一个基于生产者消费者模式的并发集合。它提供了一种方便的方式来在多个线程之间传递数据。当集合为空时,从BlockingCollection中获取数据的线程将被阻塞,直到有新数据添加到集合中。当集合已满时,向BlockingCollection中添加数据的线程将被阻塞,直到有足够的空间可用。

    使用并发集合时,需要注意一些细节。例如,虽然并发集合是线程安全的,但是对于某些操作,如ConcurrentDictionary中的GetOrAdd方法,需要使用原子操作来确保线程安全。另外,由于并发集合具有高效的并发性能,因此在单线程环境下使用它们可能会导致性能下降。

    总之,在多线程编程中,C#中的并发集合是一种非常有用的工具,可以帮助我们更轻松地实现线程安全的数据共享和修改。对于需要在多个线程之间共享数据的应用程序,使用并发集合可以极大地简化编程工作,并提高应用程序的性能和可靠性。

    6. 悲观锁:

            所谓悲观锁,就是在进行操作时针对记录加上排他锁,这样其他事务如果想操作该记录,需要等待锁的释放。

    悲观锁在处理并发量和频繁访问时,等待时间比较长,冲突概率高,并发性能不好。

    7. 乐观锁

            乐观锁,是在提交对记录的更改时才将对象锁住,提交前需要检查数据的完整性。

  • 相关阅读:
    闭区间上连续函数的一些定理
    【C++】string的使用
    【分布式websocket】聊天系统消息加密如何做
    分析和比较深度学习框架 PyTorch 和 Tensorflow
    pycharm报错提示:无法加载文件\venv\Scripts\activate.ps1,因为在此系统上禁止运行脚本
    “程序包com.sun.tools.javac.util不存在” 问题解决
    [Azure VM] Azure virtual machine agent status is not ready
    初识MySQL数据库
    RIP动态路由配置
    Linux基础指令(六)
  • 原文地址:https://blog.csdn.net/weixin_57062986/article/details/133023083