一、多线程实现方式
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. 乐观锁
乐观锁,是在提交对记录的更改时才将对象锁住,提交前需要检查数据的完整性。