【Win32多线程】如何避免线程等待浪费CPU时间,降低对系统资源的冲击?
如何避免线程浪费CPU时间?
等待是线程的必要之恶。
两个等待技术:
1.Win32的Sleep()函数,要求操作系统终止线程动作,直到度过某个指定时间之后才恢复。(不能事先知道等多久)
2.busy loop,不断调用GetExitCodeThread(),直到其结果不再是STILL_ACTIVE.(缺点浪费CPU时间),绝对不要在Win32中使用busy loop
下面的程序示范采用busy loop方式的耗时:
#define WIN32_LEAN_AND_MEAN#include <stdio.h>#include <stdlib.h>#include <windows.h>#include <time.h>DWORD WINAPI ThreadFunc(LPVOID);int main(){ HANDLE hThrd; DWORD exitCode = 0; DWORD threadId; DWORD begin; DWORD elapsed; puts("Timing normal function call..."); begin = GetTickCount(); ThreadFunc(0); //直接调用,花费时间0.063秒 elapsed = GetTickCount()-begin; printf("Function call took: %d.%.03d seconds\n\n", elapsed/1000, elapsed%1000); puts("Timing thread + busy loop..."); begin = GetTickCount(); hThrd = CreateThread(NULL, 0, ThreadFunc, (LPVOID)1, 0, &threadId ); /* This busy loop chews up lots of CPU time消耗时间最多 */ for (;;) { GetExitCodeThread(hThrd, &exitCode); if ( exitCode != STILL_ACTIVE ) break; } elapsed = GetTickCount()-begin; printf("Thread + busy loop took: %d.%.03d seconds\n", elapsed/1000, elapsed%1000); //总共花费20.062秒 CloseHandle(hThrd); return EXIT_SUCCESS;}/* * Cute little busy work routine that computes the value * of PI using probability. Highly dependent on having * a good random number generator (rand is iffy) */DWORD WINAPI ThreadFunc(LPVOID n){ int i; int inside = 0; double val; UNREFERENCED_PARAMETER(n); /* Seed the random-number generator */ srand( (unsigned)time( NULL ) ); for (i=0; i<1000000; i++) { double x = (double)(rand())/RAND_MAX; double y = (double)(rand())/RAND_MAX; if ( (x*x + y*y) <= 1.0 ) inside++; } val = (double)inside / i; printf("PI = %.4g\n", val*4);Sleep((DWORD)n*10000*2); return 0;}Windows 7 性能监视器
1、开始-运行(或者按Win+R键打开运行对话框)
2、输入“perfmon”, 然后回车
就可以进入“性能监视器”
它可以告诉我们CPU的忙碌程度,它是一个重要的工具。用以确定我们所写的程序的预期行为。
等待一个线程的结束:win32的改善方法
我们需要一个新版本的Sleep(),它能够在某个线程结束时被调用。WaitForSingleObject()函数可以这么做,第一个参数为核心对象的handle
DWORD WaitForSingleObject(
HANDLE hHandle, // handle to object
DWORD dwMilliseconds // time-out interval指定最长的等待时间
);
放置一个“线程核心对象”#2作为参数,将使线程#1开始睡眠,直到线程#2结束为止。
之前的busy loop 可以用下面代码取代:WaitForSingleObject(hThrd,INFINITE);
Win32 中大部分HANDLE表示的对象都能够作为WaitForSingleObject()的等到目标。
被激发的对象(Signaled Objects)
什么是被激发对象??
可被WaitForSingleObject()使用的核心对象有两种状态:激发与未被激发。WaitForSingleObject()会在目标物变成激发状态返回。当核心对象被激发时,会导致WaitForSingleObject()醒来。Wait()函数也是如此.当线程正在执行时,线程对象处于激发状态。当线程结束时,线程对象被激发了。因此,任何线程如果等待的是一个线程对象,将会在等待对象结束时被调用,因为当时线程对象自动变成激发状态。
线程如何应付激发对象?
------------------------等待多个对象---------------------------
写一个程序,使用最多三个线程来完成六项工作。函数名为ThreadFunc()用来执行某些事情然后返回。工作的执行是靠调用Sleep()来模拟。只要一个线程结束,就会有另外一个线程被产生做下个工作。
WaitForMultipleObject()允许你在同一个时间等待一个以上的对象。必须将一个由handles组成的数组交给此函数,并指定其中一个对象或是全部的对象。
DWORD WaitForMultipleObjects(
DWORD nCount, // number of handles in array
CONST HANDLE *lpHandles, // object-handle array
BOOL fWaitAll, // wait option 当为TRUE,表示所有的handles都必须激发,此函数才得以返回,否则任何一个handle激发时就返回。
DWORD dwMilliseconds // time-out interval
);
handles数组中的元素个数有上限,不能超过MAXIMUM_WAIT_OBJECTS.
/* * *等待多个线程,监视多个线程的状态 * Sample code for "Multithreading Applications in Win32" * * * Call ThreadFunc NUM_TASKS times, using * no more than THREAD_POOL_SIZE threads. * This version uses WaitForMultipleObjects * to provide a more optimal solution. * * */#define WIN32_LEAN_AND_MEAN#include <stdio.h>#include <stdlib.h>#include <windows.h>DWORD WINAPI ThreadFunc(LPVOID);#define THREAD_POOL_SIZE 3#define MAX_THREAD_INDEX THREAD_POOL_SIZE-1#define NUM_TASKS 6int main(){ HANDLE hThrds[THREAD_POOL_SIZE]; int slot = 0; DWORD threadId; int i; DWORD rc; for (i=1; i<=NUM_TASKS; i++) { /* Until we've used all threads in * * the pool, do not need to wait * * for one to exit */ if (i > THREAD_POOL_SIZE) { /* Wait for one thread to terminate */ rc = WaitForMultipleObjects( THREAD_POOL_SIZE, hThrds, FALSE, INFINITE ); slot = rc - WAIT_OBJECT_0; //得到具体的handles下标 /*MTVERIFY( slot >= 0 && slot < THREAD_POOL_SIZE );*/ printf("Slot %d terminated\n", slot ); CloseHandle(hThrds[slot]); } /* Create a new thread in the given * available slot */ hThrds[slot] = CreateThread(NULL, 0, ThreadFunc, (LPVOID)slot, 0, &threadId ); printf("Launched thread #%d (slot %d)\n", i, slot); slot++; } /* Now wait for all threads to terminate */ rc = WaitForMultipleObjects( THREAD_POOL_SIZE, hThrds, TRUE, INFINITE ); for (slot=0; slot<THREAD_POOL_SIZE; slot++) CloseHandle(hThrds[slot]); printf("All slots terminated\n"); return EXIT_SUCCESS;}/* * This function just calls Sleep for * a random amount of time, thereby * simulating some task that takes time. * * The param "n" is the index into * the handle array, kept for informational * purposes. */DWORD WINAPI ThreadFunc(LPVOID n){ srand( GetTickCount() ); Sleep((rand()%10)*800+500); printf("Slot %d idle\n", n); return ((DWORD)n);}------------------------在一个GUI程序中等待---------------------------
Windows中的标准消息循环如下:
while (GetMessage(&msg, NULL, 0, 0))
{ // Get Next message in queue
if(hDlgMain == NULL || !IsDialogMessage(hDlgMain,&msg))
{
TranslateMessage(&msg); /* Translate virtual key codes */
DispatchMessage(&msg); /* Dispatches message to window */
}
}
GetMessage()有点像特殊版本的WaitForSingleObject(),它等待消息而不是核心对象。一旦调用GetMessage(),除非有一个消息真正进入你的消息队列,否则它不会返回。
如何在主线程等待一个Handle?
如果使用WaitForSingleObject()或WaitForMultipleObjects()等待某个对象激发,则没有办法回到主消息循环中去。
为了解决这个问题,主消息循环必须修改,使它同时等待消息或是核心对象被激发。必须使用一个MsgWaitForMultipleObjects()函数。它会在“对象被激发”或“消息到达队列”时被唤醒而返回。
MsgWaitForMultipleObjects()多接受一个参数,指定哪些消息是观察对象。
DWORD MsgWaitForMultipleObjects(
DWORD nCount, // number of handles in array
CONST HANDLE pHandles, // object-handle array
BOOL fWaitAll, // wait option
DWORD dwMilliseconds, // time-out interval
DWORD dwWakeMask // input-event type 欲观测的用户输入消息
);
重建消息循环:
代码示例:更改后台打印
/* * * Sample code for "Multithreading Applications in Win32" * This is from Chapter 3, Listing 3-4 * * Demonstrate background printing and * using MsgWaitForMultipleObjects to * wait for threads to exit. */#define WIN32_LEAN_AND_MEAN#include <stdio.h>#include <stdlib.h>#include <windows.h>#include <windowsx.h>#include <commdlg.h>#include "resource.h"#include "MtVerify.h"//// Macro definitions//#define WM_SHOWBITMAP WM_APP#define WM_THREADCOUNT WM_APP+1#define MAX_PRINT_JOBS 64//// Structures//typedef struct{ // Information passed to background thread for printing HWND hDlg; HWND hWndParent; HDC hDc; BOOL bPrint; // TRUE if printing; char szText[256];} ThreadPrintInfo;//// Global variables//HANDLE hInst;HBITMAP gbmpDisplay;RECT gDisplayRect;int gNumPrinting = 0;// Handle to each created threadHANDLE gPrintJobs[64];// Height of bitmap returned by DrawTextint iHeight;// HWND of the dialog so other threads can find it.HWND hDlgMain;//// Function declarations//int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow);LRESULT CALLBACK MainWndProc(HWND hWnd, unsigned msg, WPARAM wParam, LPARAM lParam);LRESULT CALLBACK PrintDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);BOOL PrintDlg_OnInitDialog(HWND hwndDlg, HWND hwndFocus, LPARAM lParam);void PrintDlg_OnCommand(HWND hDlg, int id, HWND hwndCtl, UINT codeNotify);void PrintDlg_OnPaint(HWND hwnd);void PrintText(HWND hwndParent, char *pszText);void PrintToDisplay(HWND hwndParent, char *pszText);LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);DWORD WINAPI BackgroundPrintThread(LPVOID pVoid);/////////////////////////////////////////////////////////////// WinMain//// Main entry point of application. This will be a// dialog based app, not a normal window, so this// routine acts a little differently than "normal".//int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow){ MSG msg; HWND hWnd; WNDCLASS wc; BOOL quit; int exitCode; hInst = hInstance; if (!hPrevInstance) { memset(&wc, 0, sizeof(wc)); wc.lpfnWndProc = MainWndProc; wc.hInstance = hInstance; wc.hIcon = LoadIcon (hInstance, "GenIco"); wc.hCursor = LoadCursor(NULL,IDC_ARROW); wc.hbrBackground= GetSysColorBrush(COLOR_BACKGROUND); wc.lpszMenuName = "PRINTING_MENU"; wc.lpszClassName= "PrintDlgClass"; if (!RegisterClass(&wc)) return FALSE; } hWnd = CreateWindow( "PrintDlgClass", "Printing Hands-On", WS_OVERLAPPED|WS_CAPTION|WS_MINIMIZEBOX|WS_SYSMENU, CW_USEDEFAULT, // At this point we do not want to 0, // show the window until we know 0, // how big the Dialog Box is so 0, // that we can fit the main window NULL, // around it. NULL, hInstance, NULL); hDlgMain = CreateDialog(hInst, MAKEINTRESOURCE(IDD_PRINT), hWnd, PrintDlgProc); ShowWindow(hWnd, nCmdShow); ShowWindow(hDlgMain, SW_SHOW); quit = FALSE; exitCode = 0; while (!quit || gNumPrinting > 0) { // Wait for next message or object being signaled DWORD dwWake; dwWake = MsgWaitForMultipleObjects( gNumPrinting, gPrintJobs, FALSE, INFINITE, QS_ALLEVENTS); if (dwWake >= WAIT_OBJECT_0 && dwWake < WAIT_OBJECT_0 + gNumPrinting) { // Object has been signaled // Reorder the handle array so we do not leave // empty slots. Take the handle at the end of // the array and move it into the now-empty slot. int index = dwWake - WAIT_OBJECT_0; gPrintJobs[index] = gPrintJobs[gNumPrinting-1]; gPrintJobs[gNumPrinting-1] = 0; gNumPrinting--; SendMessage(hDlgMain, WM_THREADCOUNT, gNumPrinting, 0L); } // end if else if (dwWake == WAIT_OBJECT_0 + gNumPrinting) { while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { // Get Next message in queue if(hDlgMain == NULL || !IsDialogMessage(hDlgMain,&msg)) { if (msg.message == WM_QUIT) { quit = TRUE; exitCode = msg.wParam; break; } // end if TranslateMessage(&msg); DispatchMessage(&msg); } } // end while } } // end while return (exitCode); /* Returns the value from PostQuitMessage */}LRESULT CALLBACK MainWndProc(HWND hWnd, unsigned msg, WPARAM wParam, LPARAM lParam){ switch (msg) { case WM_CREATE: break; case WM_COMMAND: switch (wParam) { case IDM_ABOUT: DialogBox(hInst, "AboutBox", hWnd, (DLGPROC)About); break; case IDM_EXIT: PostQuitMessage(0); break; default: return (DefWindowProc(hWnd, msg, wParam, lParam)); } case WM_SETFOCUS: // ensure that the Dialog Box has the focus SetFocus(hDlgMain); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, msg, wParam, lParam); } return 0;}LRESULT CALLBACK PrintDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam){ switch (uMsg) { case WM_CLOSE: DestroyWindow(hDlg); hDlgMain = NULL; break; case WM_DESTROY: return TRUE; break; case WM_SHOWBITMAP: if (gbmpDisplay) DeleteObject(gbmpDisplay); gDisplayRect = *(RECT*)wParam; gbmpDisplay = (HBITMAP) lParam; InvalidateRect(hDlgMain, NULL, TRUE); break; case WM_THREADCOUNT: { HMENU hMenu; // Show number of threads SetDlgItemInt(hDlg, IDC_EDIT_THREADS, wParam, FALSE); // Enable/Disable File.Exit menu hMenu = GetMenu(GetParent(hDlg)); EnableMenuItem(hMenu, IDM_EXIT, wParam != 0); break; } // end case HANDLE_MSG(hDlg, WM_INITDIALOG, PrintDlg_OnInitDialog); HANDLE_MSG(hDlg, WM_COMMAND, PrintDlg_OnCommand); HANDLE_MSG(hDlg, WM_PAINT, PrintDlg_OnPaint); default: return (FALSE); } return 0;}BOOL PrintDlg_OnInitDialog(HWND hwndDlg, HWND hwndFocus, LPARAM lParam){ RECT rect; // Size parent to fit this dialog GetWindowRect(hwndDlg, &rect); SetWindowPos(GetParent(hwndDlg),NULL, 0,0, rect.right-rect.left, rect.bottom-rect.top+GetSystemMetrics(SM_CYMENU) +GetSystemMetrics(SM_CYCAPTION), SWP_NOMOVE | SWP_NOZORDER); SetDlgItemInt(hwndDlg, IDC_EDIT_THREADS, 0, FALSE); return TRUE;}void PrintDlg_OnCommand(HWND hDlg, int id,HWND hwndCtl, UINT codeNotify){ char szText[256]; switch (id) { case IDC_PRINT: GetDlgItemText(hDlg, IDC_EDIT_TEXT, szText, 256); PrintText(hDlg, szText); break; case IDC_DISPLAY: GetDlgItemText(hDlg, IDC_EDIT_TEXT, szText, 256); PrintToDisplay(hDlg, szText); break; case IDCANCEL: case IDM_EXIT: PostMessage(GetParent(hDlg),WM_DESTROY, (WPARAM)0, (LPARAM)0); DestroyWindow(hDlgMain); hDlgMain = NULL; break; default: break; }}void PrintDlg_OnPaint( HWND hwnd ){ PAINTSTRUCT paint; HWND hwndCtrl;HDC hdc; HDC hDcMem; HBITMAP bmpOld; RECT rect; POINT point;if (!gbmpDisplay)return; hwndCtrl = GetDlgItem(hwnd, IDC_OUTPUT); hdc = BeginPaint(hwnd, &paint); GetWindowRect(hwndCtrl, &rect); point = *((POINT *)&rect); ScreenToClient(hwnd, &point); hDcMem = CreateCompatibleDC(NULL); bmpOld = SelectObject(hDcMem, gbmpDisplay); // Copy bitmap to screen MTVERIFY( BitBlt(hdc, point.x+10, point.y+40, gDisplayRect.right-gDisplayRect.left, gDisplayRect.bottom-gDisplayRect.top, hDcMem, iHeight, 0, SRCCOPY) ); SelectObject(hDcMem, bmpOld); DeleteDC(hDcMem); EndPaint(hwnd, &paint);}//// Asks user which printer to use, then creates// background printing thread.//void PrintText(HWND hwndParent, char *pszText){ ThreadPrintInfo *pInfo; HANDLE hThread; DWORD dwThreadId; int result; DOCINFO docInfo; PRINTDLG dlgPrint; // Put up Common Dialog for Printing and get hDC. memset(&dlgPrint, 0, sizeof(PRINTDLG)); dlgPrint.lStructSize = sizeof(PRINTDLG); dlgPrint.hwndOwner = hwndParent; dlgPrint.Flags = PD_ALLPAGES | PD_USEDEVMODECOPIES | PD_NOPAGENUMS | PD_NOSELECTION | PD_RETURNDC; dlgPrint.hInstance = hInst; if (!PrintDlg(&dlgPrint)) return; // Initialize Printer device docInfo.cbSize = sizeof(DOCINFO); docInfo.lpszDocName = "Background Printing Example"; docInfo.lpszOutput = NULL; docInfo.lpszDatatype = NULL; docInfo.fwType = 0; result = StartDoc(dlgPrint.hDC, &docInfo); result = StartPage(dlgPrint.hDC); pInfo = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ThreadPrintInfo)); pInfo->hDlg = hwndParent; pInfo->hWndParent = hwndParent; pInfo->hDc = dlgPrint.hDC; pInfo->bPrint = TRUE; strcpy(pInfo->szText, pszText); MTVERIFY( hThread = CreateThread(NULL, 0, BackgroundPrintThread, (LPVOID)pInfo, 0, &dwThreadId ));// keep track of all background printing threads gPrintJobs[gNumPrinting++] = hThread; SendMessage(hwndParent, WM_THREADCOUNT, gNumPrinting, 0L);}//// Shows output on the dialog box.//void PrintToDisplay(HWND hwndParent, char *pszText){ ThreadPrintInfo *pInfo; DWORD dwThreadId; HANDLE hThread; pInfo = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(ThreadPrintInfo)); pInfo->hDlg = hwndParent; pInfo->hWndParent = GetDlgItem(hwndParent, IDC_OUTPUT);pInfo->hDc = GetDC(pInfo->hWndParent); pInfo->bPrint = FALSE; strcpy(pInfo->szText, pszText); MTVERIFY( hThread = CreateThread(NULL, 0, BackgroundPrintThread, (LPVOID)pInfo, 0, &dwThreadId ));// keep track of all background printing threads gPrintJobs[gNumPrinting++] = hThread; SendMessage(hwndParent, WM_THREADCOUNT, gNumPrinting, 0L);}//---------------------// About Box Handling//---------------------LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam){ switch (message) { case WM_COMMAND: if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { EndDialog(hDlg, TRUE); return (TRUE); } break; default: return (DefWindowProc(hDlg, message, wParam, lParam)); } return FALSE;}//---------------------// Background Printing Code//---------------------DWORD WINAPI BackgroundPrintThread(LPVOID pVoid){ ThreadPrintInfo *pInfo = (ThreadPrintInfo*) pVoid; RECT rect; RECT rectMem; HDC hDcMem; HBITMAP bmpMem; HBITMAP bmpOld; int x, y; int counter = 0; int nHeight; HFONT hFont; HFONT hFontOld; // Get dimensions of paper into rect rect.left = 0; rect.top = 0; rect.right = GetDeviceCaps(pInfo->hDc, HORZRES); rect.bottom = GetDeviceCaps(pInfo->hDc, VERTRES); nHeight = -MulDiv(36, GetDeviceCaps(pInfo->hDc, LOGPIXELSY), 72); // Create Font hFont = CreateFont(nHeight, 0, 0, 0, FW_DONTCARE, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, VARIABLE_PITCH, "Arial"); MTASSERT( hFont != 0); // Draw into memory device context hDcMem = CreateCompatibleDC(pInfo->hDc); hFontOld = SelectObject(hDcMem, hFont); iHeight = DrawText(hDcMem, pInfo->szText, -1, &rect, DT_LEFT | DT_NOPREFIX | DT_WORDBREAK | DT_CALCRECT); rectMem = rect; rectMem.left = rect.left + iHeight; rectMem.right = rect.right + (iHeight*2); bmpMem = CreateCompatibleBitmap(hDcMem, rectMem.right, rect.bottom); bmpOld = SelectObject(hDcMem, bmpMem); OffsetRect(&rect, iHeight, 0); DrawText(hDcMem, pInfo->szText, -1, &rect, DT_LEFT | DT_NOPREFIX | DT_WORDBREAK); // Italicize bitmap. We use GetPixel and // SetPixel because they are horribly inefficient, // thereby causing the thread to run for awhile. for (y = 0; y < iHeight; y++) { // Italicize line y for (x = rectMem.right; x > iHeight; x--) { // Move specified pixel to the right. COLORREF color; int offset; offset = y - iHeight; color = GetPixel(hDcMem, x + offset, y); if (color != 0) counter++; SetPixel(hDcMem, x, y, color); } // end for x } // end for y MTASSERT( counter > 0); // Copy bitmap of italicized text from memory to device if (pInfo->bPrint) { BitBlt(pInfo->hDc, 50, 50, rectMem.right-rect.left, rectMem.bottom-rect.top, hDcMem, iHeight, 0, SRCCOPY); } SelectObject(hDcMem, hFontOld); SelectObject(hDcMem, bmpOld); DeleteDC(hDcMem); if (!pInfo->bPrint) { // We can't just write to the global variable where the // bitmap is kept or we might overwrite the work of // another thread, thereby "losing" a bitmap // Also, if we used PostMessage instead of SendMessage, then // the rectangle could have been deleted (it's on the stack) // by the time the main message loop is reached. SendMessage(pInfo->hDlg, WM_SHOWBITMAP, (WPARAM)&rectMem, (LPARAM) bmpMem); } if (pInfo->bPrint) { // Finish printing int result; result = EndPage(pInfo->hDc); MTASSERT (result != SP_ERROR); result = EndDoc(pInfo->hDc); MTASSERT (result != SP_ERROR); DeleteDC(pInfo->hDc); // If we are printing, we are done with the bitmap. DeleteObject(bmpMem); } else { ReleaseDC(pInfo->hWndParent, pInfo->hDc); } // free data structure passed in. HeapFree(GetProcessHeap(), 0, pInfo); return 0;}在这个消息循环中必须注意的几点:
1.在收到WM_QUIT之后,windows仍然会传送消息给你。如果你要在收到WM_QUIT之后等待所有的线程结束,你必须处理你的
消息,否则窗口会变得反应迟钝,没有重绘能力。
2.MsgWaitForMultipleObjects()不允许数组中有缝隙产生。所以当某个handle被激发了时,应该在下一次调用MsgWaitForMultipleObjects()之前把handles数组做一个整理,紧压,不要只是把数组中的
handle设为NULL。
3.如果有另外一个线程更改了对象数组,而那时你正在等待的,那么你需要用一种办法,可以强迫MsgWaitForMultipleObjects()返回,并重新开始,以包含这个新的handle。上面采用WM_THREADCOUNT消
息解决。复制搜索复制搜索复制搜索复制搜索复制搜索复制搜索