>更新:我已经重现了这个问题!向下滚动以查看代码。
快速笔记
-
我的酷睿i5 CPU有2个内核,超线程。
-
如果我调用
SetProcessAffinityMask(GetCurrentProcess(), 1)
,即使程序仍然是多线程的,也一切都很好。 -
如果我不这样做,并且程序在Windows XP上运行(在Windows 7 x64上很好!),当我滚动列表视图并加载图标时,我的GUI开始锁定几秒钟。
问题所在
基本上,当我在Windows XP(Windows 7很好)上运行下面发布的程序(我原始代码的简化版本)时,除非我为所有线程强制使用相同的逻辑CPU,否则程序UI开始滞后半秒左右。
(注意:这里对这篇文章进行了大量编辑,因为我进一步调查了这个问题。
请注意,线程数相同 - 只是关联掩码不同。
我已经尝试了两种不同的消息传递方法:内置GetMessage
以及我自己的BackgroundWorker
。
结果呢? BackgroundWorker
受益于对 1 个逻辑 CPU 的关联(几乎没有滞后),而GetMessage
则完全受此影响(滞后现在长达数秒)。
我不知道为什么会发生这种情况 - 多个CPU不应该比单个CPU工作得更好吗?!当线程数相同时,为什么会有这样的滞后?
<小时 />更多统计数据:
GetLogicalProcessorInformation
回报:
0x0: {ProcessorMask=0x0000000000000003 Relationship=RelationProcessorCore ...}
0x1: {ProcessorMask=0x0000000000000003 Relationship=RelationCache ...}
0x2: {ProcessorMask=0x0000000000000003 Relationship=RelationCache ...}
0x3: {ProcessorMask=0x0000000000000003 Relationship=RelationCache ...}
0x4: {ProcessorMask=0x000000000000000f Relationship=RelationProcessorPackage ...}
0x5: {ProcessorMask=0x000000000000000c Relationship=RelationProcessorCore ...}
0x6: {ProcessorMask=0x000000000000000c Relationship=RelationCache ...}
0x7: {ProcessorMask=0x000000000000000c Relationship=RelationCache ...}
0x8: {ProcessorMask=0x000000000000000c Relationship=RelationCache ...}
0x9: {ProcessorMask=0x000000000000000f Relationship=RelationCache ...}
0xa: {ProcessorMask=0x000000000000000f Relationship=RelationNumaNode ...}
<小时 />《守则》
下面的代码应该在Windows XP SP3上显示此问题。(至少,它在我的电脑上是这样!
比较这两者:
正常运行程序,然后滚动。您应该看到滞后。
使用
affinity
命令行参数运行程序,然后滚动。它应该几乎完全平滑。
为什么会这样?
#define _WIN32_WINNT 0x502
#include <tchar.h>
#include <Windows.h>
#include <CommCtrl.h>
#pragma comment(lib, "kernel32.lib")
#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "user32.lib")
LONGLONG startTick = 0;
LONGLONG QPC()
{ LARGE_INTEGER v; QueryPerformanceCounter(&v); return v.QuadPart; }
LONGLONG QPF()
{ LARGE_INTEGER v; QueryPerformanceFrequency(&v); return v.QuadPart; }
bool logging = false;
bool const useWindowMessaging = true; // GetMessage() or BackgroundWorker?
bool const autoScroll = false; // for testing
class BackgroundWorker
{
struct Thunk
{
virtual void operator()() = 0;
virtual ~Thunk() { }
};
class CSLock
{
CRITICAL_SECTION& cs;
public:
CSLock(CRITICAL_SECTION& criticalSection)
: cs(criticalSection)
{ EnterCriticalSection(&this->cs); }
~CSLock() { LeaveCriticalSection(&this->cs); }
};
template<typename T>
class ScopedPtr
{
T *p;
ScopedPtr(ScopedPtr const &) { }
ScopedPtr &operator =(ScopedPtr const &) { }
public:
ScopedPtr() : p(NULL) { }
explicit ScopedPtr(T *p) : p(p) { }
~ScopedPtr() { delete p; }
T *operator ->() { return p; }
T &operator *() { return *p; }
ScopedPtr &operator =(T *p)
{
if (this->p != NULL) { __debugbreak(); }
this->p = p;
return *this;
}
operator T *const &() { return this->p; }
};
Thunk **const todo;
size_t nToDo;
CRITICAL_SECTION criticalSection;
DWORD tid;
HANDLE hThread, hSemaphore;
volatile bool stop;
static size_t const MAX_TASKS = 1 << 18; // big enough for testing
static DWORD CALLBACK entry(void *arg)
{ return ((BackgroundWorker *)arg)->process(); }
public:
BackgroundWorker()
: nToDo(0), todo(new Thunk *[MAX_TASKS]), stop(false), tid(0),
hSemaphore(CreateSemaphore(NULL, 0, 1 << 30, NULL)),
hThread(CreateThread(NULL, 0, entry, this, CREATE_SUSPENDED, &tid))
{
InitializeCriticalSection(&this->criticalSection);
ResumeThread(this->hThread);
}
~BackgroundWorker()
{
// Clear all the tasks
this->stop = true;
this->clear();
LONG prev;
if (!ReleaseSemaphore(this->hSemaphore, 1, &prev) ||
WaitForSingleObject(this->hThread, INFINITE) != WAIT_OBJECT_0)
{ __debugbreak(); }
CloseHandle(this->hSemaphore);
CloseHandle(this->hThread);
DeleteCriticalSection(&this->criticalSection);
delete [] this->todo;
}
void clear()
{
CSLock lock(this->criticalSection);
while (this->nToDo > 0)
{
delete this->todo[--this->nToDo];
}
}
unsigned int process()
{
DWORD result;
while ((result = WaitForSingleObject(this->hSemaphore, INFINITE))
== WAIT_OBJECT_0)
{
if (this->stop) { result = ERROR_CANCELLED; break; }
ScopedPtr<Thunk> next;
{
CSLock lock(this->criticalSection);
if (this->nToDo > 0)
{
next = this->todo[--this->nToDo];
this->todo[this->nToDo] = NULL; // for debugging
}
}
if (next) { (*next)(); }
}
return result;
}
template<typename Func>
void add(Func const &func)
{
CSLock lock(this->criticalSection);
struct FThunk : public virtual Thunk
{
Func func;
FThunk(Func const &func) : func(func) { }
void operator()() { this->func(); }
};
DWORD exitCode;
if (GetExitCodeThread(this->hThread, &exitCode) &&
exitCode == STILL_ACTIVE)
{
if (this->nToDo >= MAX_TASKS) { __debugbreak(); /*too many*/ }
if (this->todo[this->nToDo] != NULL) { __debugbreak(); }
this->todo[this->nToDo++] = new FThunk(func);
LONG prev;
if (!ReleaseSemaphore(this->hSemaphore, 1, &prev))
{ __debugbreak(); }
}
else { __debugbreak(); }
}
};
LRESULT CALLBACK MyWindowProc(
HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
enum { IDC_LISTVIEW = 101 };
switch (uMsg)
{
case WM_CREATE:
{
RECT rc; GetClientRect(hWnd, &rc);
HWND const hWndListView = CreateWindowEx(
WS_EX_CLIENTEDGE, WC_LISTVIEW, NULL,
WS_CHILDWINDOW | WS_VISIBLE | LVS_REPORT |
LVS_SHOWSELALWAYS | LVS_SINGLESEL | WS_TABSTOP,
rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top,
hWnd, (HMENU)IDC_LISTVIEW, NULL, NULL);
int const cx = GetSystemMetrics(SM_CXSMICON),
cy = GetSystemMetrics(SM_CYSMICON);
HIMAGELIST const hImgList =
ImageList_Create(
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON),
ILC_COLOR32, 1024, 1024);
ImageList_AddIcon(hImgList, (HICON)LoadImage(
NULL, IDI_INFORMATION, IMAGE_ICON, cx, cy, LR_SHARED));
LVCOLUMN col = { LVCF_TEXT | LVCF_WIDTH, 0, 500, TEXT("Name") };
ListView_InsertColumn(hWndListView, 0, &col);
ListView_SetExtendedListViewStyle(hWndListView,
LVS_EX_DOUBLEBUFFER | LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
ListView_SetImageList(hWndListView, hImgList, LVSIL_SMALL);
for (int i = 0; i < (1 << 11); i++)
{
TCHAR text[128]; _stprintf(text, _T("Item %d"), i);
LVITEM item =
{
LVIF_IMAGE | LVIF_TEXT, i, 0, 0, 0,
text, 0, I_IMAGECALLBACK
};
ListView_InsertItem(hWndListView, &item);
}
if (autoScroll)
{
SetTimer(hWnd, 0, 1, NULL);
}
break;
}
case WM_TIMER:
{
HWND const hWndListView = GetDlgItem(hWnd, IDC_LISTVIEW);
RECT rc; GetClientRect(hWndListView, &rc);
if (!ListView_Scroll(hWndListView, 0, rc.bottom - rc.top))
{
KillTimer(hWnd, 0);
}
break;
}
case WM_NULL:
{
HWND const hWndListView = GetDlgItem(hWnd, IDC_LISTVIEW);
int const iItem = (int)lParam;
if (logging)
{
_tprintf(_T("@%I64lld ms:")
_T(" Received: #%dn"),
(QPC() - startTick) * 1000 / QPF(), iItem);
}
int const iImage = 0;
LVITEM const item = {LVIF_IMAGE, iItem, 0, 0, 0, NULL, 0, iImage};
ListView_SetItem(hWndListView, &item);
ListView_Update(hWndListView, iItem);
break;
}
case WM_NOTIFY:
{
LPNMHDR const pNMHDR = (LPNMHDR)lParam;
switch (pNMHDR->code)
{
case LVN_GETDISPINFO:
{
NMLVDISPINFO *const pInfo = (NMLVDISPINFO *)lParam;
struct Callback
{
HWND hWnd;
int iItem;
void operator()()
{
if (logging)
{
_tprintf(_T("@%I64lld ms: Sent: #%dn"),
(QPC() - startTick) * 1000 / QPF(),
iItem);
}
PostMessage(hWnd, WM_NULL, 0, iItem);
}
};
if (pInfo->item.iImage == I_IMAGECALLBACK)
{
if (useWindowMessaging)
{
DWORD const tid =
(DWORD)GetWindowLongPtr(hWnd, GWLP_USERDATA);
PostThreadMessage(
tid, WM_NULL, 0, pInfo->item.iItem);
}
else
{
Callback callback = { hWnd, pInfo->item.iItem };
if (logging)
{
_tprintf(_T("@%I64lld ms: Queued: #%dn"),
(QPC() - startTick) * 1000 / QPF(),
pInfo->item.iItem);
}
((BackgroundWorker *)
GetWindowLongPtr(hWnd, GWLP_USERDATA))
->add(callback);
}
}
break;
}
}
break;
}
case WM_CLOSE:
{
PostQuitMessage(0);
break;
}
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
DWORD WINAPI BackgroundWorkerThread(LPVOID lpParameter)
{
HWND const hWnd = (HWND)lpParameter;
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0 && msg.message != WM_QUIT)
{
if (msg.message == WM_NULL)
{
PostMessage(hWnd, msg.message, msg.wParam, msg.lParam);
}
}
return 0;
}
int _tmain(int argc, LPTSTR argv[])
{
startTick = QPC();
bool const affinity = argc >= 2 && _tcsicmp(argv[1], _T("affinity")) == 0;
if (affinity)
{ SetProcessAffinityMask(GetCurrentProcess(), 1 << 0); }
bool const log = logging; // disable temporarily
logging = false;
WNDCLASS wndClass =
{
0, &MyWindowProc, 0, 0, NULL, NULL, LoadCursor(NULL, IDC_ARROW),
GetSysColorBrush(COLOR_3DFACE), NULL, TEXT("MyClass")
};
HWND const hWnd = CreateWindow(
MAKEINTATOM(RegisterClass(&wndClass)),
affinity ? TEXT("Window (1 CPU)") : TEXT("Window (All CPUs)"),
WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, NULL, NULL);
BackgroundWorker iconLoader;
DWORD tid = 0;
if (useWindowMessaging)
{
CreateThread(NULL, 0, &BackgroundWorkerThread, (LPVOID)hWnd, 0, &tid);
SetWindowLongPtr(hWnd, GWLP_USERDATA, tid);
}
else { SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)&iconLoader); }
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
if (!IsDialogMessage(hWnd, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if (msg.message == WM_TIMER ||
!PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
{ logging = log; }
}
PostThreadMessage(tid, WM_QUIT, 0, 0);
return 0;
}
> 根据您在 http://ideone.com/fa2fM 发布的线程间计时,这里似乎存在公平性问题。 仅基于这一假设,以下是我对感知滞后的明显原因和问题的潜在解决方案的推理。
看起来窗口进程在一个线程上生成和处理了大量LVN_GETDISPINFO
消息,虽然后台工作线程能够以相同的速率跟上并将消息发布回窗口,但它发布的WM_NULL消息在队列中太远了,需要时间才能处理。
掩码时,会给系统带来更多的公平性,因为同一处理器必须为两个线程提供服务,这将限制相对于非关联性情况生成LVN_GETDISPINFO
消息的速率。 这意味着当您发布WM_NULL消息时,窗口 proc 消息队列可能不会那么深,这反过来意味着它们将被"更快地"处理。
似乎您需要以某种方式绕过排队效果。 使用SendMessage
、SendMessageCallback
或SendNotifyMessage
而不是PostMessage
可能是做到这一点的方法。 在SendMessage
情况下,您的工作线程将阻塞,直到窗口进程线程完成其当前消息并处理发送的WM_NULL消息,但您将能够将WM_NULL消息更均匀地注入消息处理流。 有关排队与非排队消息处理的说明,请参阅此页。
如果您选择使用 SendMessage
,但由于 SendMessage
的阻塞性质,您不想限制获取图标的速率,那么您可以使用第三个线程。 I/O 线程会将消息发布到第三个线程,而第三个线程使用 SendMessage
将图标更新注入 UI 线程。 通过这种方式,您可以控制满足图标请求的队列,而不是将它们交错到窗口进程消息队列中。
和WinXP之间的行为差异,可能有很多原因导致你似乎没有在Win7上看到这种影响。 可能是列表视图公共控件的实现方式不同,并限制了生成LVN_GETDISPINFO消息的速率。 或者,Win7 中的线程计划机制可能更频繁或更公平地切换线程上下文。
编辑:
根据您的最新更改,请尝试以下操作:
...
struct Callback
{
HWND hWnd;
int iItem;
void operator()()
{
if (logging)
{
_tprintf(_T("@%I64lld ms: Sent: #%dn"),
(QPC() - startTick) * 1000 / QPF(),
iItem);
}
SendNotifyMessage(hWnd, WM_NULL, 0, iItem); // <----
}
};
...
DWORD WINAPI BackgroundWorkerThread(LPVOID lpParameter)
{
HWND const hWnd = (HWND)lpParameter;
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0 && msg.message != WM_QUIT)
{
if (msg.message == WM_NULL)
{
SendNotifyMessage(hWnd, msg.message, msg.wParam, msg.lParam); // <----
}
}
return 0;
}
编辑2:
在确定LVN_GETDISPINFO
消息被放置在队列中使用 SendMessage
而不是 PostMessage
后,我们不能使用SendMessage
自己绕过它们。
仍然假设在图标结果从工作线程发回之前,wndproc 正在处理大量消息,我们需要另一种方法来在更新准备就绪后立即处理它们。
这个想法是这样的:
工作线程将结果放置在类似队列的同步数据结构中,然后(使用
PostMessage
)向 wndproc 发布WM_NULL消息(以确保 wndproc 在将来的某个时候被执行)。在 wndproc 的顶部(在 case 语句之前),UI 线程检查同步的类似队列的数据结构以查看是否有任何结果,如果有,则从类似队列的数据结构中删除一个或多个结果并处理它们。
线程相关性关系不大,而与告诉列表视图每次更新列表项时都需要更新列表项有关。由于未将 LVIF_DI_SETITEM
标志添加到LVN_GETDISPINFO
处理程序中的pInfo->item.mask
,并且手动调用ListView_Update
,因此在调用 ListView_Update
时,列表视图会使仍将其iImage
设置为 I_IMAGECALLBACK
的任何项失效。
您可以通过以下两种方式之一(或两者的组合)解决此问题:
从
WM_NULL
处理程序中删除ListView_Update。当您设置图像时,列表视图将自动重绘您在WM_NULL
处理程序中为其设置图像的项,并且不会尝试重绘您多次未设置图像的项。
在LVN_GETDISPINFO
处理程序中pInfo->item.mask
设置LVIF_DI_SETITEM
标志,并将pInfo->item.iImage
设置为未I_IMAGECALLBACK
的值。
我在 Vista 上复制了类似的可怕行为,滚动整页。执行上述任一操作都解决了问题,同时仍异步更新图标。
-
建议这与XP的超线程/逻辑核心调度有关是合理的,我将第二个IvoTops建议在禁用超线程的情况下尝试此操作。请尝试此操作并告诉我们。
为什么?因为:
a) 逻辑内核为 CPU 密集型任务提供较差的并行性。在同一物理内核上的两个逻辑 HT 内核上运行多个 CPU 绑定线程不利于性能。例如,请参阅这篇英特尔论文 - 它解释了启用 HT 如何导致典型服务器线程导致每个请求的延迟或处理时间增加(同时提高净吞吐量)。
b) Windows 7确实有一些HT/SMT(对称多线程)调度改进。Mark Russinovich的幻灯片在这里简要提到了这一点。尽管他们声称XP调度程序可以识别SMT,但Windows 7明确修复了此问题的事实意味着XP中可能缺少某些东西。所以我猜操作系统没有适当地设置对第二个内核的线程亲和力。(也许是因为第二个内核在调度第二个线程的那一刻可能没有空闲,可以疯狂推测)。
-
您写道:"我刚刚尝试将进程(甚至单个线程)的 CPU 关联设置为我能想到的所有潜在组合,在相同和不同的逻辑 CPU 上"。
设置后,我们可以尝试验证执行是否确实在第二个内核上吗?
您可以在任务管理器或perfmon/perf计数器中直观地检查这一点
也许在设置线程亲和力的地方发布代码(我注意到您没有检查 SetProcessorAffinity 上的返回值,也请检查一下。
如果 Windows 性能计数器没有帮助,英特尔的 VTune 性能分析器对这类事情很有帮助。
我认为您可以使用任务管理器手动强制线程亲和力。
还有一件事:你的核心i5要么是Nehalem,要么是SandyBridge微架构。Nehalem和后来的HT实现与上一代架构(Core等)有很大不同。事实上,Microsoft建议禁用HT以在Nehalem之前的系统上运行Biztalk服务器。 因此,也许Windows XP不能很好地处理新的HT架构。
这可能是一个超线程错误。要检查这是否是导致它的原因,请在关闭超线程的情况下运行您的错误程序(在 BIOS 中,您通常可以将其关闭)。在过去的五年中,我遇到了两个问题,这些问题仅在启用超线程时才浮出水面。