游戏中的多线程同步问题

antimatterworld 2012-08-08 10:16:08
比如游戏中的主线程负责一帧的渲染,令一个线程负责一帧的逻辑处理。

发现使用如下方法造成了死锁:
逻辑线程使用一个死循环处理每一帧的逻辑。每一个循环处理逻辑后,将手动形式的Event设置为信号发出状态,然后挂起自己。
主线程是将手动形式的Event设置为非信号发出状态,然后Resume线程,然后Wait这个手动Event的信号发出状态。

想法似乎不错,就是出现了死锁的现象。

如果想让这两个线程在每一帧同步,在Windows下应该怎么处理这个问题?


Intel线程构建模块就不用了,小小的程序,暂时不需要这么强大的线程处理机制,再说还得考虑效率问题。
...全文
540 29 打赏 收藏 转发到动态 举报
写回复
用AI写文章
29 条回复
切换为时间正序
请发表友善的回复…
发表回复
tiandu0803 2012-08-23
  • 打赏
  • 举报
回复
受益良多,感谢楼主及众位大神分享
weiwuyuan 2012-08-21
  • 打赏
  • 举报
回复
[Quote=引用 26 楼 的回复:]
引用 24 楼 的回复:
ReleaseSemaphore(hRender,1,NULL);

这位仁兄,在Windows中,Mutex就是Semaphore的退化“物种”。由于线程可以重入,这个办法不行啊。



使用了一个Event+Worker线程消息队列,用DX SDK自带的工具测试了一下,在9.7秒的时间内渲染了7728个Frame,FPS大约796,平均每个Frame大约……
[/Quote]

看了这个测试,让我联想到了我当初的2D游戏:
就是在我还没写游戏的时候,我对FPS自信满满,却未做高强度的测试(实际上也不好测试,因为不仅仅是图形,还有很多其他逻辑),然而当我的游戏制作起来,并达到一定规模的时候,FPS出现问题了,倒不是低,而是渲染跟不上(多线程加再动画,然后渲染,结果动画都播完了,动画还没加载好.是800x600共20帧的法术动画),就是最后输在了图像的解压和加载上.而这个加载除了要创建纹理,还涉及到了大量的内存分配,让我深感初期的考虑太单薄了.
weiwuyuan 2012-08-21
  • 打赏
  • 举报
回复
[Quote=引用 25 楼 的回复:]
[Quote=引用 23 楼 的回复:]

C/C++ code

while (true)
{
// 加锁
if (InterlockedExchange(&g_Mutex, TRUE) == FALSE)
{
// 解锁
InterlockedExchange(&g_Mutex, FALSE);
……
[/Quote]

这个问题我之前想过,感觉应该没问题.
因为,在主线程里面,游戏总是要限制FPS的吧? 我的限制方法就是用Sleep(),而对于睡眠过度的情况,我还会做一些补偿,基本上可以达到我期望的FPS.
另外,子线程的Sleep()就不会影响到FPS了.

不过,看楼主的代码,似乎WaitForSingleObject()对CPU的多核,多线程处理更好一些,
只是我担心这个东西才会影响到FPS,在我的实现里,限制FPS,就是靠一个Sleep()或者开启垂直同步,
如果其他地方还有一个可能Sleep()的东西,那整个FPS限制体制就被影响到了.
正因为WaitForSingleObject()会阻塞,所以,我总是尽可能的避免使用它.
antimatterworld 2012-08-20
  • 打赏
  • 举报
回复
[Quote=引用 24 楼 的回复:]
ReleaseSemaphore(hRender,1,NULL);
[/Quote]
这位仁兄,在Windows中,Mutex就是Semaphore的退化“物种”。由于线程可以重入,这个办法不行啊。



使用了一个Event+Worker线程消息队列,用DX SDK自带的工具测试了一下,在9.7秒的时间内渲染了7728个Frame,FPS大约796,平均每个Frame大约需要1.25毫秒。
左上角的三角形是主线程自己渲染的,其他的三角形是用CPU修改了三个顶点的位置,并且修改了HLSL中控制颜色的变量。主线程渲染的三角形使用的顶点缓冲区是一直固定不变的,也没有被CPU修改的。
antimatterworld 2012-08-20
  • 打赏
  • 举报
回复
[Quote=引用 23 楼 的回复:]
while (true)
{
// 加锁
if (InterlockedExchange(&g_Mutex, TRUE) == FALSE)
{
// 解锁
InterlockedExchange(&g_Mutex, FALSE);
}

Sleep(0);
}

如果这样子也效果不好的话,那我就没别的办法了,我只会这么做.
[/Quote]

感谢这位仁兄这么关心俺的帖子,并且认真编写了一定量的代码。
首先在 if (InterlockedExchange(&g_Mutex, TRUE) == FALSE)中,如果在一定时间内g_Mutex总为false,就会有多个无用判断,浪费了CPU的计算资源。虽然使用Sleep(0);能让该线程暂停一会,但是这又限制了FPS,因为Sleep()可以阻挡大约15毫秒的线程时间,让线程暂停大约15毫秒(根据系统的硬件配置和操作系统版本有关)。如果更点倍的,在线程暂停的时候g_Mutex又为true了,if判断的时候又为false了,情况就更糟糕了,可能导致游戏的长时间暂停。
值得表扬的是
volatile LONG    g_Mutex = FALSE;

volatile关键字的使用说明楼主具有多线程编程的经验和扎实的C++基础。

俺只使用了一个Event句柄,和Worker线程消息队列。应该不会对FPS有太大影响。
Kevin_qing 2012-08-20
  • 打赏
  • 举报
回复
render:
while(run){
waitforsingleobject(hRender);

//do work
....
//

ReleaseSemaphore(hRender,1,NULL);
}

main:
msploop{

waitforsingleobject(hRender);
//dowork;
.....

ReleaseSemaphore(hRender,1,NULL);
Sleep(0);
}

weiwuyuan 2012-08-20
  • 打赏
  • 举报
回复

while (true)
{
// 加锁
if (InterlockedExchange(&g_Mutex, TRUE) == FALSE)
{
// 解锁
InterlockedExchange(&g_Mutex, FALSE);
}

Sleep(0);
}


如果这样子也效果不好的话,那我就没别的办法了,我只会这么做.
weiwuyuan 2012-08-20
  • 打赏
  • 举报
回复
更正下代码:

#include <stdio.h>
#include <windows.h>


// 假设是这样一种结构:
// 1:帧开始
// 2:计算AI和物理
// 3:处理图形逻辑
// 4:进行渲染
// 5:在渲染的过程中,计算下一帧的AI和物理
// 6:渲染完毕,重复3

enum
{
FRAME_STATE_AI = 0x00000001, // 包括物理计算
FRAME_STATE_LOGIC = 0x00000002, // 图形逻辑处理
FRAME_STATE_RENDER = 0x00000004, // 渲染状态
};

volatile LONG g_Mutex = FALSE;
UINT g_FrameState = FRAME_STATE_AI;



// 渲染线程
void MainThread_Render()
{
while (true)
{
// 加锁
if (InterlockedExchange(&g_Mutex, TRUE) == FALSE)
{
// 处于渲染状态
if (g_FrameState & FRAME_STATE_RENDER)
{
// 渲染部分
{
printf("渲染开始...\n");
getchar();
}

g_FrameState &= ~FRAME_STATE_RENDER; // 清除渲染状态

printf("渲染完毕...\n");
getchar();
}

// 解锁
InterlockedExchange(&g_Mutex, FALSE);
}

Sleep(0);
}
}

// AI逻辑处理(包括物理)
DWORD WINAPI SubThread_AILogic(LPVOID lpParam)
{
while (true)
{
// 加锁
if (InterlockedExchange(&g_Mutex, TRUE) == FALSE)
{
// 处于AI计算状态
if (g_FrameState & FRAME_STATE_AI)
{
// 执行AI计算
{
printf("AI计算开始...\n");
getchar();
}

g_FrameState &= ~FRAME_STATE_AI; // 清除AI计算状态
g_FrameState |= FRAME_STATE_LOGIC; // 设置图形逻辑状态

printf("AI计算完毕...\n");
getchar();
}

// 解锁
InterlockedExchange(&g_Mutex, FALSE);
}

Sleep(0);
}
}

// 图形逻辑线程
DWORD WINAPI SubThread_GraphicsLogic(LPVOID lpParam)
{
while (true)
{
// 加锁
if (InterlockedExchange(&g_Mutex, TRUE) == FALSE)
{
// 当前处于图形逻辑处理状态下,且不在渲染状态下
if ((g_FrameState & FRAME_STATE_LOGIC) && !(g_FrameState & FRAME_STATE_RENDER))
{
// 执行图形逻辑
{
printf("图形逻辑处理开始...\n");
getchar();
}

g_FrameState &= ~FRAME_STATE_LOGIC; // 清除图形逻辑状态
g_FrameState |= FRAME_STATE_RENDER; // 设置渲染状态
g_FrameState |= FRAME_STATE_AI; // 设置AI计算状态(下一帧)

printf("图形逻辑处理完毕...\n");
getchar();
}

// 解锁
InterlockedExchange(&g_Mutex, FALSE);
}

Sleep(0);
}
}



int main()
{
::CreateThread(NULL, 0, SubThread_AILogic, NULL, 0, NULL);
::CreateThread(NULL, 0, SubThread_GraphicsLogic, NULL, 0, NULL);

while (true)
{
MainThread_Render();
}

return 0;
}
weiwuyuan 2012-08-20
  • 打赏
  • 举报
回复
你不说清AI和物理和图形逻辑之间的依赖关系,我也只能自己猜测了。
我不觉得InterlockedExchange()会白白消耗CPU,因为不会一直去调用.

以下代码是我的一种理解:

#include <stdio.h>
#include <windows.h>


// 假设是这样一种结构:
// 1:帧开始
// 2:计算AI和物理
// 3:处理图形逻辑
// 4:进行渲染
// 5:在渲染的过程中,计算下一帧的AI和物理
// 6:渲染完毕,重复3

enum
{
FRAME_STATE_AI = 0x00000001, // 包括物理计算
FRAME_STATE_LOGIC = 0x00000002, // 图形逻辑处理
FRAME_STATE_RENDER = 0x00000004, // 渲染状态
};

volatile LONG g_Mutex = FALSE;
UINT g_FrameState = FRAME_STATE_AI;



// 渲染线程
void MainThread_Render()
{
while (true)
{
// 加锁
if (InterlockedExchange(&g_Mutex, TRUE) == TRUE)
continue;

// 处于渲染状态
if (g_FrameState & FRAME_STATE_RENDER)
{
// 渲染部分
{
printf("渲染开始...\n");
getchar();
}

g_FrameState &= ~FRAME_STATE_RENDER; // 清除渲染状态

printf("渲染完毕...\n");
getchar();
}

// 解锁
InterlockedExchange(&g_Mutex, FALSE);
Sleep(0);
}
}

// AI逻辑处理(包括物理)
DWORD WINAPI SubThread_AILogic(LPVOID lpParam)
{
while (true)
{
// 加锁
if (InterlockedExchange(&g_Mutex, TRUE) == TRUE)
continue;

// 处于AI计算状态
if (g_FrameState & FRAME_STATE_AI)
{
// 执行AI计算
{
printf("AI计算开始...\n");
getchar();
}

g_FrameState &= ~FRAME_STATE_AI; // 清除AI计算状态
g_FrameState |= FRAME_STATE_LOGIC; // 设置图形逻辑状态

printf("AI计算完毕...\n");
getchar();
}

// 解锁
InterlockedExchange(&g_Mutex, FALSE);
Sleep(0);
}
}

// 图形逻辑线程
DWORD WINAPI SubThread_GraphicsLogic(LPVOID lpParam)
{
while (true)
{
// 加锁
if (InterlockedExchange(&g_Mutex, TRUE) == TRUE)
continue;

// 当前处于图形逻辑处理状态下,且不在渲染状态下
if ((g_FrameState & FRAME_STATE_LOGIC) && !(g_FrameState & FRAME_STATE_RENDER))
{
// 执行图形逻辑
{
printf("图形逻辑处理开始...\n");
getchar();
}

g_FrameState &= ~FRAME_STATE_LOGIC; // 清除图形逻辑状态
g_FrameState |= FRAME_STATE_RENDER; // 设置渲染状态
g_FrameState |= FRAME_STATE_AI; // 设置AI计算状态(下一帧)

printf("图形逻辑处理完毕...\n");
getchar();
}

// 解锁
InterlockedExchange(&g_Mutex, FALSE);
Sleep(0);
}
}



int main()
{
::CreateThread(NULL, 0, SubThread_AILogic, NULL, 0, NULL);
::CreateThread(NULL, 0, SubThread_GraphicsLogic, NULL, 0, NULL);

while (true)
{
MainThread_Render();
}

return 0;
}
antimatterworld 2012-08-20
  • 打赏
  • 举报
回复
由于CPU的核心越来越多,同时又有了PPU(物理处理单元)的辅助,所以未来的游戏,在处理AI和物理方面所需的时间就比较少了。
而CPU处理图形逻辑不能与GPU处理图形的时间并行起来,也就是,必须等CPU处理完图形逻辑之后,再把数据送到GPU之后,GPU才能处理图形的渲染。
所以,如果AI和物理逻辑早早的就处理完了,就差图形逻辑没处理完了,这就会导致FPS卡在了图形逻辑上。
而Direct11以前的版本,无法将图形逻辑多线程话。所以最可能出现的情况是,多核CPU早就把AI和物理逻辑处理完毕,而CPU只能有1个核心正在处理图形逻辑,其他CPU核心处于闲置状态。
Direct11以来,引入了多线程渲染机制,可以充分利用每个CPU的核心,并行处理图形逻辑,最大程度缩短图形逻辑的时间。充分利用每个CPU核心。
如果使用InterlockedExchange(),就等于使用了自旋锁,所以线程在完成任务后,也会不断地检测变量,占用CPU资源。





图中,已经清楚地表示了在单线程处理图形逻辑的情况下,购买更多核心的CPU,也是白费。
weiwuyuan 2012-08-20
  • 打赏
  • 举报
回复
另外,也不知道AI线程和物理线程是否互相影响?

最好把每一个线程的影响关系,依赖关系列个表,说明清楚.
weiwuyuan 2012-08-20
  • 打赏
  • 举报
回复
是不是这样的关系?

当前一帧的依赖顺序:
1:执行AI和物理逻辑
2:执行图形逻辑(这一步应该就是把AI和物理逻辑的演算结果作为最终的渲染依据)
3:执行渲染
4:在渲染的过程中计算下一帧的AI和物理逻辑.
5:渲染完毕
weiwuyuan 2012-08-20
  • 打赏
  • 举报
回复
图形逻辑会影响渲染线程,这个一确定了.
但是,AI和物理影响哪些线程?
上面的图表,只能说明不影响渲染线程.

这个关系还是有点不清晰。

但有一点,同步方面InterlockedExchange()就足够了.
InterlockedExchange()只用来保证某个状态量的安全修改.
antimatterworld 2012-08-19
  • 打赏
  • 举报
回复
[Quote=引用 15 楼 的回复:]
先前不是说,主线程在渲染状态下,逻辑线程处于挂起等待状态么?
[/Quote]
那是说,主线程在渲染状态下,与图形渲染有关的逻辑线程处于等待状态。与AI和物理相关的线程可能正在计算下一个Frame的逻辑。

渲染一个Frame所需的时间,图中已经表示的很清楚了,GPU的处理时间显然是固定的,我们需要尽量充分利用CPU的每个核心,缩短CPU处理图形逻辑和AI物理逻辑的时间。
由于AI和物理的计算可以比较简单的用多线程实现,剩下的就是让图形的处理也可能用多线程实现。
weiwuyuan 2012-08-18
  • 打赏
  • 举报
回复
[Quote=引用 14 楼 的回复:]
以后的CPU的核心越来越多,所以要尽量让每个CPU核心都能分配到计算任务。
通常大型游戏,是在主线程渲染第n个Frame的同时,使用其他线程计算出第n+1个Frame的逻辑。
即使在单核心CPU的时候,调用Draw()函数启动显卡的渲染过程,在显卡渲染完成之前这个Draw()函数不会返回,也就是显卡在处理渲染流程的过程中,主线程将阻塞在Draw()函数上,此时CPU就会空闲。
[/Quote]

先前不是说,主线程在渲染状态下,逻辑线程处于挂起等待状态么?
antimatterworld 2012-08-17
  • 打赏
  • 举报
回复
以后的CPU的核心越来越多,所以要尽量让每个CPU核心都能分配到计算任务。
通常大型游戏,是在主线程渲染第n个Frame的同时,使用其他线程计算出第n+1个Frame的逻辑。
即使在单核心CPU的时候,调用Draw()函数启动显卡的渲染过程,在显卡渲染完成之前这个Draw()函数不会返回,也就是显卡在处理渲染流程的过程中,主线程将阻塞在Draw()函数上,此时CPU就会空闲。
antimatterworld 2012-08-17
  • 打赏
  • 举报
回复
用CPU对顶点、索引、常量缓冲区进行数据修改和填充是需要使用CPU核心的。
在游戏中如果有多个顶点、索引、常量缓冲区需要CPU对其进行数据的修改和填充,显然用多核同时对多个缓冲区进行数据加工比单线程处理要节省很多时间。如果用单个线程处理,只有1个CPU核心在工作,其他的CPU可能处于空闲状态。如果AI和物理模拟的计算量不是很大,至少可以让更多的CPU核心来处理缓冲数据的修改。

这是自DX10以来的新功能,可以让DX充分利用多核CPU。
weiwuyuan 2012-08-17
  • 打赏
  • 举报
回复
[Quote=引用 11 楼 的回复:]
引用 10 楼 的回复:

主线程每收到一次子线程的通知就渲染一次,否则不渲染,等待?
然后子线程也是每次处理完一帧的逻辑后,也要挂起,等待主线程渲染完毕,再去执行下一帧的逻辑?

你的结构是不是这样?

说得非常正确。
[/Quote]

不太明白这和单线程有什么区别?
不都是先执行逻辑,再执行渲染,这样一直重复下去?

另一方面,建议尽可能避免加锁,能用原子操作,就用原子操作(InterlockedExchange)
antimatterworld 2012-08-16
  • 打赏
  • 举报
回复
[Quote=引用 10 楼 的回复:]

主线程每收到一次子线程的通知就渲染一次,否则不渲染,等待?
然后子线程也是每次处理完一帧的逻辑后,也要挂起,等待主线程渲染完毕,再去执行下一帧的逻辑?

你的结构是不是这样?
[/Quote]
说得非常正确。
weiwuyuan 2012-08-16
  • 打赏
  • 举报
回复
主线程每收到一次子线程的通知就渲染一次,否则不渲染,等待?
然后子线程也是每次处理完一帧的逻辑后,也要挂起,等待主线程渲染完毕,再去执行下一帧的逻辑?

你的结构是不是这样?
加载更多回复(9)

8,303

社区成员

发帖
与我相关
我的任务
社区描述
游戏开发相关内容讨论专区
社区管理员
  • 游戏开发
  • 呆呆敲代码的小Y
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧