C#多线程系列之资源池限制
semaphore、semaphoreslim 类
两者都可以限制同时访问某一资源或资源池的线程数。
这里先不扯理论,我们从案例入手,通过示例代码,慢慢深入了解。
semaphore 类
这里,先列出 semaphore 类常用的 api。
其构造函数如下:
构造函数 | 说明 |
---|---|
semaphore(int32, int32) | 初始化 semaphore 类的新实例,并指定初始入口数和最大并发入口数。 |
semaphore(int32, int32, string) | 初始化 semaphore 类的新实例,并指定初始入口数和最大并发入口数,根据需要指定系统信号灯对象的名称。 |
semaphore(int32, int32, string, boolean) | 初始化 semaphore 类的新实例,并指定初始入口数和最大并发入口数,还可以选择指定系统信号量对象的名称,以及指定一个变量来接收指示是否创建了新系统信号量的值。 |
semaphore 使用纯粹的内核时间(kernel-time)方式(等待时间很短),并且支持在不同的进程间同步线程(像mutex)。
semaphore 常用方法如下:
方法 | 说明 |
---|---|
close() | 释放由当前 waithandle占用的所有资源。 |
openexisting(string) | 打开指定名称为信号量(如果已经存在)。 |
release() | 退出信号量并返回前一个计数。 |
release(int32) | 以指定的次数退出信号量并返回前一个计数。 |
tryopenexisting(string, semaphore) | 打开指定名称为信号量(如果已经存在),并返回指示操作是否成功的值。 |
waitone() | 阻止当前线程,直到当前 waithandle 收到信号。 |
waitone(int32) | 阻止当前线程,直到当前 waithandle 收到信号,同时使用 32 位带符号整数指定时间间隔(以毫秒为单位)。 |
waitone(int32, boolean) | 阻止当前线程,直到当前的 waithandle 收到信号为止,同时使用 32 位带符号整数指定时间间隔,并指定是否在等待之前退出同步域。 |
waitone(timespan) | 阻止当前线程,直到当前实例收到信号,同时使用 timespan 指定时间间隔。 |
waitone(timespan, boolean) | 阻止当前线程,直到当前实例收到信号为止,同时使用 timespan 指定时间间隔,并指定是否在等待之前退出同步域。 |
示例
我们来直接写代码,这里使用 《原子操作 interlocked》 中的示例,现在我们要求,采用多个线程执行计算,但是只允许最多三个线程同时执行运行。
使用 semaphore ,有四个个步骤:
new 实例化 semaphore,并设置最大线程数、初始化时可进入线程数;
使用 .waitone();
获取进入权限(在获得进入权限前,线程处于阻塞状态)。
离开时使用 release()
释放占用。
close()
释放semaphore 对象。
《原子操作 interlocked》 中的示例改进如下:
class program { // 求和 private static int sum = 0; private static semaphore _pool; // 判断十个线程是否结束了。 private static int iscomplete = 0; // 第一个程序 static void main(string[] args) { console.writeline("执行程序"); // 设置允许最大三个线程进入资源池 // 一开始设置为0,就是初始化时允许几个线程进入 // 这里设置为0,后面按下按键时,可以放通三个线程 _pool = new semaphore(0, 3); for (int i = 0; i < 10; i++) { thread thread = new thread(new parameterizedthreadstart(addone)); thread.start(i + 1); } console.foregroundcolor = consolecolor.red; console.writeline("任意按下键(不要按关机键),可以打开资源池"); console.foregroundcolor = consolecolor.white; console.readkey(); // 准许三个线程进入 _pool.release(3); // 这里没有任何意义,就单纯为了演示查看结果。 // 等待所有线程完成任务 while (true) { if (iscomplete >= 10) break; thread.sleep(timespan.fromseconds(1)); } console.writeline("sum = " + sum); // 释放池 _pool.close(); } public static void addone(object n) { console.writeline($" 线程{(int)n}启动,进入队列"); // 进入队列等待 _pool.waitone(); console.writeline($"第{(int)n}个线程进入资源池"); // 进入资源池 for (int i = 0; i < 10; i++) { interlocked.add(ref sum, 1); thread.sleep(timespan.frommilliseconds(500)); } // 解除占用的资源池 _pool.release(); iscomplete += 1; console.writeline($" 第{(int)n}个线程退出资源池"); } }
看着代码有点多,快去运行一下,看看结果。
示例说明
实例化 semaphore 使用了new semaphore(0,3);
,其构造函数原型为
public semaphore(int initialcount, int maximumcount);
initialcount 表示一开始允许几个进程进入资源池,如果设置为0,所有线程都不能进入,要一直等资源池放通。
maximumcount 表示最大允许几个线程进入资源池。
release()
表示退出信号量并返回前一个计数。这个计数指的是资源池还可以进入多少个线程。
可以看一下下面的示例:
private static semaphore _pool; static void main(string[] args) { _pool = new semaphore(0, 5); _pool.release(5); new thread(addone).start(); thread.sleep(timespan.fromseconds(10)); _pool.close(); } public static void addone() { _pool.waitone(); thread.sleep(1000); int count = _pool.release(); console.writeline("在此线程退出资源池前,资源池还有多少线程可以进入?" + count); }
信号量
前面我们学习到 mutex,这个类是全局操作系统起作用的。我们从 mutex 和 semphore 中,也看到了 信号量这个东西。
信号量分为两种类型:本地信号量和命名系统信号量。
- 命名系统信号量在整个操作系统中均可见,可用于同步进程的活动。
- 局部信号量仅存在于进程内。
当 name 为 null 或者为空时,mutex 的信号量时局部信号量,否则 mutex 的信号量是命名系统信号量。
semaphore 的话,也是两种情况都有。
如果使用接受名称的构造函数创建 semaphor 对象,则该对象将与该名称的操作系统信号量关联。
两个构造函数:
semaphore(int32, int32, string) semaphore(int32, int32, string, boolean)
上面的构造函数可以创建多个表示同一命名系统信号量的 semaphore 对象,并可以使用 openexisting 方法打开现有的已命名系统信号量。
我们上面使用的示例就是局部信号量,进程中引用本地 semaphore 对象的所有线程都可以使用。 每个 semaphore 对象都是单独的本地信号量。
semaphoreslim类
semaphoreslim 跟 semaphore 有啥关系?
微软文档:
semaphoreslim 表示对可同时访问资源或资源池的线程数加以限制的 semaphore 的轻量替代。
semaphoreslim 不使用信号量,不支持进程间同步,只能在进程内使用。
它有两个构造函数:
构造函数 | 说明 |
---|---|
semaphoreslim(int32) | 初始化 semaphoreslim 类的新实例,以指定可同时授予的请求的初始数量。 |
semaphoreslim(int32, int32) | 初始化 semaphoreslim 类的新实例,同时指定可同时授予的请求的初始数量和最大数量。 |
示例
我们改造一下前面 semaphore 中的示例:
class program { // 求和 private static int sum = 0; private static semaphoreslim _pool; // 判断十个线程是否结束了。 private static int iscomplete = 0; static void main(string[] args) { console.writeline("执行程序"); // 设置允许最大三个线程进入资源池 // 一开始设置为0,就是初始化时允许几个线程进入 // 这里设置为0,后面按下按键时,可以放通三个线程 _pool = new semaphoreslim(0, 3); for (int i = 0; i < 10; i++) { thread thread = new thread(new parameterizedthreadstart(addone)); thread.start(i + 1); } console.writeline("任意按下键(不要按关机键),可以打开资源池"); console.readkey(); // _pool.release(3); // 这里没有任何意义,就单纯为了演示查看结果。 // 等待所有线程完成任务 while (true) { if (iscomplete >= 10) break; thread.sleep(timespan.fromseconds(1)); } console.writeline("sum = " + sum); // 释放池 } public static void addone(object n) { console.writeline($" 线程{(int)n}启动,进入队列"); // 进入队列等待 _pool.wait(); console.writeline($"第{(int)n}个线程进入资源池"); // 进入资源池 for (int i = 0; i < 10; i++) { interlocked.add(ref sum, 1); thread.sleep(timespan.frommilliseconds(200)); } // 解除占用的资源池 _pool.release(); iscomplete += 1; console.writeline($" 第{(int)n}个线程退出资源池"); } }
semaphoreslim 不需要 close()
。
两者在代码上的区别是就这么简单。
区别
如果使用下面的构造函数实例化 semaphor(参数name不能为空),那么创建的对象在整个操作系统内都有效。
public semaphore (int initialcount, int maximumcount, string name);
semaphorslim 则只在进程内内有效。
semaphoreslim 类不会对 wait
、waitasync
和 release
方法的调用强制执行线程或任务标识。
而 semaphor 类,会对此进行严格监控,如果对应调用数量不一致,会出现异常。
此外,如果使用 semaphoreslim(int32 maximumcount) 构造函数来实例化 semaphoreslim 对象,获取其 currentcount 属性,其值可能会大于 maximumcount。 编程人员应负责确保调用一个 wait 或 waitasync 方法,便调用一个 release。
这就好像笔筒里面的笔,没有监控,使用这使用完毕后,都应该将笔放进去。如果原先有10支笔,每次使用不放进去,或者将别的地方的笔放进去,那么最后数量就不是10了。
到此这篇关于c#多线程系列之资源池限制的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: MySQL 中定位 DDL 被阻塞的问题及解决方案
下一篇: C#中IntPtr类型的具体使用