首页 > Python基础教程 >
-
C#教程之C# 多线程学习笔记 - 2(2)
生产消费者队列的构成如下所描述的一致。
- 构建一个队列,用于存放需要执行的工作项。
- 如果有新的任务需要执行,将其放在队列当中。
- 一个或多个工作线程在后台执行,从队列中拿取工作项执行,将其消费。
生产/消费者队列可以精确控制工作线程的数量,CLR 的线程池就是一种生产/消费者队列。
结合 AutoResetEvent
事件等待句柄,我们可以很方便地实现一个生产/消费者队列。
class Program
{
static void Main(string[] args)
{
using (var queue = new ProducerConsumerQueue())
{
queue.EnqueueTask("Hello");
for (int i = 0; i < 10; i++)
{
queue.EnqueueTask($"{i}");
}
queue.EnqueueTask("End");
}
}
}
public class ProducerConsumerQueue : IDisposable
{
private readonly EventWaitHandle _waitHandle = new AutoResetEvent(false);
private readonly object _locker = new object();
private readonly Queue<string> _taskQueue = new Queue<string>();
private readonly Thread _workThread;
public ProducerConsumerQueue()
{
_workThread = new Thread(Work);
_workThread.Start();
}
public void EnqueueTask(string task)
{
// 向队列当中插入任务,加锁保证线程安全
lock (_locker)
{
_taskQueue.Enqueue(task);
}
// 通知工作线程开始干活
_waitHandle.Set();
}
private void Work()
{
while (true)
{
string task = null;
lock (_locker)
{
if (_taskQueue.Count > 0)
{
task = _taskQueue.Dequeue();
if (task == null) return;
}
}
if (task != null)
{
Thread.Sleep(100);
Console.WriteLine($"正在处理任务 {task}");
}
else
{
// 如果任务等于空则阻塞线程,等待心的工作项
_waitHandle.WaitOne();
}
}
}
public void Dispose()
{
// 优雅退出
EnqueueTask(null);
_workThread.Join();
_waitHandle.Close();
}
}
.NET 4.0 以后提供了一个 BlockingCollection<T>
类型实现了生产/消费者队列。
4.2 ManualResetEvent
-
与
AutoResetEvent
类似,但在调用Set()
方法的时候打开门,是可以允许任意数量的线程在调用WaitOne()
后通过。(与AutoResetEvent
每次只能通过 1 个不一样) -
如果是在关闭状态下调用
WaitOne()
方法,线程会被阻塞,其余功能都与AutoResetEvent
一致。 -
ManualResetEvent
的基类也是EventWaitHandle
,通过以下两种方式均可构造。var manual1 = new ManualResetEvent(false); var manual2 = new EventWaitHandle(false, EventResetModel.ManualReset);
-
.NET 4.0 提供了性能更高的
ManualResetEventSliam
,但是不能够跨线程使用。
4.3 CountdownEvent
-
使用
CountdownEvent
可以指定一个计数器的值,用于表明需要等待的线程数量。 -
调用
Signal()
方法会将计数器自减 1 ,如果调用其Wait()
则会阻塞计数到 0 ,通过AddCount()
可以增加计数。class Program { static void Main() { var test = new CountdownEventTest(); new Thread(test.Say).Start("Hello 1"); new Thread(test.Say).Start("Hello 2"); new Thread(test.Say).Start("Hello 3"); test.CountdownEvent.Wait(); Console.WriteLine("所有线程执行完成..."); } } public class CountdownEventTest { public readonly CountdownEvent CountdownEvent = new CountdownEvent(3); public void Say(object info) { Thread.Sleep(1000); Console.WriteLine(info); CountdownEvent.Signal(); } }
-
当计数为 0 的时候,无法通过
AddCount()
增加计数,只能调用Reset()
进行复位。
4.4 等待句柄与线程池
-
除了手动开启线程之外,事件等待句柄也支持通过线程池来运行工作任务。
-
通过
ThreadPool.RegisterWaitForSingleObject()
方法可以减少资源消耗,当需要执行的委托处于等待状态的时候,不会浪费线程资源。class Program { static void Main() { var test = new ThreadPoolTest(); test.Test(); } } public class ThreadPoolTest { private readonly EventWaitHandle _waitHandle = new ManualResetEvent(false); public void Test() { RegisteredWaitHandle regHandle = ThreadPool.RegisterWaitForSingleObject(_waitHandle, Work, "OJBK", -1, true); Thread.Sleep(1000); _waitHandle.Set(); Console.ReadLine(); regHandle.Unregister(_waitHandle); } public void Work(object data,bool timeout) { Console.WriteLine($"正在执行任务 {data} ....."); } }
-
上述代码如果通过传统的方式进行阻塞与信号发送, 那么有 1000 个请求
Work()
方法,就会造成大量服务线程阻塞,而RegisterWaitForSingleObject
可以立即返回,不会浪费线程资源。
4.5 跨进程的 EventWaitHandle
可以通过对 EventWaitHandle
类型构造函数的第三个参数传入标识,来获得跨进程的事件的等待句柄。
EventWaitHandle wh = new EventWaitHandle(false,EventResetMode.AutoReset,"AppName.Identity");
五、同步上下文
5.1 使用同步上下文
-
这里的同步上下文不是
SynchronizationContext
类,而是 CLR 的自动锁机制。 -
通过继承
ContextBoundObject
基类并添加Synchronization
特性即可让 CLR 自动加锁。 - 同步上下文是一种比较重型的加锁方法,很容易引起死锁的情况发生。
5.2 重入
- 线程安全方法也被称之为可重入的,因为其可以在运行途中被其他线程抢占。
-
使用了自动锁会有一个严重问题,如果将
Synchronization
特性的reentrant
参数设置为true
。则允许同步类是可被重入的,这就导致同步上下文被临时释放,会导致过度期间任何线程都可以自由调用原对象的任何方法。 -
这是因为
Synchronization
特性是直接作用于类,所以其所有方法都会带来可重入的问题。 - 所以因尽量减少粗粒度的自动锁。