文章目录
一、同步阻塞,异步阻塞,同步非阻塞,异步非阻塞的理解
同步阻塞、异步阻塞、同步非阻塞和异步非阻塞主要是描述线程与消息通知机制的关系。
同步阻塞,就像公路上只有一个车道,所有车辆需要依次行驶,一次只能过一辆车。如果这条车道发生交通堵塞,那么后面的车辆就必须等待,此过程会阻塞后续的车辆。在计算机中,这相当于只有一个线程,并且该线程处于阻塞(Blocked)状态。
异步阻塞,则有两条或两条以上的车道可以通行,即不同任务可以并行执行。但是,如果所有的车道都发生了交通堵塞,那么所有的任务都必须等待,此过程同样会阻塞其他的任务。在计算机中,这相当于有两个或两个以上的线程,并且每个线程都在阻塞状态。
同步非阻塞,还是只有一条车道,不过这次没有交通堵塞,车辆可以正常通行。在计算机中,这相当于只有一个线程,并且该线程处于运行(Running)状态。
异步非阻塞,则是车道没有任何堵塞,车辆可以自由通行。在计算机中,这相当于有两个或两个以上的线程,并且每个线程都在运行状态。
总的来说,同步/异步关注的是消息通知的机制,而阻塞/非阻塞关注的是程序(线程)等待消息通知时的状态。也就是说,同步/异步是“下载完成消息”的通知方式(机制),而阻塞/非阻塞则是在等待“下载完成消息”通知过程中的状态(能否做其他任务)。
二、线程创建
1.C#创建多线程的三种方式
使用Thread类:
使用Thread类的构造方法来创建线程,支持以下两种委托
public delegate void ThreadStart();
public delegate void ParameterizedThreadStart (object? obj);
Thread类的构造函数可以接受两种类型的委托参数:ThreadStart和ParameterizedThreadStart。如果你不传入任何参数,那么默认就是使用ThreadStart类型的方法来启动线程。如果你需要向线程传递参数,那么就需要使用ParameterizedThreadStart类型的方法来启动线程。例如:
// 使用ThreadStart
ThreadStart threadStart = new ThreadStart(Calculate);
Thread thread = new Thread(threadStart);
thread.Start();
public void Calculate() { double Diameter =0.5; }
// 使用ParameterizedThreadStart
ParameterizedThreadStart pts = new ParameterizedThreadStart(DoWork);
Thread t = new Thread(pts);
t.Start("Hello");
public void DoWork(object data) { Console.WriteLine(data); }
属性和方法 | 作用 |
---|---|
Start() | 启动线程并执行线程的入口点。 |
Join() | 等待线程终止。将调用join的线程优先执行,当前正在执行的线程阻塞,直到调用join方法的线程执行完毕。 |
Interrupt() | 中断线程。 |
Sleep( int millisecondsTimeout ) | 让线程暂停一段时间。 |
Abort() | 终止线程。 |
Yield() | 让当前线程主动放弃执行权,允许其他线程获得执行权。 |
Name | 设置线程名称 |
Priority | 获取或设置线程的优先级 |
IsAlive | 获取当前线程是否处于启动状态 |
IsBackground | 该线程是否为后台线程 |
CurrentThread | 获取当前线程 |
线程优先级设定
在C#中,可以使用Thread.CurrentThread.Priority
属性来设置线程的优先级。这个属性接受一个整数值,表示线程的优先级。
Priority有个几个设置等级分别为:Lowest,BelowNormal,Normal,AboveNormal,Highest这几个等级,再通过设定thread.Priority来设置线程的优先执行顺序。
2.使用Task类:
public void DoWorkAsync()
{
// 创建一个异步任务
Task task = Task.Run(() =>
{
// 在这里执行耗时操作,例如读取文件或从数据库获取数据
Console.WriteLine("开始执行耗时操作...");
Thread.Sleep(3000); // 模拟耗时操作
Console.WriteLine("耗时操作完成!");
});
// 等待异步任务完成
task.Wait();//若后没有这一步等待,在主线程执行完之后会直接结束,在线程sleep及之后操作将不会执行
}
Task类在实际场景中常用的方法有许多,以下将介绍一些主要的方法:
Task.Run()
: 这个方法可以用于执行异步操作,并在操作完成后返回结果。使用这个方法可以将操作添加到线程池中,并返回一个Task对象,通过该对象可以获取操作的执行情况和结果。Task<T>.Result
: 对于有返回值的异步操作,可以使用此方法获取操作的结果。如果异步操作尚未完成,此方法会阻塞当前线程,直到操作完成并返回结果。Task.ContinueWith()
: 允许您在异步任务完成之后执行另一个委托。这个方法常用于在异步任务完成后执行一些后续的操作。Task.Wait()
: 此方法用于等待异步任务完成。与Task<T>.Result
不同的是,Task.Wait()
会阻塞当前线程,直到指定的任务完成。Task.Cancel()
: 如果您需要取消一个正在运行的任务,可以使用此方法来实现。当调用此方法时,如果任务尚未完成,它将被取消,并且如果任务正在执行某个可能会被取消的操作(如读取或写入操作),则该操作将立即停止。Task.WhenAll(tasks)
.NET中Task
类的一个静态方法,用于等待所有给定的任务完成。当所 有任务都完成时,它将返回一个 新的任务,该任务的结果是一个包含所有输入任务结果的数组。如果 任何一个输入任务失败,则返回的新任务也会失败。
配置Task的创建和执行行为
TaskCreationOptions 是一个枚举类型,它包含了一组用于指定创建任务时的行为和优先级的选项。以下是 TaskCreationOptions 中包含的选项:
选项 | 作用 |
---|---|
None | 没有启用任何选项,这是一个默认值。 |
PreferFairness | 启用公平性优先级。这会使得任务调度器在分配任务时尽可能地平衡各个任务之间的执行时间,以避免某些任务长时间占用 CPU 时间而其他任务等待时间过长。 |
LongRunning | 启用长时间运行优先级。这会使得任务调度器在分配任务时优先考虑长时间运行的任务,以充分利用系统资源并减少任务切换的开销。 |
AttachedToParent | 启用附加到父任务的选项。这会使得任务作为另一个任务的子任务创建,子任务会随着父任务的完成而自动完成。 |
DenyChildAttach | 禁用子任务附加。这会阻止任务被附加到其他任务作为子任务。 |
HideScheduler | 隐藏调度程序。这会使得任务在创建时不显示与调度程序相关的信息。 |
RunContinuationsAsynchronously | 异步运行延续。这会使得任务的延续(如果存在)在另一个线程中异步执行,而不是在当前任务完成后再执行。 |
协作取消模式
C#中的协作取消模式通常使用CancellationTokenSource
和CancellationToken
来实现。
- CancellationTokenSource:这是一个可以取消令牌源的对象,它包含一个CancellationToken实例。你可以使用CancellationTokenSource的Cancel方法来取消与之关联的令牌。当你调用Cancel方法时,与该令牌源关联的所有取消请求都会被发送到所有注册的监听器。
- CancellationToken:这是一个表示取消请求的标记。当你想要取消某个异步操作时,你可以创建一个CancellationToken并将其传递给异步操作。异步操作可以通过检查CancellationToken的IsCancellationRequested属性来确定是否应该取消其执行。如果IsCancellationRequested为true,那么异步操作应该立即停止执行并返回。
总的来说,CancellationTokenSource是一个可以创建和管理CancellationToken的工具,而CancellationToken则是一个表示取消请求的标志。
下面,让我给你举个例子来说明CancellationTokenSource和CancellationToken的用法:
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
Task.Run(() =>
{
while (true)
{
Console.WriteLine("正在执行");
Console.WriteLine("休眠3秒结束");
Thread.Sleep(3000);
cancellationToken.ThrowIfCancellationRequested();
}
},cancellationToken);
Thread.Sleep(3000);
Console.WriteLine("已经休眠3秒");
cancellationTokenSource.Cancel();
通过cancellationTokenSource
调用Cancel()
方法来进行对IsCancellationRequested
这个属性进行修改,当ThrowIfCancellationRequested()
得到了IsCancellationRequested
这个属性被修改之后则会调用ThrowOperationCanceledException()
这个方法来结束。
ThrowIfCancellationRequested()这个方法的源码还是蛮简单的,能看看:
public void ThrowIfCancellationRequested()
{
if (!this.IsCancellationRequested)
return;
this.ThrowOperationCanceledException();
}
所以在调用Cancel()
之后线程里面的token得到了IsCancellationRequested这个指令,所以就取消了线程。
3.线程池
public static void MyAction(Object state)
{
Console.WriteLine("ThreadPool-----创建线程");
}
ThreadPool.QueueUserWorkItem(MyAction);
ThreadPool.QueueUserWorkItem((stat) =>
{
Console.WriteLine("ThreadPool创建线程");
});
Console.ReadKey();
常用方法 | 作用 |
---|---|
QueueUserWorkItem() | 将工作项添加到线程池队列中。当线程池中的线程可用时,它将执行队列中的工作项。 |
SetMaxThreads() | 设置线程池的最大线程数。 |
SetMinThreads() | 设置线程池的最小线程数。 |
GetMaxThreads() | 获取线程池的最大线程数。 |
GetMinThreads() | 获取线程池的最小线程数。 |
GetAvailableThreads() | 获取当前系统中可用的线程数 |
SetMaxQueueLength() | 设置线程池中任务队列的最大长度。 |
线程变量相关的知识:
volatile关键字
用于修饰变量,表示该变量的值可能会被其他线程修改,因此编译器不会对其进行优化。这在多线程编程中非常有用,因为它可以确保每个线程都能看到共享变量的最新值。
ThreadStatic特性
被ThreadStatic标记的static字段不会在线程间共享,每个执行线程都有一个单独的字段实例。
三、多线程锁
在C#中,排他锁(Exclusive Lock)是一种同步机制,用于控制对共享资源的访问。当一个线程获得排他锁后,其他线程将无法获取该锁,直到当前线程释放锁。这样可以确保在同一时间只有一个线程可以访问共享资源,从而避免数据竞争和不一致的问题。
1、lock关键字
第一种就是lock,lock的底层是用Monitor的Enter和Exit实现的。
private static object lockObject = new object();
public void ExclusiveMethod()
{
lock (lockObject)
{
// 在这里执行需要同步的代码块
}
}
lock的对象选择
C#中的lock关键字用于实现线程同步,它需要一个对象作为锁,并且锁的对象要是static且不能是值类型,原因如下:
在lock里面不能传入一个值类型,因为lock需要一个对象作为锁的标识符。值类型是按值传递的,每次调用方法时都会创建一个新的副本,因此无法用作锁的标识符。而引用类型是按引用传递的,它们在内存中的地址是唯一的,可以用作锁的标识符。
为什么要加static?因为静态成员变量在整个应用程序的生命周期内只有一个实例,这意味着多个线程可以共享同一个锁对象。如果不加static,每次调用lock方法时都会创建一个新的锁对象,这将导致每个线程都有自己的锁,无法实现线程同步。
2、SpinLock自旋锁
自旋锁是一种线程同步的机制,当一个线程试图获取已经被其他线程持有的锁时,该线程将循环等待并持续判断锁是否能够被成功获取。只有当该线程成功获取到锁后,它才会退出循环。
SpinLock是一种自旋锁,用于在多线程环境中保护共享资源。当一个线程需要访问共享资源时,它会尝试获取锁。如果锁已经被其他线程持有,那么该线程会不断地循环检查锁的状态,直到成功获取到锁为止。这种机制可以有效地减少线程之间的竞争,提高程序的性能。
在C#中,可以使用SpinLock类来实现自旋锁。以下是一个简单的示例:
static int count = 0;
static object lockObj = new object();
private static SpinLock _s= new SpinLock();
public void run()
{
// 创建两个线程,分别执行Increment和Decrement操作
Thread thread1 = new Thread(new ThreadStart(Decrement));
Thread thread2 = new Thread(new ThreadStart(Decrement));
Thread thread3 = new Thread(new ThreadStart(Decrement));
thread1.Start();
thread3.Start();
thread2.Start();
//
// thread1.Join(1000);
// thread2.Join(1000);
Console.WriteLine("最终计数值: " + count);
}
static void Decrement()
{
for (int i = 0; i < 10000; i++)
{
bool x = false;
_s.Enter(ref x);
count++;
Console.WriteLine(Thread.CurrentThread.ManagedThreadId+": " + count);
_s.Exit();
}
}
3、Monitor类
第二种就是Monitor。Monitor类是一个内置对象,用作线程同步的基石。它主要有两个方法:Enter和Exit。
- Enter方法:当一个线程首次调用一个对象的Monitor的Enter方法时,它会获取该对象的锁。如果其他线程已经持有该锁,那么当前线程会被阻塞,直到拥有锁的线程调用Exit方法释放该锁。一旦当前线程获取到锁,它可以执行需要同步的代码段。
- Exit方法:当一个线程完成了对共享资源的修改后,它应调用对应对象的Monitor的Exit方法来释放锁,以便其他等待的线程可以获取该锁并访问共享资源。
private int _count = 0;
private readonly object _lock = new object();
public void run()
{
// 创建两个线程,分别执行Increment和Decrement操作
Thread thread1 = new Thread(new ThreadStart(Increment));
Thread thread2 = new Thread(new ThreadStart(Decrement));
thread1.Start();
thread2.Start();
// thread1.Join();
// thread2.Join();
Console.WriteLine("最终计数值: " + count);
}
static void Increment()
{
for (int i = 0; i < 1000; i++)
{
lock (lockObj)
{
count++;
Console.WriteLine(Thread.CurrentThread.ManagedThreadId+": " + count);
}
}
}
static void Decrement()
{
for (int i = 0; i < 1000; i++)
{
lock (lockObj)
{
count--;
Console.WriteLine(Thread.CurrentThread.ManagedThreadId+": " + count);
}
}
}
在上面的例子中,我们定义了一个Counter类,其中有一个私有变量_count
表示计数器的值,还有一个私有对象_lock
作为锁的标识符。Increment和Decrement方法分别用于增加和减少计数器的值,它们都使用了Monitor类的Enter和Exit方法来获取和释放锁。当一个线程进入这些方法时,它会获取到_lock
对象的锁,其他线程必须等待该线程释放锁才能继续执行。这样可以保证多个线程对计数器的操作是互斥的,避免了竞争条件的发生。
4、Mutex类
第三种则是Mutex。Mutex类用于实现多线程同步,确保同一时间只有一个线程可以访问共享资源。以下是一个简单的例子来说明如何使用Mutex类。
private static readonly Mutex myMutex = new Mutex(false, "MyMutex",out bool outOwned);
-
第一个参数是一个布尔值,表示互斥量是否已经锁定。如果设置为true,则表示互斥量已经锁定,简单来说就是如果设置为true的话,就只能被所创建的线程所调用,如:
Thread th1 = new Thread(Thread1); Thread th2 = new Thread(Thread2); th1.Start(); th2.Start(); static void Thread1() { Console.WriteLine("Thread1已经被创建--------------------------"); Mutex m = new Mutex(true, "a", out bool NewMutex); //如果名为"HAHA"的互斥锁不存在,返回参数bCreatedNewMutex为true,否则为false if (NewMutex) //如果名为“a”的互斥锁不存在 { Console.WriteLine("Thread1执行成功"); } else { Console.WriteLine("Thread1执行失败"); } try { m.WaitOne(); Console.WriteLine("Thread1在里面已经被调用------------------"); } catch (Exception e) { Console.WriteLine(e); throw; } finally { m.ReleaseMutex(); } } static void Thread2() { Console.WriteLine("Thread2已经被创建------------------------------"); Mutex m = new Mutex(true, "a", out bool NewMutex); if (NewMutex) { Console.WriteLine("Thread2执行成功"); } else { Console.WriteLine("Thread2执行失败"); } try { m.WaitOne(); Console.WriteLine("Thread2在里面已经被调用------------------"); } catch (Exception e) { Console.WriteLine(e); throw; } finally { m.ReleaseMutex(); } } //两个线程所创建的Mutex名一样 //结果1 //Thread1已经被创建-------------------------- //Thread2已经被创建------------------------------ //Thread2执行失败 //Thread1执行成功 //Thread1在里面已经被调用------------------ //结果2 //Thread2已经被创建------------------------------ //Thread1已经被创建-------------------------- //Thread1执行失败 //Thread2执行成功 //Thread2在里面已经被调用------------------ //两个线程所创建的Mutex名不一样 //Thread2已经被创建------------------------------ //Thread1已经被创建-------------------------- //Thread2执行成功 //Thread2在里面已经被调用------------------ //Thread1执行成功 //Thread1在里面已经被调用------------------
在这2个线程中,只有先创建名为a的Mutex才可以被调用,NewMutex才会为true才能够在
WaitOne()和ReleaseMutex()之间调用。
当然如果Mutex所创建的名不一样的话,所创建的锁也不一样,所使用的锁也不一样。两个线程都将能执行。
如果设置为false,则表示互斥量未锁定。则为互斥,实现线程同步。
-
第二个参数是一个字符串,用于指定互斥量的名称。互斥量的名称用于在调试过程中识别和跟踪互斥量的状态。
-
Mutex的第三个参数是继承权限。它用于指定创建的Mutex是否允许线程继承其访问权限。如果将第三个参数设置为true,则子线程可以继承该Mutex的访问权限;如果设置为false,则子线程无法继承该Mutex的访问权限。默认情况下,第三个参数为false。
5、信号量Semaphore
C#中的多线程信号量是一种同步原语,用于控制对共享资源的访问。它允许多个线程同时访问共享资源,但在访问过程中需要遵循一定的规则,以确保资源的一致性和避免竞争条件。
在C#中,可以使用Semaphore
类来实现多线程信号量。
Semaphore sema = new Semaphore(2, 2);// 创建一个信号量,允许2个线程同时访问,最多允许2个线程等
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(AccessResource);
thread.Start();
}
void AccessResource()
{
sema.WaitOne(); // 请求访问共享资源
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 开始访问资源");
Thread.Sleep(1000); // 模拟访问资源所需的时间
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 结束访问资源");
sema.Release(); // 释放访问共享资源的许可
}
/*结果
线程 9 开始访问资源
线程 7 开始访问资源
线程 9 结束访问资源
线程 7 结束访问资源
线程 8 开始访问资源
线程 10 开始访问资源
线程 8 结束访问资源
线程 10 结束访问资源
线程 11 开始访问资源
线程 11 结束访问资源*/
在这个示例中,我们创建了一个信号量,允许2个线程同时访问共享资源,最多允许2个线程等待。然后,我们创建了5个线程,每个线程都会尝试访问共享资源。由于信号量的设置,这5个线程将按照顺序访问共享资源,而不是同时访问。
6、ReaderWriterLockSlim读写锁
ReaderWriterLockSlim是一个读写锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这可以提高多线程访问共享资源的性能。
int value = 0;
object lockObject = new object();
ReaderWriterLockSlim readerWriterLock = new ReaderWriterLockSlim();
// 创建两个任务,分别执行读操作和写操作
Task readTask = Task.Run(Read);
Task readTask1 = Task.Run(Read);
Task readTask2 = Task.Run(Read);
Task readTask3 = Task.Run(Read);
var startNew = Task.Factory.StartNew(() => Write(),TaskCreationOptions.LongRunning);
var startNew1 = Task.Factory.StartNew(() => Write(),TaskCreationOptions.LongRunning);
// 等待任务完成
readTask.Wait();
readTask1.Wait();
readTask2.Wait();
readTask3.Wait();
startNew.Wait();
startNew1.Wait();
void Read()
{
readerWriterLock.EnterReadLock();
try
{
Thread.Sleep(3000);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId+$":Read: {value}");
}
finally
{
readerWriterLock.ExitReadLock();
}
}
void Write()
{
readerWriterLock.EnterWriteLock();
try
{
value = new Random().Next(1, 100);
Thread.Sleep(3000);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId+$":Write: {value}");
}
finally
{
readerWriterLock.ExitWriteLock();
}
}
//结果
//6:Read: 0
//10:Read: 0
//7:Read: 0
//11:Read: 0
//Write: 98
//Write: 53
4、有关多线程的安全集合框架
在C#中,.NET框架提供了一些集合类,这些类是线程安全的,可以在多线程环境下使用。这些类包括:
- BlockingCollection:这是一个线程安全的集合类,主要用于在多线程环境下进行数据共享和通信。它提供了阻塞机制,使得生产者和消费者可以协调数据的生产和消费。当集合为空时,获取元素的操作会被阻塞,直到有元素可用;当集合已满时,添加元素的操作也会被阻塞,直到有空间可用。
- ConcurrentQueue:这是一个线程安全的队列实现,用于提供线程间的安全出队和入队操作。它支持多线程的并发访问,可以安全地进行入队和出队操作,而不会出现竞争条件或死锁等问题。
- ConcurrentDictionary<TKey, TValue>:这是一个线程安全的字典实现,用于在多线程环境下进行键值对的存储和访问。它提供了线程安全的键值对插入、删除和查找操作,适用于多线程环境下的数据共享和访问。
- ConcurrentBag:这是一个线程安全的非有序集合实现,用于在多线程环境下进行元素的添加和删除操作。它没有提供对元素的索引访问或排序操作,但可以在多线程环境下安全地进行元素的添加和删除操作。
- ConcurrentStack:这些类都位于
System.Collections.Concurrent
命名空间下,为开发人员提供了丰富的多线程安全集合操作和功能,适用于各种并发场景下的数据安全和性能需求。在使用时需要根据具体的应用场景选择合适的类来实现多线程安全的数据访问和控制。
1、BlockingCollection阻塞队列
BlockingCollection是.NET Framework中的一个类,它位于System.Collections.Concurrent命名空间下。它是一个线程安全的集合类,可以用于多线程环境下的数据共享。
BlockingCollection提供了添加、移除和读取集合中元素的方法,并且这些方法都是线程安全的。此外,它还提供了阻塞读取和阻塞写入的方法,这些方法可以在集合为空时阻塞读取操作,或者在集合已满时阻塞写入操作。
使用BlockingCollection可以避免多线程环境下的竞争条件和死锁等问题,并且可以提高程序的效率和性能。
使用场景
BlockingCollection
是 C# 中的一个非常有用的类,主要用于在并行编程中同步数据的访问和修改。下面是一些使用 BlockingCollection
的典型场景:
- 多线程数据收集和生产者-消费者模式:当你有一个生产者线程生成数据,并希望多个消费者线程能并行处理这些数据时,
BlockingCollection
非常有用。生产者将数据添加到集合中,而消费者从集合中获取并处理数据。如果集合为空,消费者会自动等待直到有数据可用。如果集合已满,生产者会自动等待直到有空间可用。 - 并行任务协调:当你有多个并行任务需要共享一个公共数据集合,并且需要确保在任何时刻,只有一个任务可以访问或修改这个集合时,可以使用
BlockingCollection
。通过使用BlockingCollection
,你可以很容易地实现数据的互斥访问。 - 缓冲区处理:
BlockingCollection
可以作为一个缓冲区,在生产者和消费者之间提供一个数据中转站。这可以防止生产者过度生产和消费者过快消费,从而平衡两者之间的速率。 - 并行批处理:当你需要并行处理大量数据时,可以使用
BlockingCollection
来存储和处理数据。由于BlockingCollection
支持线程安全的数据修改,因此可以用来实现高效的并行批处理。
下面是一个简单的使用 BlockingCollection
的示例:
int count = 0 ;
BlockingCollection<string> blockingCollection = new();
//生产者
Task.Factory.StartNew(() =>
{
Console.WriteLine("开始添加");
while (true)
{
blockingCollection.TryAdd("String: " + count);
count++;
if (count > 10)
{
blockingCollection.CompleteAdding();
}
}
});
//消费者
Task.Factory.StartNew(() =>
{
Console.WriteLine("开始消费");
foreach (var element in blockingCollection.GetConsumingEnumerable())
{
Thread.Sleep(1000);
Console.WriteLine("Work: " + element);//Dump 为工具Linq的功能
}
}).Wait();
Console.ReadKey();
这段代码是一个简单的生产者-消费者模型,使用了C#的BlockingCollection类。BlockingCollection是一个线程安全的集合,可以在多个线程之间共享和操作。
- 首先,定义一个整数变量count并初始化为0,用于计数。
- 创建一个BlockingCollection实例,用于存储字符串类型的数据。
- 创建一个生产者任务,使用Task.Factory.StartNew方法启动。生产者任务的功能是不断地向BlockingCollection中添加元素,直到count大于10时,调用blockingCollection.CompleteAdding()方法表示添加完成。
- 创建一个消费者任务,同样使用Task.Factory.StartNew方法启动。消费者任务的功能是从BlockingCollection中获取元素并处理,每次处理完一个元素后,线程休眠1秒(模拟实际处理过程)。
- 最后,使用Console.ReadKey()方法等待用户按键,使程序保持运行状态。
常用方法
方法 | 作用 |
---|---|
Add() | 向集合中添加一个元素。 |
TryAdd() | 尝试向集合中添加一个元素,如果集合已满则返回false。 |
Take() | 从集合中取出一个元素并删除。 |
TryTake() | 尝试从集合中取出一个元素并删除,如果集合为空则返回false。 |
CompleteAdding() | 标记集合为已完成添加操作,以便消费者线程知道何时停止。 |
GetEnumerator() | 返回一个可枚举的迭代器。 |
int count = 0 ;
BlockingCollection<string> blockingCollection = new();
//生产者
Task.Factory.StartNew(() =>
{
Console.WriteLine("开始添加");
while (true)
{
blockingCollection.TryAdd("String: " + count);
count++;
if (count > 10)
{
blockingCollection.CompleteAdding();
}
}
});
//消费者
Task.Factory.StartNew(() =>
{
Console.WriteLine("开始消费");
foreach (var element in blockingCollection.GetConsumingEnumerable())
{
Thread.Sleep(1000);
Console.WriteLine("Work: " + element);//Dump 为工具Linq的功能
}
}).Wait();
Console.ReadKey();
2、ConcurrentQueue
C#中的ConcurrentQueue是一个线程安全的队列,它允许多个线程同时访问和修改队列。与普通的队列不同,ConcurrentQueue使用锁来确保在多线程环境下的安全性。
以下是ConcurrentQueue的一些主要特点:
- 线程安全:ConcurrentQueue支持多个线程同时访问和修改队列,而不需要额外的同步机制。
- 无阻塞操作:ConcurrentQueue提供了一些无阻塞的操作方法,如TryDequeue、TryPeek等,这些方法可以在不等待的情况下尝试获取或查看队列中的元素。
- 高效性能:由于使用了锁来确保线程安全,ConcurrentQueue的性能可能略低于其他线程安全的队列实现。但是,在大多数情况下,这种性能差异可以忽略不计。
- 泛型支持:ConcurrentQueue支持泛型,这意味着你可以使用任何类型的对象作为队列的元素。
ConcurrentQueue的一些常用方法和属性:
方法 | 作用 |
---|---|
Enqueue(TItem) | 将指定的元素添加到队列的末尾。 |
TryDequeue(out TResult result) | 尝试从队列中移除并返回第一个元素,如果队列为空,则返回false并将result设置为null。 |
TryPeek(out TResult result) | 尝试获取队列中的第一个元素,但不将其从队列中移除,如果队列为空,则返回false并将result设置为null。 |
IsEmpty | 返回一个布尔值,表示队列是否为空。 |
Count | 获取队列中的元素数量。 |
Clear() | 从队列中移除所有元素。 |
Contains(TItem item) | 返回一个布尔值,表示队列是否包含指定的元素。 |
CopyTo(TItem[] array, int index) | 将队列中的元素复制到指定的数组中,从指定的索引位置开始。 |
ToArray() | 返回一个包含队列中所有元素的数组。 |
ToList() | 返回一个包含队列中所有元素的List。 |
这些方法可以帮助您在多线程环境中安全地进行队列操作。请注意,ConcurrentQueue是一个线程安全集合类,但它并不保证元素的排序。
下面是一个简单的示例,展示了如何使用ConcurrentQueue:
ConcurrentQueue<int> queue = new ConcurrentQueue<int>();
// 生产者线程
Thread producer = new Thread(() =>
{
for (int i = 0; i < 10; i++)
{
queue.Enqueue(i);
Console.WriteLine($"生产者添加了元素: {i}");
}
});
// 消费者线程
Thread consumer = new Thread(() =>
{
while (true)
{
if (queue.TryDequeue(out int item))
{
Console.WriteLine($"消费者消费了元素: {item}");
}
else
{
break;
}
}
});
producer.Start();
consumer.Start();
producer.Join();
consumer.Join();
3、ConcurrentDictionary<TKey, TValue>
C#中的ConcurrentDictionary是一个线程安全的字典,它允许多个线程同时访问和修改字典。与普通的字典不同,ConcurrentDictionary使用锁来确保在多线程环境下的安全性。
以下是ConcurrentDictionary的一些主要特点:
- 线程安全:ConcurrentDictionary支持多个线程同时访问和修改字典,而不需要额外的同步机制。
- 高效性能:由于使用了锁来确保线程安全,ConcurrentDictionary的性能可能略低于其他线程安全的字典实现。但是,在大多数情况下,这种性能差异可以忽略不计。
- 泛型支持:ConcurrentDictionary支持泛型,这意味着你可以使用任何类型的键和值作为字典的元素。
- 并发集合操作:ConcurrentDictionary提供了一些并发集合操作方法,如TryAdd、TryUpdate等,这些方法可以在不等待的情况下尝试添加或更新字典中的元素。
以下是ConcurrentDictionary的一些常用方法:
方法 | 作用 |
---|---|
Add(TKey key, TValue value) | 将一个键值对添加到字典中。 |
TryAdd(TKey key, TValue value) | 尝试将一个键值对添加到字典中,如果键已经存在,则返回false,否则返回true。 |
Remove(TKey key) | 从字典中移除指定的键及其对应的值。 |
ContainsKey(TKey key) | 返回一个布尔值,表示字典是否包含指定的键。 |
TryGetValue(TKey key, out TValue value) | 尝试获取指定键对应的值,如果键存在,则返回true并将值存储在out参数中,否则返回false并将value设置为null。 |
TryUpdate(TKey key, TValue newValue, TValue comparer) | 尝试更新指定键的值,如果键存在且值相等,则更新为newValue,否则返回false。 |
GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) | 获取指定键的值,如果键不存在,则使用valueFactory函数创建一个新值并添加到字典中。 |
GetOrRemove(TKey key, out TValue value) | 获取指定键的值,如果键不存在,则将value设置为null并返回true,否则返回false并将value设置为键对应的值。 |
TryRemove(TKey key) | 尝试从字典中移除指定的键及其对应的值,如果成功则返回true,否则返回false。 |
Clear() | 从字典中移除所有键值对。 |
这些方法可以帮助您在多线程环境中安全地进行键值对的存储和访问操作。请注意,ConcurrentDictionary是一个线程安全集合类,但它并不保证键值对的排序。
下面是一个简单的示例,展示了如何使用ConcurrentDictionary:
ConcurrentDictionary<int, string> dictionary = new ConcurrentDictionary<int, string>();
// 生产者线程
Thread producer = new Thread(() =>
{
for (int i = 0; i < 10; i++)
{
dictionary.TryAdd(i, $"Value {i}");
Console.WriteLine($"生产者添加了元素: {i} - {dictionary[i]}");
}
});
// 消费者线程
Thread consumer = new Thread(() =>
{
for (int i = 0; i < 10; i++)
{
dictionary.TryRemove(i, out string value);
Console.WriteLine($"消费者消费了元素: {i} - {value}");
}
});
producer.Start();
consumer.Start();
producer.Join();
consumer.Join();
4、ConcurrentBag
ConcurrentBag 是 .NET 中的一个线程安全集合类,位于 System.Collections.Concurrent 命名空间下。它提供了在多线程环境中进行无序的、不重复的元素收集操作的方法。
ConcurrentBag 的主要特点是不保证元素的顺序,并且不会抛出异常,即使在并发环境中插入重复的元素。它适用于需要高效、线程安全且无序的元素收集操作的场景。
方法 | 作用 |
---|---|
Add(T item) | 将一个元素添加到 ConcurrentBag 中。 |
TryTake(out T item) | 尝试从 ConcurrentBag 中移除并返回第一个元素,如果 ConcurrentBag 为空,则返回 false,item 保持不变。 |
Count | 获取 ConcurrentBag 中的元素数量。 |
Clear() | 从 ConcurrentBag 中移除所有元素。 |
Contains(T item) | 返回一个布尔值,表示 ConcurrentBag 是否包含指定的元素。 |
下面是一个简单的示例,展示了如何使用ConcurrentBag:
ConcurrentBag<int> bag = new ConcurrentBag<int>();
// 生产者线程
Thread producer = new Thread(() =>
{
for (int i = 0; i < 10; i++)
{
bag.Add(i);
Console.WriteLine($"生产者添加了元素: {i}");
}
});
// 消费者线程
Thread consumer = new Thread(() =>
{
for (int i = 0; i < 10; i++)
{
bag.TryTake(out int value);
Console.WriteLine($"消费者消费了元素: {value}");
}
});
producer.Start();
consumer.Start();
producer.Join();
consumer.Join();
5、ConcurrentStack
ConcurrentStack<T>
是 .NET 中的一个线程安全集合类,位于 System.Collections.Concurrent
命名空间下。它提供了在多线程环境中进行栈操作的方法。
栈(Stack)是一种遵循“先进后出”(LIFO)原则的线性数据结构,只允许在一端进行插入和删除操作,这一端称为栈顶。ConcurrentStack 中:
方法 | 作用 |
---|---|
Push(T item) | 方法用于将一个元素压入栈顶。 |
TryPop(out T item) | 方法尝试从栈顶弹出并返回元素,如果栈为空,则返回 false,item 保持不变。 |
IsEmpty | 属性表示栈是否为空。 |
Count | 属性获取栈中的元素数量。 |
Clear() | 方法用于清空栈。 |
下面是使用 ConcurrentStack<T>
的示例代码:
// 创建一个线程安全的栈
ConcurrentStack<int> stack = new ConcurrentStack<int>();
// 第一个线程:将数字压入栈中
Task.Run(() =>
{
for (int i = 1; i <= 5; i++)
{
stack.Push(i); // 将数字压入栈中
Console.WriteLine($"Thread 1: Pushed {i}");
Task.Delay(1000).Wait(); // 模拟延迟
}
});
// 第二个线程:从栈中弹出数字并处理
Task.Run(() =>
{
while (true)
{
int number;
if (stack.TryPop(out number)) // 尝试从栈中弹出数字
{
Console.WriteLine($"Thread 2: Popped {number}");
Task.Delay(1000).Wait(); // 模拟延迟
}
else // 栈为空,退出循环
{
break;
}
}
});
// 等待两个线程执行完毕
Task.WaitAll();
Console.ReadKey();
在这个例子中,我们创建了一个 ConcurrentStack<int>
对象来存储整数。第一个线程将数字 1 到 5 压入栈中,每次压入后都会打印一条消息。第二个线程从栈中弹出数字并处理,每次弹出后也会打印一条消息。两个线程交替执行,由于 ConcurrentStack<T>
是线程安全的,它们不会产生竞争条件或数据不一致的问题。