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))这种写法,不过还是建议用私有字段做锁,下面给出锁的几中应用场景:
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 somethingint 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 somethingmutex.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. 乐观锁
乐观锁,是在提交对记录的更改时才将对象锁住,提交前需要检查数据的完整性。