同时只允许Count个线程访问同一块区域的实现方式

好吧,后来才发现有Semaphore和SemaphoreSlim这两个类。

以前的答案:

最近.Net项目中用到了网页截图功能,这个截图功能是类似后台开了一个IE浏览器默默加载某个网页然后截取下来保存,因此并发截图量不能太大,但是又不能一个一个的截(因为截图函数里要设置等待网页加载时间,故一个一个截的话截完N个图要很长时间)。由此引出N个线程一次性只能让_concurrentSnapCount个线程进入截图区域。

一开始我是用一个计数器来计数截图区域进入了多少个线程,达到_concurrentSnapCount就不让进入,而阻塞部分用的是Monitor.Enter(_lkSnap);该部分代码:

/* 注:_lkSnap和_lkNum是object类型全局对象,_snapingCount是int类型初值为0的全局变量代表同时进入截图区域的线程数,_concurrentSnapCount是int类型常量且值大于1*/

          // 一次性只有一个线程能获得Enter返回
          Monitor.Enter(_lkSnap);
lock (_lkNum) { ++_snapingCount; // 进入截图区域的线程数+1 if (_snapingCount < _concurrentSnapCount) // 判断进入截图区域的线程数是否达到设定最大值 { Monitor.Exit(_lkSnap); // 没有达到最大值,Exit,让其它线程也能进入截图区域 } } // 一次性只能有_concurrentSnapCount个线程调用此函数 var img = WebPageSnapshot.WebSnapshot(postUrl, _snapshotWidth, delayTime); // 这块即为截图区域 lock(_lkNum) { // 判断此时在截图区域的线程数,如果等于最大值说明此时调用Enter会被阻塞,而次线程已经截图完毕可以是否截图区域的占用,故Exit。 if(_snapingCount == _concurrentSnapCount) Monitor.Exit(_lkSnap); // 注意,Exit(obj)只能在Enter(obj)后调用一次,这也是为什么要判断_snapingCount==_concurrentSnapCount的原因 --_snapingCount; // 进入区域的线程数变量-1 } DoOtherThing(...);

 上面的代码是存在bug的,即下面的Monitor.Exit(_lkSnap)有可能产生异常信息从不同步的代码块中调用了对象的同步方法

比如说当_concurrentSnapCount值为2时,如果有三个线程要进入截图区域,第一个进入后由于_snapingCount < _concurrentSnapCount 为true故Monitor.Exit(...),

因此第二个线程Enter成功,但是_snapingCount < _concurrentSnapCount为false,故不执行Exit,因此第三个线程会被Enter阻塞。

我们假设第一个进入截图区域的线程也是第一个执行完WebSnapshot(...),该线程在判断_snapingCount == _concurrentSnapCount为true,故会执行Monitor.Exit(_lkSnap),由此

引发 从不同步的代码块中调用了对象的同步方法 的异常,因为最新的Enter(_lkSnap)是第二个线程执行的(或说_lkSnap的锁是由第二个线程加的),而下面的Exit(_lkSnap)却是由第一个线程执行,

释放锁只能由加该锁的线程释放。如果只能由加锁的线程释放那么就变成了必须一次性进入截图区域的_concurrentSnapCount个线程全部执行完,然后由最后进入区域的线程释放锁,再进入下一批。

变成了分批进入而不是出一个进一个,这显然不和要求。

要做到符合要求的功能要将Monitor.Enter(_lkSnap)、Monitor.Exit(_lkSnap)改成由AutoResetEvent对象来实现,具体代码:     

/*_autoRstEvt 也是全局AutoResetEvent对象,且initialState为true*/

       
_autoRstEvt.WaitOne(); // 首个线程进入将直接获得信号并自动执行Reset阻塞下一个线程
       lock
(_lkNum) { ++_snapingCount; if(_snapingCount < _concurrentSnapCount) {
_autoRstEvt.Set(); // 未满,让下一个正在WaitOne的线程获得信号,或下一个将要WaitOne()的线程在WaitOne时直接获得信号。 } } // 一次性只能进入_concurrentSnapCount个 var img = WebPageSnapshot.WebSnapshot(postUrl, _snapshotWidth, delayTime); lock(_lkNum) { // 注意,_autoRstEvt可以重复Set,这点跟Monitor.Exit(obj)不一样,故此处判断其实没必要直接Set()就行。 if(_snapingCount == _concurrentSnapCount) _autoRstEvt.Set(); --_snapingCount; }
       DoOtherThing(...);

至此,实现Count个线程并发进入某一区域且某个线程离开后进入新线程的功能完成。

原文地址:https://www.cnblogs.com/silentdoer/p/6236058.html