Skip to content

Latest commit

 

History

History
427 lines (334 loc) · 25.8 KB

03.1-Double Buffer.md

File metadata and controls

427 lines (334 loc) · 25.8 KB

双缓冲模式

目的

进行一系列序列化操作来表现出瞬发性或同步性。

动机

计算机的心脏里藏着凶残的序列化处理能力。其力量源于它们能够将庞大的任务分解为许多细小的步骤以便逐个处理。 尽管通常对于我们的用户而言,他们希望看到的是问题能够即刻被处理,或者多个任务能同时被执行。

注解: 虽然线程和多核技术在不断进步,但即便在多核环境下,也仅有少数操作能真正同步地执行

举个典型的例子,每个游戏引擎所必会涉及的——渲染。当引擎为用户渲染出可见的世界时,它是分步骤来完成渲染任务的:远处的山峰,起伏的山脉,树木,这些被轮流渲染。假如用户也跟着逐步地观察引擎的渲染,那么这个连续游戏世界的幻像将会破碎。场景必须快速而平滑地进行更新,显示一系列完整的帧,而每帧都应当瞬间显示出来。

双缓冲模式解决了上述问题,但为便于理解,首先让我们回顾一下计算机是如何显示图形的。

##计算机图形系统工作原理概述 诸如计算机显示屏的显示设备在每一时刻仅绘制一个像素。显示设备从左至右地扫描屏幕第一行的每个像素,并如此从上至下地扫描屏幕上的每一行。直到扫描至屏幕的右下角,它将重置回屏幕地左上角并如前述那样地重复扫描屏幕。这一扫描过程是如此地快速(大概每秒60次),以至于我们的眼睛无法察觉这一过程。于我们而言,扫描的结果就是一块静态的彩色像素区域,即一张图片。

注解: 这样的阐述不太妥当——它过于简单了。假如你从事底层硬件开发我想你大概已经笑了,你可以轻松地跳过后面的部分,并完全能够理解本章余下的内容。但假如你并非这样的人物,那么在此我的目的是给予你足够的背景知识以便你能理解我们随后要讨论的设计模式。

你可以将上述过程想象成一根细小的软管在向显示区域不断喷洒出像素。单个颜色像素到达软管的末端,软管将它们喷射到显示区域中,每次往每个像素上喷洒一点。那么它如何知道哪个像素该往哪喷呢?

在多数计算机中答案是:它从帧缓冲区(framebuffer)中获知这些信息。帧缓冲区是一块内存,是存储着像素的数组(它是RAM中的一个块,其中每两个字节表示一个像素)。当软管往显示区域喷洒时,它从这个数组中读取颜色值,每次读取1字节。

注解: 字节值与颜色之间的特殊映射关系是通过系统中的像素格式以及色彩深度来描述的。在当今的多数控制台游戏平台,每个像素占32位:红绿蓝色彩通道各占8位,剩余的8位则用于其他多种用途。

基本上讲,为了让游戏在屏幕上显示出来,我们只需要往这个数组里写东西。我们熬夜折腾出来的那些先进图形算法,其根本都只是在往帧缓冲区里设置字节的值。但这里有个小问题。

前面我说计算机的处理是序列化的。假设计算机正在处理我们的一段渲染代码,我们便不希望计算机同时在做其他不相干的事。这几乎是对的,然而在我们的程序运行过程中间还是会穿插着许多其他的事情:比如当我们的游戏在运行时,显示设备会从帧缓存中读取内存中的像素信息。这就为我们带来了问题。

比如我们希望在屏幕上显示一张笑脸。我们的程序开始循环访问帧缓存并对像素进行渲染。出乎我们意料的是,显卡正是在我们往帧缓存中写入数据的同时进行数据读取的。一开始它扫描到那些我们已经写入的数据,笑脸便开始在屏幕上浮现,但它渐渐超过我们的写入速度而访问了帧缓存中那些未写入数据的部分——悲惨的结局,屏幕上留下了一个半成品,这是个能看得一清二楚的BUG。

注解: 如图,我们在显卡设备开始从帧缓存读取数据的同时进行像素数据的写入(图16.1)。最终显卡赶上并超过了渲染器并访问了我们尚未写入数据的帧缓存区域(图16.2)。我们结束绘制(图16.3)时,那些在被显卡读取后才写入的数据就没有被它读取到。结果用户看到的是渲染的半成品(图16.4)。我称它是”哭丧脸“——笑脸的下半边像是被撕掉了一样。

这就是我们需要本设计模式的原因。我们的程序一次性渲染所有的像素,同时我们要求显示器也一次性将其显示出来——可能这一帧看不到任何东西,但下一帧显示的就是完整的笑脸。双缓冲解决了这一问题。下面我会以类比的形式来阐述。

##场景1,幕1 设想我们的用户正在观看我们创作的一场表演。当第一个场景谢幕后第二个场景跟着上映,这时候我们需要切换场景。如果我们在场景后台控制舞台管理设备直接开始收起场景道具,那么场景在视觉上的连续性会被破坏。我们可以在收拾场景的同时将灯光变暗(这也正是影剧院所做的),而观众们依然知道黑暗中戏剧仍在继续。我们希望在剧幕之间不会产生间隙。

在资源允许的情况下,我们想到了这个好办法:我们建立两个舞台以便它们都能为观众所见。它们各有各的光源设置。我们称其为A舞台和B舞台。场景1正在A舞台上上演,同时舞台B正处在黑暗中并正由场景后台进行着场景2的准备。一旦场景1结束,我们就关掉A舞台的灯光并将灯光转移到B舞台,观众们便立即聚焦到新舞台并看到了第二幕场景上映。

与此同时,我们的场景后台正在清理舞台A,它清理场景1并为场景3做准备。一旦场景2结束,我们再将光线聚焦到A舞台上。我们在整场表演过程中重复上述过程,将黑暗中的舞台作为工作区来为下个场景做准备。每次场景切换,我们只是将灯光在两个舞台之间来回切换。我们的观众于是就看到了衔接流畅而无缝的场景转换。他们从不会看到舞台的后台。

注解: 借助单面镜以及其他一些巧妙的布局,实际上你能够在同一个舞台进行场景之间的无缝切换。当灯光转移时,观众们可能会聚焦到另一个舞台上,但他们并不一定要转移视线。如何做到这一点就留给读者思考吧。

##回到图形上 上面就是双缓冲的工作原理,你所见到的任何一款游戏其渲染系统中都重复着这样的过程,我们也是。如我们所类比的,双缓冲中的一个缓存用于展示当前帧,即A舞台。它就是显示硬件读取像素数据的地方,GPU对其进行扫描,整个缓冲区的数据都是它的。

注解: 然而并非所有的游戏和控制台都这么做。早前比较简单的控制台游戏受到内存的局限,小心翼翼地将渲染与机器刷新操作进行同步来取代双缓冲,这可是要技巧的。

于此同时,我们的渲染代码正在往另一个帧缓冲区中写入数据,它就是我们黑暗中的B舞台。当渲染代码完成场景2的渲染时,它通过交换两个缓冲区来”切换光线”。这使得显卡驱动开始从第一个缓冲区转向第二个缓冲区以读取其数据。只要它掌握好时机在每次刷新显示结束时进行切换,我们就不会看到任何衔接的裂隙,且整个场景能一次性显示出来。这时候,旧的帧缓冲变得可用了,我们就开始往它的内存区域渲染入下一帧。这真棒!

##(双缓冲)模式 定义一个缓冲区类来代表一个缓冲区:一系列能被修改的状态。这块缓冲区能被一步步地修改,但我们希望任何外部的代码对该缓冲区的修改都是原子操作。为实现这一点,此类中维护两个缓冲区实例:当前缓冲区和后台缓冲区。

当要从缓冲区中读取信息时,总是从当前缓冲区读取。当要往缓冲区中写数据时,则总在后台缓冲区上进行。当改动完成后,则执行”交换”操作来讲当前缓冲区与后台缓冲区交换,以便让新的缓冲区为我们所见,同时刚被换下来的当前缓冲区则成为现在的后台缓冲区以供复用。

##使用情境 这是个到需要时你自然会想起的设计模式之一。假如你的系统不支持双缓冲,那么显然是没法用了(比如会出现”撕裂”现象),或者显示将表现出异常。但是说”需要的时候你自然会想起”还是太宽泛了,更准确地说,当下面这些条件都成立时,适用双缓冲模式:

  • 我们需要维护一些能够不断被修改的状态量。
  • 同个状态可能会在其被修改的同时被访问到。
  • 我们希望改变状态的工作进程对正在访问这些状态的外部代码透明。
  • 我们希望能够读取到这些状态,而无需在其被写入时等待。 ##使用须知 不同于其他大架构的设计模式,双缓冲模式的实现处于较底层。因此,它对代码库的影响较少——甚至多数游戏都不会在意这些差别。当然,下面这些附加说明还是值得一提的。 ##交换操作本身是耗时的 双缓冲模式需要在状态写入完成后进行一个交换缓冲区的动作。这个操作必须是原子性的:也就是说任何代码都无法在这个操作其间对任何一块缓冲区内的状态进行访问。通常这个交换过程和分配一个指针的速度差不多,但万一交换花去了比修改初始状态更多的时间,那这模式就毫无助益了。 ##必须要有两个缓冲区 使用此模式的另一结果是导致内存占用率增加。正如其名,此模式要求你在任何时刻都维护着两份存储着状态的内存区域。在内存受限的硬件上,这可是个很苛刻的要求。假如你无法分配出两份内存,你就必须想其他办法来避免你的状态在修改时被访问。

##示例 说完理论,让我们来结合实践,看看它是如何工作的。我们将写一个及其简单的图形系统以供我们在帧缓存上绘制像素。在多数控制台和PC上,显卡驱动提供了这一底层部分的图形系统,而这里通过手动实现它,我们将能窥其全貌。首先是缓冲区:

class Framebuffer
{
public:  
	Framebuffer() { clear(); } 
	void clear() 
	{    
		for (int i = 0; i < WIDTH * HEIGHT; i++)    
		{      
			pixels_[i] = WHITE;    
		}  
	}  
	
	void draw(int x, int y) 
	{    
		pixels_[(WIDTH * y) + x] = BLACK;  
	}  
	
	const char* getPixels() 
	{    
		return pixels_;  
	}
private:  
	static const int WIDTH = 160;  
	static const int HEIGHT = 120;  
	
	char pixels_[WIDTH * HEIGHT];
};

缓冲区拥有一些基本操作:将整个缓冲区清理为默认颜色,对指定位置的像素颜色值进行设置。它还包含了getPixels()函数,用于外部访问缓冲区持有的整个原始像素数组。我们并不会在例子中看到它,但实际中,显卡驱动会频繁地调用这个函数来将缓冲区的内存流式地输出到屏幕上。

我们在Scene类里包装这个原始的缓冲区。此类的任务在于对其缓冲区进行一系列的draw()函数调用来渲染出图形。

class Scene
{
public:  
	void draw()  
	{    
		buffer_.clear();    
		buffer_.draw(1, 1);    
		buffer_.draw(4, 1);    
		buffer_.draw(1, 3);   
		buffer_.draw(2, 4);   
		buffer_.draw(3, 4);   
		buffer_.draw(4, 3); 
	}  
	
	Framebuffer& getBuffer() { return buffer_; }

private:  
	Framebuffer buffer_;
};

注解: 具体来说,它画出了这样一幅杰作:

游戏在每帧通知场景进行绘制。场景清理缓冲区接着一次性地绘制一系列像素。它也通过方法getBuffer()提供了对内部缓冲区的访问,以便显卡驱动能够获取到它。

这听起来直接了当,但假如我们就这么结束了,那么就会出现问题:显卡驱动可以在任何时刻对缓冲区调用getPixels(),甚至是在下面这样的时机调用:

buffer_.draw(1, 1);
buffer_.draw(4, 1);
// <- Video driver reads pixels here!
buffer_.draw(1, 3);
buffer_.draw(2, 4);
buffer_.draw(3, 4);
buffer_.draw(4, 3);

当上述情况发生时,用户将看到笑脸的眼睛部分,但单对这一帧而言它的嘴巴却没了。在下一帧它又可能在其他某个地方受到干扰。结果是可怕的频闪图像。我们可以用双缓冲来修正它:

class Scene
{
public:  
	Scene()  
	: current_(&buffers_[0]),
	next_(&buffers_[1])  
	{} 
	
	void draw()  
	{    
		next_->clear();    
		next_->draw(1, 1);    
		// ...    
		next_->draw(4, 3);   
		swap();  
	}  
	
	Framebuffer& getBuffer() { return *current_; }
	
private:  
	void swap()  
	{    
		// Just switch the pointers.   
		Framebuffer* temp = current_;    
		current_ = next_;    
		next_ = temp;  
	} 
	
	Framebuffer  buffers_[2];  
	Framebuffer* current_;  
	Framebuffer* next_;
};

现在Scene拥有两个缓冲区,它们置于buffers_数组中。我们并不直接从数组中引用它们,而是通过next_current_这两个成员来指向数组。当我们绘图时,我们往next这个缓冲区(通过next_访问)里绘制,而当显卡驱动需要获取像素信息时,它总是从current_所指向的current缓冲区中获取。

由此,显卡驱动将不会访问到我们所正在进行处理的缓冲区。剩下的问题就在于在场景完成帧绘制后,对swap()方法的调用。它简单地通过交换next_current_这两个指针的指向来交换两个缓冲区。当下一次显卡驱动调用getBuffer()函数时,它将获取到我们刚刚完成绘制的那块新的缓冲区,并将其内容绘制到屏幕上。再也不会有图形撕裂和不美观的问题了。

##并非只针对图形 双缓冲模式所解决的核心问题在于对状态同时进行修改与访问的冲突。造成此问题的情况通常有两个,我们已经通过上述图形例子描述了第一种情况——状态直接被另一个线程的代码所直接访问或者打断。

还有另一种很类似且常见的情况。一个状态同时被两段代码进行修改。这会在很多地方发生:尤其是实体的AI和物理部分,在它与其他实体进行交互时会发生这样的情况,双缓冲模式往往能在此能奏效。

##没智商的AI 假设我们在为所有实体构建行为系统,这是个基于打斗漫画的游戏。游戏包含一个舞台,许多角色在其中追逐打闹。下面是我们的演员角色类:

class Actor
{
public:  
	Actor() : slapped_(false) {} 
	
	virtual ~Actor() {}  
	virtual void update() = 0;  
	
	void reset()      { slapped_ = false; }
	void slap()       { slapped_ = true; } 
	bool wasSlapped() { return slapped_; }

private:  
	bool slapped_;
};

游戏需要在每一帧对演员实例调用update()以让其进行自身的处理。从用户的角度严格来说,所有的角色必须看起来是同步地进行更新。

注解: 这是一个[Update Method](./03.3-Update Method.md)的例子

演员也可以通过”相互作用”与其他角色进行交互,这里的相互作用指他们可以互相扇对方巴掌。当更新时,角色可以对其他角色调用自身的slap()方法来扇巴掌并通过调用wasSlapped()方法来获知对方是否已经被扇过巴掌。

这些角色需要一个可以交互的舞台,我们下面构建它:

class Stage
{
public:  
	void add(Actor* actor, int index)  
	{    
		actors_[index] = actor;  
	}  
	
	void update() 
	{    
		for (int i = 0; i < NUM_ACTORS; i++)    
		{      
			actors_[i]->update();      
			actors_[i]->reset();   
		}  
	}

private:  
	static const int NUM_ACTORS = 3; 
	Actor* actors_[NUM_ACTORS];
};

Stage允许我们往里添加角色,并提供一个简单的update()方法来更新所有角色。对于用户而言,角色开始同步地各自移动,但从内部看,一个时刻仅有一个角色被更新。

另一点需要注意的是,每个角色”被扇巴掌”的状态在其更新结束后立即被清空重置。这是为了确保一个角色只会对一个巴掌作出响应。

为了推动事情的进展,我们来为角色创建一个具体的子类。我们的内容很简单,它面对一个角色,不论谁给了它一巴掌,它就冲着这个角色扇巴掌。

class Comedian : public Actor
{
	public:  void face(Actor* actor) { facing_ = actor; } 
	virtual void update()  
	{    
		if (wasSlapped()) facing_->slap();  
	}

private:  
	Actor* facing_;
};

现在,让我们往舞台里放一些角色来看看会发生什么。对三个角色进行恰当的设置,使他们每个都面对着下一个,而最后一个面向第一个,组成一个圈。(译者注: 即构成一个小的单循环链表, facing_成员即为next)

Stage stage;

Comedian* harry = new Comedian();
Comedian* baldy = new Comedian();
Comedian* chump = new Comedian();

harry->face(baldy);
baldy->face(chump);
chump->face(harry);

stage.add(harry, 0);
stage.add(baldy, 1);
stage.add(chump, 2);

现在舞台的布局如下图所示。箭头指明了角色所面朝的另一个角色,而数字表示角色在舞台数组中的索引号。 现在我们往harry脸上扇一巴掌来启动舞台,看看现在会发生些什么:

harry->slap();
stage.update();

切记Stage中的update()方法轮流对每个角色进行更新,所以假如我们浏览一遍代码,我们将推测舞台上表演的进展过程:

Stage updates actor 0 (Harry)  
  	Harry was slapped, so he slaps Baldy
Stage updates actor 1 (Baldy)  
	Baldy was slapped, so he slaps Chump
Stage updates actor 2 (Chump)  
	Chump was slapped, so he slaps Harry
Stage update ends

在单独一帧内,我们最开始给Harry的一巴掌传递给了所有演员。现在为了让事情更复杂些,我们把舞台上的这些演员在数组中的顺序打乱但不改变他们脸的朝向。 我们将剩余的部分交给舞台自己处理,但要将上面添加三个角色的代码替换为以下:

stage.add(harry, 2);
stage.add(baldy, 1);
stage.add(chump, 0);

让我们再来实验看看会发生什么:

Stage updates actor 0 (Chump)  
	Chump was not slapped, so he does nothing
Stage updates actor 1 (Baldy) 
	Baldy was not slapped, so he does nothing
Stage updates actor 2 (Harry)
	Harry was slapped, so he slaps Baldy
Stage update ends

哦!完全不一样了。问题很明显,当我们更新角色时,我们修改它们的”被掴巴掌”状态,我们也在更新中同时读取这些状态。因此在同一次舞台更新循环中,状态的修改仅仅会影响到在其后更新的那些角色。

注解: 假如你继续更新舞台,你将看到扇巴掌的动作开始在角色之间传递,每帧传递一个。在第一帧, Harry扇了Baldy一巴掌,下一帧Baldy扇了Chump一巴掌,如此递推。

最终的结果是某个角色可能不会在被扇巴掌的这一帧做出反应也不会在下一帧做出反应——这完全取决于两个角色在舞台中的顺序。这违背了我们对角色的需求:我们希望它们同步地运转,而他们在某帧更新中的顺序是不应该对结果产生影响的。

##缓存这些巴掌 幸运的是,我们的双缓冲模式能帮上忙。这一次,我们将缓存一系列粒度更恰当的数据:每个角色的”被掴”状态,而不是先前的那两个巨大的缓冲区对象:

class Actor
{
public:  
	Actor() : currentSlapped_(false) {}  
	
	virtual ~Actor() {}  
	virtual void update() = 0;  
	
	void swap()  
	{    
		// Swap the buffer.    
		currentSlapped_ = nextSlapped_;    
		
		// Clear the new "next" buffer.   
		nextSlapped_ = false;  
	}  
	
	void slap()       { nextSlapped_ = true; }  
	bool wasSlapped() { return currentSlapped_; }
	
private: 	
	bool currentSlapped_;  
	bool nextSlapped_;
};

现在每个角色有两个slapped_状态而不是一个。正如先前图形的例子一样,当前的状态用于读取,下一个状态用于写入。

reset()函数被swap()方法所替换。现在,在清除交换的状态之前,角色先将下一状态复制到当前状态中,使其成为当前状态,这里还需要在Stage中进行一些小改动:

class Stage
{  
	void update()  
	{    
		for (int i = 0; i < NUM_ACTORS; i++)   
		{      
			actors_[i]->update();
		}    
		
		for (int i = 0; i < NUM_ACTORS; i++)   
		{      
			actors_[i]->swap();    
		}  
	}  
	
	// Previous Stage code...
};

现在update()函数更新所有的角色接着对他们的状态进行交换。

这样的结果是,每个角色在其被扇巴掌的那一帧中仅会看到一个巴掌。这样一来,这些角色就会表现一致而不受他们在舞台上顺序的影响。对于用户和外部的代码而言,这些角色在一帧中就是同步更新的。

##设计决策 双缓冲模式很直白,我们上面所看到的例子也几乎将你可能遇到的问题都涵盖到了。当实现这种模式时主要会有如下两点的讨论:

##缓冲区如何进行交换? 交换缓冲区的操作是整个过程最关键的一步,因为在这一过程中我们必须封锁对两个缓冲区所有的读写操作。为达到最优性能,我们希望这个过程越快越好。

  • 交换指向两个缓冲区的指针。

    这是我们图形例子中的做法,也是处理图形双缓冲最通用的解决方案。

    • 这很快。它无视缓冲区的大小,交换操作只是两个指针分配的动作。没办法让这个过程更加简化或更快了。

    • 外部代码无法永久存储指向某块缓冲区的指针。这是该方法主要的约束。因为我们并没有实际移动数据,我们实际上做的是周期性地告诉其他代码库去另外一些地方找缓冲区,就像我们最初所比的舞台那样。这意味着其他代码库无法直接存储指向某个缓冲区的指针,因为过一会儿它就可能指向错误的缓冲区了。

    • 这对于那些显卡希望帧缓冲区在内存中固定地址的系统来说尤其会造成麻烦。如果是那样,我们就不能采用这种办法。

    • 缓冲区中现存的数据会来自两帧之前而不是上一帧的。连续几帧里在交替的两个缓冲区中进行绘制而不在它们之间进行数据复制,如下:

    • 你将会注意到当我们要绘制第三帧时,在缓冲区中的数据来自第一帧的,而不是来自最近的第二帧。在多数情况下,这并不是问题——我们往往在绘制前会清理整个缓冲区。但假如我们企图复用某些缓冲区现存的数据,那么就必须考虑到那些数据是比我们所预期的更提早一帧。

注解: 双缓冲一个经典的用法是处理动态模糊。当前帧与先前渲染帧的一部分进行混合,以便让产生的图像更接近于真实摄像机拍摄产生的效果。

  • 在两个缓冲区之间进行数据的拷贝:

    假如我们无法对缓冲区进行指针重定向,那么唯一的办法就是将数据从后台缓冲区实实在在地拷贝到当前缓冲区。这就是我们在打斗喜剧里所做的。在这一情况下,我们选择此方法是因为其缓冲区仅仅是一个简单的布尔值标志位——它并不会比复制指向缓冲区的指针花去更长的时间。

    位于后台缓冲区里的数据与当前的数据就只差一帧时间。这是拷贝数据方法的优点,它就像打乒乓球那样一来一回通过两个缓冲区的翻转来推进画面。假如我们需要访问先前缓冲区的数据,此方法会提供更加实时的数据以供我们使用。

    • 交换操作可能会花去更多时间。这当然是个大缺点。这里的交换就意味着拷贝内存中的整个缓冲区数据块。假如缓冲区很大,比如是一整个帧缓冲区,那么进行交换就会很明显地花去一整块时间。由于在交换期间无法对缓冲区进行任何读写操作,故这是个很大的局限。

##缓冲区的粒度如何?

另一个问题在于缓冲区其自身是如何组织的:它是单个庞大数据块还是分布在某个集合里的每个对象之中?我们在图形的例子中使用了前一形式而演员类中使用了后者。多数时候,你所要缓存的内容将会告诉你答案,当然也会有些变数。例如,我们的演员也都可以将他们的信息集中存储在一个独立的信息块中,并让演员们通过他们的索引指向其中各自的状态。

  • 假如缓冲区是单个大块

    交换操作很简单,因为全局只有一对缓冲区,只需要进行一次交换操作。假如你通过交换指针来交换缓冲区,那么你就可以交换整个缓冲区而无视其大小,只是两次指针分配而已。

  • 假如许多对象都持有一块数据

    • 交换较慢。为实现交换,我们需要遍历对象集合并通知每个对象进行交换。
    • 在我们的打斗喜剧中,这是没啥问题的,因为我们总需要清理后台”被扇巴掌”的状态——每帧都必须访问到每个对象所缓存的状态。假如我们不需要访问缓存的状态,那么我们就可以对其进行优化来使其达到与使用单块大缓冲区存储一系列对象状态一样的效率。——此时的办法就是使用”当前”和”下一个”指针的概念并以此建立对象之间的联系(译者注: 类似建立链表)。如下:
class Actor
{
public:  
	static void init() { current_ = 0; }
	static void swap() { current_ = next();}  
	
	void slap()        { slapped_[next()] = true; } 
	bool wasSlapped()  { return slapped_[current_]; }
	
private:  
	static int current_;  
	static int next()  { return 1 - current_;}  
	
	bool slapped_[2];
};
  • 演员们通过current_变量来访问状态数组。下个状态总是数组中的另一个索引,故我们可以通过next()来计算它。此时交换状态只需变换current_的索引。聪明的地方在于swap()现在是一个静态方法——只需要调用一次,则每个演员的状态都会被交换。

##参考

  • 你几乎能在任何一个图形API中找到双缓冲模式的应用。例如OpenGl中的swapBuffers()函数, Direct3D中的“swap chains”,微软XNA框架在endDraw()方法中也进行的帧缓冲区交换。