热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Threadsynchronization:WaitandPulsedemystified

转载至 https:www.codeproject.comArticles28785Thread-synchronization-Wait-and-Pulse-demystifie

转载至 https://www.codeproject.com/Articles/28785/Thread-synchronization-Wait-and-Pulse-demystified

Table of Contents



  • Introduction

  • Basics

  • Locks

  • Queues

  • Recommended pattern

  • Uses

  • Tips

  • Example

  • Conclusion


Introduction

This short article is about the three less well understood methods of the Monitor class: WaitPulse, and PulseAll. The MSDN documentation for these methods explains what they do, but not why or how to use them. I will try to fill this gap and demystify them.

These methods provide low-level synchronization between threads. They are the most versatile constructs, but they are harder to use correctly than other synchronization primitives like AutoResetEvent. However, if you follow the recommended pattern I will describe, they can be used with confidence.

Basics


public partial static class Monitor
{
public static bool Wait( object o ) { ... } // and overloads
public static void Pulse( object o ) { ... }
public static void PulseAll( object o ) { ... }
}

At the most basic level, a thread calls Wait when it wants to enter a wait state, and another thread calls Pulse when it wants to wake up the waiting thread. Note that if a thread calls Pulse when no other threads are waiting, the Pulse is lost. This is in contrast to primitives like AutoResetEvent, where a call to Set is remembered until a subsequent call to WaitOne occurs.

There is a requirement that all these methods must be called from within a lock statement. If they are called while not holding the lock, then they will throw a SynchronizationLockException. We will see why this is actually useful, later on.

For example:

readonly object key = new object();
// thread A
lock ( key ) Monitor.Wait( key );
// thread B
lock ( key ) Monitor.Pulse( key );

If thread A runs first, it acquires the lock and executes the Wait method. Then, thread B runs, releasing thread A to allow it to continue.

Locks

You may have noticed a little problem with the code above. If thread A holds the lock on the key object, why does thread B not block when it tries to acquire the lock? This is, of course, handled properly. The call to Wait in thread A releases the lock before it waits. This allows thread B to acquire the lock and call Pulse. Thread A then resumes, but it has to wait until thread B releases the lock, so it can reacquire it and complete the Wait call. Note that Pulse never blocks.

技术分享图片

Queues

The ready queue is the collection of threads that are waiting for a particular lock. The Monitor.Wait methods introduce another queue: the waiting queue. This is required as waiting for a Pulse is distinct from waiting to acquire a lock. Like the ready queue, the waiting queue is FIFO.

技术分享图片

Recommended pattern

These queues can lead to unexpected behaviour. When a Pulse occurs, the head of the waiting queue is released and is added to the ready queue. However, if there are other threads in the ready queue, they will acquire the lock before the thread that was released. This is a problem, because the thread that acquires the lock can alter the state that the pulsed thread relies on.

The solution is to use a while condition inside the lock statement:

readonly object key = new object();
bool block = true;
// thread A
lock ( key )
{
while ( block )
Monitor.Wait( key );
block = true;
}
// thread B
lock ( key )
{
block = false;
Monitor.Pulse( key );
}

This pattern shows the reason for the rule that locks must be used: they protect the condition variable from concurrent access. Locks are also memory barriers, so you do not have to declare the condition variables as volatile.

The while loop also solves a couple of other problems. Firstly, if thread B executes before thread A calls Wait, the Pulse is lost. However, when thread A eventually runs and acquires the lock, the first thing it does is check the condition. Thread B has already set block to false, so Thread A continues without ever calling Wait.

Secondly, it solves the problem of the queues. If thread A is pulsed, it leaves the waiting queue and is added to the ready queue. If, however, a different thread acquires the lock and this thread sets block back to true, it now doesn‘t matter. This is because thread A will go round the while loop, find the condition to block is true, and execute Wait again.

Uses

The code above is actually an implementation of an AutoResetEvent. If you omitted the block = true statement in thread A, it would be a ManualResetEvent. If you use an int instead of bool for the condition, it would be a Semaphore. This shows how versatile this pattern is. You have complete control over what condition you use in the while loop.

So, the rule of thumb is: use higher level primitives if they fit. If you need finer control, use the Monitor methods with this pattern to ensure correctness.

Tips

It is usually okay to use PulseAll instead of Pulse. This causes all the waiting threads to re-evaluate their conditions and either continue or go back to waiting. As long as the condition evaluation is not too expensive, this is normally acceptable. PulseAll is also useful where you have multiple threads waiting on the same synchronization object, but with different conditions.

There are overloads of Wait that take a timeout parameter. Similar to above, this usually doesn‘t hurt. It can help prevent stuck Waits or missed Pulses by re-evaluating the conditions on a regular basis.

Example

Here is an example with full source code that demonstrates the versatility of this pattern. It implements a blocking queue that can be stopped. A blocking queue is a fixed-size queue. If the queue is full, attempts to add an item will block. If the queue is empty, attempts to remove an item will block. When Quit() is called, the queue is stopped. This means that you can‘t add any more items, but you can remove existing items until the queue is empty. At that point, the queue is finished.

This is a fairly complex set of conditions. You could implement this using a combination of higher-level constructs, but it would be harder. The pattern makes this implementation relatively trivial.

class BlockingQueue
{
readonly int _Size = 0;
readonly Queue _Queue = new Queue();
readonly object _Key = new object();
bool _Quit = false;
public BlockingQueue( int size )
{
_Size = size;
}
public void Quit()
{
lock ( _Key )
{
_Quit = true;
Monitor.PulseAll( _Key );
}
}
public bool Enqueue( T t )
{
lock ( _Key )
{
while ( !_Quit && _Queue.Count >= _Size ) Monitor.Wait( _Key );
if ( _Quit ) return false;
_Queue.Enqueue( t );
Monitor.PulseAll( _Key );
}
return true;
}
public bool Dequeue( out T t )
{
t = default( T );
lock ( _Key )
{
while ( !_Quit && _Queue.Count == 0 ) Monitor.Wait( _Key );
if ( _Queue.Count == 0 ) return false;
t = _Queue.Dequeue();
Monitor.PulseAll( _Key );
}
return true;
}
}

This implementation is safe for concurrent access by an arbitrary number of producers and consumers. Here is an example with one fast producer and two slow consumers:

internal static void Test()
{
var q = new BlockingQueue( 4 );
// Producer
new Thread( () =>
{
for ( int x = 0 ; ; x++ )
{
if ( !q.Enqueue( x ) ) break;
Trace.WriteLine( x.ToString( "0000" ) + " >" );
}
Trace.WriteLine( "Producer quitting" );
} ).Start();
// Consumers
for ( int i = 0 ; i <2 ; i++ )
{
new Thread( () =>
{
for ( ; ; )
{
Thread.Sleep( 100 );
int x = 0;
if ( !q.Dequeue( out x ) ) break;
Trace.WriteLine( " <" + x.ToString( "0000" ) );
}
Trace.WriteLine( "Consumer quitting" );
} ).Start();
}
Thread.Sleep( 1000 );
Trace.WriteLine( "Quitting" );
q.Quit();
}

And, here is the output of one run:

0.00000000 0000 >
0.00006564 0001 >
0.00009096 0002 >
0.00011540 0003 >
0.09100076 <0000
0.09105981 <0001
0.09118936 0004 >
0.09121715 0005 >
0.19127709 <0002
0.19138214 0006 >
0.19141905 0007 >
0.19156006 <0003
0.29184034 <0004
0.29195839 <0005
0.29209006 0008 >
0.29211268 0009 >
0.39240077 <0006
0.39249521 <0007
0.39265713 0010 >
0.39268187 0011 >
0.49300483 <0008
0.49308145 0012 >
0.49310759 0013 >
0.49324051 <0009
0.59353358 <0010
0.59361452 <0011
0.59378797 0014 >
0.59381104 0015 >
0.69410956 <0012
0.69421405 0016 >
0.69423932 0017 >
0.69443953 <0013
0.79467082 <0014
0.79478532 <0015
0.79493624 0018 >
0.79496473 0019 >
0.89524573 <0016
0.89536309 <0017
0.89549100 0020 >
0.89552164 0021 >
0.98704302 Quitting
0.98829663 Producer quitting
0.99580252 <0018
0.99590403 <0019
1.09638131 <0020
1.09647286 <0021
1.19700873 Consumer quitting
1.19717586 Consumer quitting


推荐阅读
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文内容为asp.net微信公众平台开发的目录汇总,包括数据库设计、多层架构框架搭建和入口实现、微信消息封装及反射赋值、关注事件、用户记录、回复文本消息、图文消息、服务搭建(接入)、自定义菜单等。同时提供了示例代码和相关的后台管理功能。内容涵盖了多个方面,适合综合运用。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 本文讲述了作者通过点火测试男友的性格和承受能力,以考验婚姻问题。作者故意不安慰男友并再次点火,观察他的反应。这个行为是善意的玩人,旨在了解男友的性格和避免婚姻问题。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 本文介绍了指针的概念以及在函数调用时使用指针作为参数的情况。指针存放的是变量的地址,通过指针可以修改指针所指的变量的值。然而,如果想要修改指针的指向,就需要使用指针的引用。文章还通过一个简单的示例代码解释了指针的引用的使用方法,并思考了在修改指针的指向后,取指针的输出结果。 ... [详细]
  • 在project.properties添加#Projecttarget.targetandroid-19android.library.reference.1..Sliding ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
author-avatar
liu
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有