支持HW团队,就支付宝领取下面的红包吧!(2018年3月31前,就几毛,也会几块,可以和其他红包叠加使用),你领取消费,HW有奖励。红包使用无条件限制,有条件请注意是不是有病毒。

小伙伴们,给大家发红包喽!人人可领,领完就能用。祝大家领取的红包金额大大大!#吱口令#长按复制此消息,打开支付宝就能领取!er1OEj73Uj

登入 注册 | 验证
| 搜索
HelloWorld论坛 : > 计算机科学、技术、教学> 编程专题> 开源免费项目> [转]多线程,从动画说起
 
 
 
 
 
 
类别:游戏 阅读:3612 评论:0 时间:五月 16, 2013, 2:27 p.m. 关键字:

 

来源:
http://www.cppblog.com/lf426/archive/2008/04/26/48206.html
http://www.cppblog.com/lf426/archive/2008/04/28/48325.html
http://www.cppblog.com/lf426/archive/2008/04/28/48331.html
作者:龙飞  

1、多线程,从动画说起
1.1:简单动画
游戏离不开动画。我们考虑最简单的情况:将一个角色从一个位置移动到另外一个位置。这个行为表述给电脑就是,将一个surface不断的blit(),从起始位置的坐标,移动到结束位置的坐标。移动速度取决于每次blit()的坐标差和blit()的时间间隔(v = ds/dt )。
我们来设计一个函数实现这个简单的动画。我们需要的数据有:起始坐标(int beginX, int beginY),结束坐标(int endX, int endY),以及作为SDL基础的ScreenSurface窗口(const ScreenSurface& screen)。一般的考虑是,将这5个数据以参数的方式传入函数;但是一种更加通用一点的方式是,将这5种数据合并成一个结构,这样函数的参数形式会更加的统一,这正是触发多线程的函数所需要的。在SDL中,我们通过函数:
SDL_Thread *SDL_CreateThread(int (*fn)(void *), void *data);
触发多线程,其中所需要的函数指针形式为:
typedef int (*fn)(void*);
而void*类型的data就是函数(*fn)()需要的的数据。我们可以将任意的结构体指针,转化为void*,作为这个函数的第二个参数需要。
因此,我们可以为我们需要的动画函数定义一个结构作为传递所有数据的载体:
struct AmnArg
{
int beginX;
int beginY;
int endX;
int endY;
const ScreenSurface& screen;
AmnArg(int begin_x, int begin_y, int end_x, int end_y, const ScreenSurface& _screen): beginX(begin_x), beginY(begin_y), endX(end_x), endY(end_y), screen(_screen){}
};
这样,我们可以将AmnArg对象的指针传递给动画函数——考虑到多线程函数的需要,我们再曲折一点:先将AmnArg*转换成void*传递给函数,在函数内部再将其转换回来以供调用。
int amn(void* data)
{
AmnArg* pData = (AmnArg*)data;
PictureSurface stand("./images/am01.png", pData->screen);
stand.colorKey();
PictureSurface bg("./images/background.png", pData->screen);

const int SPEED_CTRL = 300;
int speedX = (pData->endX - pData->beginX) / SPEED_CTRL;
int speedY = (pData->endY - pData->beginY) / SPEED_CTRL;

for ( int i = 0; i < SPEED_CTRL; i++ ){
pData->beginX += speedX;
pData->beginY += speedY;
bg.blit(pData->beginX, pData->beginY, pData->beginX, pData->beginY, stand.point()->w, stand.point()->h, 2, 2);
stand.blit(pData->beginX, pData->beginY);
pData->screen.flip();
}

return 0;
}
注意:我们这里仅仅设定了每次blit()的位移差(ds)而没有设定时间差(dt)。这并不意味着dt == 0,事实上,电脑处理数据是需要时间的,包括运算和显示。我们这里事实上将dt的设定交给了电脑,也就是说,让电脑以其最快的速度来完成。为什么要这么做呢?这是为了演示多线程的一个现象,卖个关子,后面解释。:)

1.2:动画函数在主程序中的调用
#include "SurfaceClass.hpp"
#include "amn.hpp"

int main(int argc ,char* argv[])
{
//Create a SDL screen.
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const Uint32 SCREEN_FLAGS = 0; //SDL_FULLSCREEN | SDL_DOUBLEBUF | SDL_HWSURFACE
const std::string WINDOW_NAME = "Amn Test";
ScreenSurface screen(SCREEN_WIDTH, SCREEN_HEIGHT, WINDOW_NAME, 0, SCREEN_FLAGS);

PictureSurface bg("./images/background.png", screen);
bg.blit(0);
screen.flip();

AmnArg test1(0, 250, 600, 250, screen);
amn((void*)&test1);

SDL_Event gameEvent;
bool gameOver = false;
while ( gameOver == false ){
while ( SDL_PollEvent(&gameEvent) != 0 ){
if ( gameEvent.type == SDL_QUIT ){
gameOver = true;
}
if ( gameEvent.type == SDL_KEYDOWN ){
if ( gameEvent.key.keysym.sym == SDLK_ESCAPE ){
gameOver = true;
}
}
screen.flip();
}
}

return 0;
}
当这个程序运行的时候,我们会发现一些很明显的问题:
1、图片移动的时候,界面不接受任何信息。这是因为必须把amn()执行完毕才会运行到有事件响应的事件轮询循环。
2、如果我们需要另外一张图片移动起来,我们唯一能做的事情,是修改amn()函数,而不是把amn()以不同的参数调用两次——如果以不同的参数调用两次,那么移动总是有先后的——是不可能完成“同时”移动的。

1.3:创建线程

如果要将这个程序从主线程(主进程)调用函数修改为通过新创建的线程调用函数,只需要做很小的修改,即将amn((void*)&test1);修改为:
SDL_Thread* thread1 = SDL_CreateThread(amn, (void*)&test1);
然后在return 0;之前加入清理线程的语句:
SDL_KillThread(thread1);
这样,程序在执行动画的同时,事件轮询就已经开始,我们可以随时结束程序,SDL界面也不会出现不响应的情况。

2、初识多线程
2.1:竞争条件(Race Conditions)
我们在前面将一个普通函数调用转换成了用线程调用,这意味着我们可以“同时”调用两个以上的线程。例如,我们希望在屏幕的另外一个位置也播放这段简单的动画,我们只需要添加一个线程的调用就可以了。
int main(int argc ,char* argv[])
{
//Create a SDL screen.
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const Uint32 SCREEN_FLAGS = 0; //SDL_FULLSCREEN | SDL_DOUBLEBUF | SDL_HWSURFACE
const std::string WINDOW_NAME = "Amn Test";
ScreenSurface screen(SCREEN_WIDTH, SCREEN_HEIGHT, WINDOW_NAME, 0, SCREEN_FLAGS);

PictureSurface bg("./images/background.png", screen);
bg.blit(0);
screen.flip();

AmnArg test1(0, 250, 600, 250, screen);
SDL_Thread* thread1 = SDL_CreateThread(amn, (void*)&test1);

AmnArg test2(0, 0, 600, 0, screen);
SDL_Thread* thread2 = SDL_CreateThread(amn, (void*)&test2);

SDL_Event gameEvent;
bool gameOver = false;
while ( gameOver == false ){
while ( SDL_PollEvent(&gameEvent) != 0 ){
if ( gameEvent.type == SDL_QUIT ){
gameOver = true;
}
if ( gameEvent.type == SDL_KEYDOWN ){
if ( gameEvent.key.keysym.sym == SDLK_ESCAPE ){
gameOver = true;
}
}
screen.flip();
}
}

SDL_KillThread(thread1);
SDL_KillThread(thread2);
return 0;
}
这段程序看起来似乎没有什么问题,但是运行的时候,不可预知的情况出现了:理论上我们几乎同时调用了两个线程,动画似乎应该是同步播放的,但是实际上,两段动画的播放并不同步,并且每次执行的效果都不一样——有时候上面的图片移动快,有时候下面的图片移动快,并且速度不均匀。
这就是典型的race conditions的表现。还记得我说过没有定义dt吗,我们让电脑以其所能达到的最快速度决定dt,换句话说,我们每一个线程都试图“咬死”CPU的运算,当然,在实际中多任务的OS会帮助CPU分配任务,但是如何分配却是不确定的,因为OS并不知道哪些任务需要优先执行,所以,两个线程实际上在竞争电脑的性能资源,产生的结果就是不确定的。

2.2:松开“死咬”的CPU
void SDL_Delay(Uint32 ms);
解决race conditions的方法就是给CPU足够的时间“休息”,而这正好也是我们自己定义dt所需要的。SDL_Delay()在这个时候就显得意义重大了。当今电脑的运算速度非常非常快,以至于哪怕我们仅仅给电脑0.01秒的时间“休息”(每次循环中),电脑都会显得很轻松了。
int amn(void* data)
{
AmnArg* pData = (AmnArg*)data;
PictureSurface stand("./images/am01.png", pData->screen);
stand.colorKey();
PictureSurface bg("./images/background.png", pData->screen);

const int SPEED_CTRL = 300;
int speedX = (pData->endX - pData->beginX) / SPEED_CTRL;
int speedY = (pData->endY - pData->beginY) / SPEED_CTRL;

for ( int i = 0; i < SPEED_CTRL; i++ ){
pData->beginX += speedX;
pData->beginY += speedY;
bg.blit(pData->beginX, pData->beginY, pData->beginX, pData->beginY, stand.point()->w, stand.point()->h, 2, 2);
stand.blit(pData->beginX, pData->beginY);
pData->screen.flip();
SDL_Delay(10);
}

return 0;
}
说到这里,我们不得不提及之前一直所忽略的一个问题:我们之前凡是涉及循环等待事件轮询的程序总是占用100%的CPU,这并不是因为我们真正用到了100%的CPU性能,而是我们让CPU陷入了“空等”(Busy Waiting)的尴尬境地。轮询事件得到响应相对于循环等待来说,是发生得非常缓慢的事情,我们在循环中,哪怕是让电脑休息0.01秒,事情都会发生本质性的改变:
while ( gameOver == false ){
while ( SDL_PollEvent(&gameEvent) != 0 ){
if ( gameEvent.type == SDL_QUIT ){
gameOver = true;
}
if ( gameEvent.type == SDL_KEYDOWN ){
if ( gameEvent.key.keysym.sym == SDLK_ESCAPE ){
gameOver = true;
}
}
screen.flip();
}
SDL_Delay(10);
}
当我们重新运行新程序的时候,我们可以看到程序对CPU的占用从100%骤降到了0%!这当然并不意味着程序就用不上CPU了,而是说,这些运算对于我们的CPU来说,实在是小菜一碟了,或者从数据上说,处理这些运算的时间与0.01秒来比较,都几乎可以忽略不计!

2.3:GUI线程与worker线程

我们的另外一项试验是将事件轮询放到动画线程中,程序就不多写了,大家可以自己试下。我直接说结论:动画线程中无法响应事件轮询。
一般提倡的模式,是将GUI事件都写在主线程中,而将纯粹的运算才写到由主线程创建的线程中,后者也就是所谓的worker线程。从另外一个概念看,只有主线程控制着“当前窗口”,其它线程也许在后台,也许虽然也是在前台但是并非是我们可见的,所以,轮询事件找不到接口。
对于抛出的线程与主线程之间的通讯,我们可以通过他们共享的数据来进行控制,所以,尽管事件轮询不能直接影响worker线程,但是我们仍然是可以通过主线程进行间接影响的。

3、封装多线程
SDL创建多线程的函数SDL_CreateThread()所调用的是函数指针,这意味着我们不可以传入(非静态)成员函数的指针。关于两种函数指针我们之前已经讨论过:函数指针与成员函数指针,我们可以有两种方法能让具有普通函数指针(函数指针以及静态成员函数指针)的函数调用类的私有成员,一是友元函数,另外就是静态成员函数。而能够受到类私有保护的,只有静态成员函数。所以,我们可以通过静态成员函数调用一个对象数据的形式,实现对于创建多线程函数的封装。
另外,我们希望测试在主线程中读写线程数据的效果,所以添加了两个方法show() 和reset(),多线程演示的类源代码如下:
#include <iostream>
#include "SurfaceClass.hpp"

class AmnArg
{
private:
int beginX;
int beginY;
int endX;
int endY;
const ScreenSurface& screen;
//
static int amn(void* pThat);
public:
AmnArg(int begin_x, int begin_y, int end_x, int end_y, const ScreenSurface& _screen);
SDL_Thread* createThrd();
void show() const;
void reset();
};
其中SurfaceClass.hpp请参考:
http://www.cppblog.com/lf426/archive/2008/04/14/47038.html
实现函数如下:
#include "amn.hpp"

AmnArg::AmnArg(int begin_x, int begin_y, int end_x, int end_y, const ScreenSurface& _screen):
beginX(begin_x), beginY(begin_y), endX(end_x), endY(end_y), screen(_screen)
{}

SDL_Thread* AmnArg::createThrd()
{
return SDL_CreateThread(amn, (void*)this);
}

void AmnArg::show() const
{
std::cout << "Now x at: " << beginX << std::endl;
}

void AmnArg::reset()
{
beginX = 0;
}

int AmnArg::amn(void* pThat)
{
AmnArg* pData = (AmnArg*)pThat;
PictureSurface stand("./images/am01.png", pData->screen);
stand.colorKey();
PictureSurface bg("./images/background.png", pData->screen);

const int SPEED_CTRL = 300;
int speedX = (pData->endX - pData->beginX) / SPEED_CTRL;
int speedY = (pData->endY - pData->beginY) / SPEED_CTRL;

for ( int i = 0; i < SPEED_CTRL; i++ ){
pData->beginX += speedX;
pData->beginY += speedY;
bg.blit(pData->beginX, pData->beginY, pData->beginX, pData->beginY, stand.point()->w, stand.point()->h, 2, 2);
stand.blit(pData->beginX, pData->beginY);
pData->screen.flip();
SDL_Delay(10);
}

return 0;
}
最后,我们修改了主演示程序,并测试了show()和reset()的效果。我们可以看到,直接修改线程数据的reset()的结果也是不可预知的,所以,我们似乎更应该通过改变线程“流”的效果,而不是直接对数据进行修改。这个我们以后再讨论了。
#include "SurfaceClass.hpp"
#include "amn.hpp"

int game(int argc ,char* argv[]);
int main(int argc ,char* argv[])
{
int mainRtn = 0;
try {
mainRtn = game(argc, argv);
}
catch ( const ErrorInfo& info ) {
info.show();
return -1;
}
catch ( const char* s ) {
std::cerr << s << std::endl;
return -1;
}

return mainRtn;
}

int game(int argc ,char* argv[])
{
//Create a SDL screen.
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const Uint32 SCREEN_FLAGS = 0; //SDL_FULLSCREEN | SDL_DOUBLEBUF | SDL_HWSURFACE
const std::string WINDOW_NAME = "Amn Test";
ScreenSurface screen(SCREEN_WIDTH, SCREEN_HEIGHT, WINDOW_NAME, 0, SCREEN_FLAGS);

PictureSurface bg("./images/background.png", screen);
bg.blit(0);
screen.flip();

AmnArg test1(0, 250, 600, 250, screen);
SDL_Thread* thread1 = test1.createThrd();

AmnArg test2(0, 0, 400, 0, screen);
SDL_Thread* thread2 = test2.createThrd();

SDL_Event gameEvent;
bool gameOver = false;
while ( gameOver == false ){
while ( SDL_PollEvent(&gameEvent) != 0 ){
if ( gameEvent.type == SDL_QUIT ){
gameOver = true;
}
if ( gameEvent.type == SDL_KEYDOWN ){
if ( gameEvent.key.keysym.sym == SDLK_ESCAPE ){
gameOver = true;
}
if ( gameEvent.key.keysym.sym == SDLK_SPACE ){
test1.show();
test2.show();
}
}
screen.flip();
}
SDL_Delay(100);
}

SDL_KillThread(thread1);
SDL_KillThread(thread2);

return 0;
}

[挂载人]初学MPEG

个人签名--------------------------------------------------------------------------------

Please Login (or Sign Up) to leave a comment