Отрывок из книги "Programmer's Stone "
Автор: Alan G Carter and Colston Sanger
Перевод: Сергей Козлов teleman@elnet.msk.ru
7 декабря 1999 г.
Единственный аргумент, который мы можем предложить -- это обещание, что это произойдет на самом деле. И хотя это ничего не доказывает, все что мы можем сделать -- это показать работающий пример приведения в минимальное состояние. И это работает -- спросите любого, кто пытался.
В качестве примера возьмем код из прекрасной книги Джеффри Рихтера (Jeffrey Richter's Advanced Windows). Эта книга - полезное чтение для любого, кто пытается писать программы для Win32 API (Application Programming Interface) (поскольку иначе у вас не появится мысленная карта семантик системы).
Рихтер очень четко раскладывает по полочкам вопросы использования Win32, но даже в его примерах (и, в частности, как результат соглашений, которым он следует) появляется сложность, которую мы попробуем убрать. На странице 319 имеется функция SecondThread() Мы просто посмотрим на эту функцию, опустив остальную программу и некоторые глобальные определения:
DWORD WINAPI SecondThread (LPVOID lpwThreadParm) { BOOL fDone = FALSE; DWORD dw; while (!fDone) { // Wait forever for the mutex to become signaled. dw = WaitForSingleObject(g_hMutex, INFINITE); if (dw == WAIT_OBJECT_0) { // Mutex became signalled. if (g_nIndex >= MAX_TIMES) { fDone = TRUE; } else { g_nIndex++; g_dwTimes[g_nIndex - 1] = GetTickCount(): } // Release the mutex. ReleaseMutex(g_hMutex); } else { // The mutex was abandoned. break;// Exit the while loop. } } return(0); }
DWORD WINAPI SecondThread(LPVOID lpwThreadParm) { BOOL fDone = FALSE; DWORD dw; while(!fDone) { // Wait forever for the mutex to become signaled. dw = WaitForSingleObject(g_hMutex, INFINITE); if(dw == WAIT_OBJECT_0) { // Mutex became signalled. if(g_nIndex >= MAX_TIMES) { fDone = TRUE; } else { g_nIndex++; g_dwTimes[g_nIndex - 1] = GetTickCount(): } ReleaseMutex(g_hMutex); } else { // The mutex was abandoned. break;// Exit the while loop. } } return(0); }
DWORD WINAPI SecondThread (LPVOID lpwThreadParm) { BOOL fDone = FALSE; while (!fDone) { // Wait forever for the mutex to become signaled. if (WaitForSingleObject(g_hMutex, INFINITE)==WAIT_OBJECT_0) { // Mutex became signalled. if (g_nIndex < MAX_TIMES) { g_dwTimes[g_nIndex++] = GetTickCount(); } else { fDone = TRUE; } ReleaseMutex(g_hMutex); } else { // The mutex was abandoned. break;// Exit the while loop. } } return(0); }
Прерывание цикла (break) зависит только от результата WaitForSingleObject, поэтому естественно переместить проверку в управляющее выражение, избавляясь от прерывания цикла и одного уровня вложенности:
Теперь просто сожмем... Мы знаем, многие стандарты кодирования говорят, что мы всегда должны ставить фигурные скобки, поскольку иногда глупые люди допускают неочевидные ошибки, но посмотрите, что получается, когда мы пренебрегаем этим правилом и концентрируемся на повышении читаемости кода.
Теперь немного настоящей ереси. Черт возьми, в момент когда мы покончим с этой полной безответственностью, результат окажется совершенно неочевидным. (Здравый смысл поможет сделать лучше, чем правила.)
Ересь в том, что если мы знаем, для чего наши переменные, то мы знаем их типы. Если мы не знаем, для чего предназначена переменная, знание ее типа немногим поможет. В любом случае, компилятор все равно сделает проверку типов. Поэтому избавимся от венгерской записи, а заодно и от переопределений типов, которые просто определены ( #define ), но не для нас. Сокрытие разыменования используя typedef другое бесцельное упражнение, поскольку хотя и позволяет выполнить некоторую инкапсуляцию валюты, этого совершенно недостаточно, чтобы избавиться от беспокойства по этому поводу, поэтому аккуратные программисты вынуждены отслеживать настоящие типы в голове. Поддержка концепции дальних указателей в именах переменных для 32 битного API с плоской адресацией -- тоже довольно глупое занятие.
Одиннадцать строк против 26. На один уровень меньшая вложенность, но структура полностью прозрачна. Две локальных переменных исчезли. Нет блоков. Совсем нет вложенных else. Меньше мест, где могут скрываться ошибки.
(Если вы еще не программировали используя потоки (threads), то повторная проверка значения Index внутри тела цикла кажется грубой и ненужной. Если же программировали, то повторная проверка естественна и очевидна. Это очень важно: пока текущий поток приостановлен в WaitForSingleObject другой поток будет скорее всего активен и изменит значение. То, что для вас очевидно, зависит от вашего опыта: еще одна мораль из этого примера -- рассмотрения только структуры куска кода недостаточно.)
Наконец, текст делает совершенно ясным, что разные потоки выполняют функции в разных контекстах. Поэтому совершенно не нужно определять функцию с именем FirstThread(), в точности такую же, как SecondThread(), и вызывать их так:
DWORD WINAPI SecondThread (LPVOID lpwThreadParm)
{
BOOL fDone = FALSE;
{
BOOL fDone = FALSE;
while (!fDone && WaitForSingleObject(g_hMutex, INFINITE)==WAIT_OBJECT_0)
{
// Mutex became signalled.
if (g_nIndex < MAX_TIMES)
{
g_dwTimes[g_nIndex++] = GetTickCount();
}
else
{
fDone = TRUE;
}
ReleaseMutex(g_hMutex);
}
return(0);
}
{
// Mutex became signalled.
if (g_nIndex < MAX_TIMES)
{
g_dwTimes[g_nIndex++] = GetTickCount();
}
else
{
fDone = TRUE;
}
ReleaseMutex(g_hMutex);
}
return(0);
}
DWORD WINAPI SecondThread (LPVOID lpwThreadParm)
{
BOOL fDone = FALSE;
{
BOOL fDone = FALSE;
while (!fDone && WaitForSingleObject(g_hMutex, INFINITE)==WAIT_OBJECT_0)
{
if (g_nIndex < MAX_TIMES)
g_dwTimes[g_nIndex++] = GetTickCount();
else
fDone = TRUE;
ReleaseMutex(g_hMutex);
}
return(0);
}
{
if (g_nIndex < MAX_TIMES)
g_dwTimes[g_nIndex++] = GetTickCount();
else
fDone = TRUE;
ReleaseMutex(g_hMutex);
}
return(0);
}
Теперь немного настоящей ереси. Черт возьми, в момент когда мы покончим с этой полной безответственностью, результат окажется совершенно неочевидным. (Здравый смысл поможет сделать лучше, чем правила.)
Ересь в том, что если мы знаем, для чего наши переменные, то мы знаем их типы. Если мы не знаем, для чего предназначена переменная, знание ее типа немногим поможет. В любом случае, компилятор все равно сделает проверку типов. Поэтому избавимся от венгерской записи, а заодно и от переопределений типов, которые просто определены ( #define ), но не для нас. Сокрытие разыменования используя typedef другое бесцельное упражнение, поскольку хотя и позволяет выполнить некоторую инкапсуляцию валюты, этого совершенно недостаточно, чтобы избавиться от беспокойства по этому поводу, поэтому аккуратные программисты вынуждены отслеживать настоящие типы в голове. Поддержка концепции дальних указателей в именах переменных для 32 битного API с плоской адресацией -- тоже довольно глупое занятие.
DWORD SecondThread (void *ThreadParm)
{
BOOL done = FALSE;
{
BOOL done = FALSE;
while (!done && WaitForSingleObject(Mutex, INFINITE)==WAIT_OBJECT_0)
{
if (Index < MAX_TIMES)
Times[Index++] = GetTickCount();
else
done = TRUE;
{
if (Index < MAX_TIMES)
Times[Index++] = GetTickCount();
else
done = TRUE;
ReleaseMutex(Mutex); } return(0); }Теперь посмотрим. Мы вышли на Плато Качества...
DWORD SecondThread(void *ThreadParm) { while(Index < MAX_TIMES && WaitForSingleObject(Mutex, INFINITE) == WAIT_OBJECT_0) { if (Index < MAX_TIMES) Times[Index++] = GetTickCount(): ReleaseMutex(Mutex); } return(0); }
(Если вы еще не программировали используя потоки (threads), то повторная проверка значения Index внутри тела цикла кажется грубой и ненужной. Если же программировали, то повторная проверка естественна и очевидна. Это очень важно: пока текущий поток приостановлен в WaitForSingleObject другой поток будет скорее всего активен и изменит значение. То, что для вас очевидно, зависит от вашего опыта: еще одна мораль из этого примера -- рассмотрения только структуры куска кода недостаточно.)
Наконец, текст делает совершенно ясным, что разные потоки выполняют функции в разных контекстах. Поэтому совершенно не нужно определять функцию с именем FirstThread(), в точности такую же, как SecondThread(), и вызывать их так:
hThreads[0] = CreateThread(..., FirstThread, ...); hThreads[1] = CreateThread(..., SecondThread, ...);
Когда можно просто
hThreads[0] = CreateThread(..., TheThread, ...); hThreads[1] = CreateThread(..., TheThread, ...);
Почти треть этого примера получена клонированием! Если мы обнаружим ошибку в одной реализации, мы будем должны не забыть исправить аналогичные ошибки везде. Зачем беспокоиться, когда можно просто слить их в одну. Это хороший способ выхода из тупиков.
Комментариев нет:
Отправить комментарий