C#多线程

本文介绍了C#中多线程的不同实现方式,如使用Thread类、ThreadPool和Task类,以及C#5引入的async/await关键字的优势。还详细讨论了线程安全问题,如竞争条件、死锁和内存泄漏,以及如何通过互斥锁、原子操作和并发集合来解决这些问题。此外,文中还提到了悲观锁和乐观锁的概念及其在并发控制中的应用。
摘要由CSDN通过智能技术生成

一、多线程实现方式
        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))这种写法,不过还是建议用私有字段做锁,下面给出锁的几中应用场景:

class Program
{
   private readonly object lockObj = new object();
   private object obj = null;
   public void TryInit()
   {
       if (obj == null)
       {
           lock (lockObj)
           {
               if (obj == null)
               {
                   obj = new object();
               }
           }
       }
   }
}

自动编号

class DemoService
{
      private static int id;
      private static readonly object lockObj = new object();
      public void Action()
      {
          //do something
          int newid;
          lock (lockObj)
          {
              newid = id + 1;
              id = newid;
          }
          //use newid...
      }
  }

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

lock (typeof(Program))
{
    int i = 0;
    //do something
}

被编译成IL后就变成了:

try
{
     Monitor.Enter(typeof(Program));
     int i = 0;
     //do something
 }
 finally
 {
     Monitor.Exit(typeof(Program));
 }
注意: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

 

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
}

3、ReaderWriteLock[Slim]

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

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

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

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

 并行读

using System;
using System.Threading;

class Program
{
    //static ReaderWriterLock readerWriterLock = new ReaderWriterLock();
    static ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();
    public static void Main(string[] args)
    {
        var thread = new Thread(() =>
        {
            Console.WriteLine("thread1 start...");
            //readerWriterLock.AcquireReaderLock(3000);
            readerWriterLock.TryEnterReadLock(3000);
            int index = 0;
            while (true)
            {
                index++;
                Console.WriteLine("du...");
                Thread.Sleep(1000);
                if (index > 6) break;
            }
            //readerWriterLock.ReleaseReaderLock();
            readerWriterLock.ExitReadLock();
        });
        thread.Start();
        var thread2 = new Thread(() =>
        {
            Console.WriteLine("thread2 start...");
            //readerWriterLock.AcquireReaderLock(3000);
            readerWriterLock.TryEnterReadLock(3000);
            int index = 0;
            while (true)
            {
                index++;
                Console.WriteLine("读...");
                Thread.Sleep(1000);
                if (index > 6) break;
            }
            //readerWriterLock.ReleaseReaderLock();
            readerWriterLock.ExitReadLock();
        });
        thread2.Start();

        Console.ReadLine();
    }
}

串行写

using System;
using System.Threading;

class Program
{
    //static ReaderWriterLock readerWriterLock = new ReaderWriterLock();
    static ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();
    public static void Main(string[] args)
    {
        var thread = new Thread(() =>
        {
            Console.WriteLine("thread1 start...");
            //readerWriterLock.AcquireWriterLock(1000);
            readerWriterLock.TryEnterWriteLock(1000);
            Console.WriteLine("写...");
            Thread.Sleep(5000);
            Console.WriteLine("写完了...");
            //readerWriterLock.ReleaseReaderLock();
            readerWriterLock.ExitWriteLock();
        });
        thread.Start();
        var thread2 = new Thread(() =>
        {
            Console.WriteLine("thread2 start...");
            try
            {
                //readerWriterLock.AcquireReaderLock(2000);
                readerWriterLock.TryEnterReadLock(2000);
                Console.WriteLine("du...");
                //readerWriterLock.ReleaseReaderLock();
                readerWriterLock.ExitReadLock();
                Console.WriteLine("du wan...");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        });
        Thread.Sleep(100);
        thread2.Start();

        Console.ReadLine();
    }
}

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

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

4.mutex

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

class Program
{
    private static Mutex mutex = null;
    static void Main()
    {
        bool firstInstance;
        mutex = new Mutex(true, @"Global\MutexSampleApp", out firstInstance);
        try
        {
            if (!firstInstance)
            {
                Console.WriteLine("已有实例运行,输入回车退出……");
                Console.ReadLine();
                return;
            }
            else
            {
                Console.WriteLine("我们是第一个实例!");
                for (int i = 60; i > 0; --i)
                {
                    Console.WriteLine(i);
                    Thread.Sleep(1000);
                }
            }
        }
        finally
        {
            if (firstInstance)
            {
                mutex.ReleaseMutex();
            }
            mutex.Close();
            mutex = null;
        }
    }
}

需要注意的地方:

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

class Program
{
    private static Mutex mutex = null;
    static void Main()
    {
        mutex = new Mutex(false, @"Global\MutexSampleApp");
        while (true)
        {
            try
            {
                Console.WriteLine("start wating...");
                mutex.WaitOne(20 * 1000);
                Console.WriteLine("enter success");
                Thread.Sleep(20 * 1000);
                break;
            }
            catch (AbandonedMutexException ex)
            {
                Console.WriteLine(ex.Message);
                continue;
            }
        }
        //do something

        mutex.ReleaseMutex();
        Console.WriteLine("Released");
        Console.WriteLine("ok");
        Console.ReadKey();
    }
}

5、并发集合

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

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

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

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

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

6. 悲观锁:

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

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

7. 乐观锁

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值