zoukankan      html  css  js  c++  java
  • 第十一章 Windows线程池

    //1.Windows内置的线程池函数允许我们做如下事情:
    (A):以异步方式调用一个函数
    (B):每隔一段时间调用一个函数
    (C):当内核对象触发的时候调用一个函数
    (D):当异步I/O请求完成时调用一个函数
    当进程初始化的时候,他并没有任何与线程池有关的开销,但是一旦调用了新的线程池函数,系统就会为进程创建相应的内核资源,其中一些资源在进程终止之前都将一直存在
    务必注意线程池间的线程之间以及线程池线程与主调线程之间的线程同步问题
    
    //2.
    以异步方式调用函数:
    (A):
    BOOL WINAPI TrySubmitThreadpoolCallback
    (			
    	__in        PTP_SIMPLE_CALLBACK  pfns,	//回调函数
    	__inout_opt PVOID                pv,	//传递给回调函数的参数
    	__in_opt    PTP_CALLBACK_ENVIRON pcbe	//用于线程池定制,若使用默认线程池,可以传NULL
    );
    Requests that a thread pool worker thread call the specified callback function.
    If the function succeeds, it returns TRUE.If the function fails, it returns FALSE
    
    
    typedef VOID (NTAPI *PTP_SIMPLE_CALLBACK)(
    __inout     PTP_CALLBACK_INSTANCE Instance,	//用于回调函数的终止操作
    __inout_opt PVOID                 Context	//接收主调线程传递的参数
    );
    (B):某些情况下, TrySubmitThreadpoolCallback 函数会失败,比如内存不足等,其函数内部会创建一个工作项,若打算提交大量工作项,处于性能考虑,创建一个工作项一次,然后多次提交会更好
    (C):
    PTP_WORK WINAPI CreateThreadpoolWork		
    (
    	__in        PTP_WORK_CALLBACK    pfnwk,
    	__inout_opt PVOID                pv,
    	__in_opt    PTP_CALLBACK_ENVIRON pcbe
    );
    If the function succeeds, it returns a TP_WORK structure that defines the work object. Applications do not modify the members of this structure.
    If the function fails, it returns NULL
    
    
    typedef VOID (NTAPI *PTP_WORK_CALLBACK)
    (
    	__inout     PTP_CALLBACK_INSTANCE Instance,
    	__inout_opt PVOID                 Context,
    	__inout     PTP_WORK              Work
    );
    
    
    VOID WINAPI SubmitThreadpoolWork(__inout PTP_WORK pwk);
    Posts a work object to the thread pool. A worker thread calls the work object's callback function.
    
    
    VOID WINAPI WaitForThreadpoolWorkCallbacks(__inout PTP_WORK pwk, __in BOOL fCancelPendingCallbacks);
    fCancelPendingCallbacks:
    为 TRUE, 会试图取消先前提交的工作项,若此工作项正被处理则处理过程不会被打断
    为 FALSE,会将主调线程挂起,直到指定工作项已经处理完,而且线程池中处理该工作项的线程已被回收准备好处理下一个工作为止
    Waits for outstanding work callbacks to complete and optionally cancels pending callbacks that have not yet started to execute.
    若工作项尚未提交,那么此函数将立即返回而不执行任何操作
    
    
    CloseThreadpoolWork:用于关闭工作项
    (D):
    void __stdcall NamelessWork(PTP_CALLBACK_INSTANCE, void* pVoid)
    {
    	printf("%d
    ", *static_cast<int*>(pVoid));
    }
    
    void __stdcall NameWork(PTP_CALLBACK_INSTANCE, void* pVoid, PTP_WORK pWork)
    {
    	printf("%d
    ", *static_cast<int*>(pVoid));
    }
    
    int main()
    {
    	int nValue0 = 10;
    	auto nRe = TrySubmitThreadpoolCallback(NamelessWork, &nValue0, nullptr);
    
    	int nValue1 = 20;
    	auto pWork = CreateThreadpoolWork(NameWork, &nValue1, nullptr);
    	SubmitThreadpoolWork(pWork);
    	WaitForThreadpoolWorkCallbacks(pWork, TRUE);
    	SubmitThreadpoolWork(pWork);
    	SubmitThreadpoolWork(pWork);
    	WaitForThreadpoolWorkCallbacks(pWork, FALSE);
    	CloseThreadpoolWork(pWork);
    	//输出10 20 20
    
    	system("pause");
    }
    
    //3.
    每隔一段时间调用一个函数(类似的功能可以利用可等待的计时器内核对象完成,并且后者是在同一个线程内完成的):
    (A):
    PTP_TIMER WINAPI CreateThreadpoolTimer
    (
    	__in        PTP_TIMER_CALLBACK   pfnti,
    	__inout_opt PVOID                pv,
    	__in_opt    PTP_CALLBACK_ENVIRON pcbe
    );
    Creates a new timer object.
    If the function succeeds, it returns a TP_TIMER structure that defines the timer object. Applications do not modify the members of this structure.
    If the function fails, it returns NULL.
    
    
    typedef VOID (NTAPI *PTP_TIMER_CALLBACK)
    (
    	__inout     PTP_CALLBACK_INSTANCE Instance,
    	__inout_opt PVOID                 Context,
    	__inout     PTP_TIMER             Timer
    );
    
    
    VOID WINAPI SetThreadpoolTimer
    (
    	__inout  PTP_TIMER pti,
    	__in_opt PFILETIME pftDueTime,
    	__in     DWORD     msPeriod,
    	__in_opt DWORD     msWindowLength
    );
    Sets the timer object—, replacing the previous timer, if any. A worker thread calls the timer object's callback after the specified timeout expires.
    
    pftDueTime:
    A pointer to a FILETIME structure that specifies the absolute or relative time at which the timer should expire. If positive or zero, 
    it indicates the absolute time since January 1, 1601 (UTC), measured in 100 nanosecond units. If negative, it indicates the amount of time to wait relative to the current time. 
    
    msPeriod:
    The timer period, in milliseconds. If this parameter is zero, the timer is signaled once. If this parameter is greater than zero, the timer is periodic. 
    A periodic timer automatically reactivates each time the period elapses, until the timer is canceled.
    
    msWindowLength:
    The maximum amount of time the system can delay before calling the timer callback. If this parameter is set, the system can batch calls to conserve power.
    一般为NULL即可
    
    
    WaitForThreadpoolTimerCallbacks:与 WaitForThreadpoolWorkCallbacks 类似
    
    
    CloseThreadpoolTimer:CreateThreadpoolTimer 对应的关闭函数
    (B):
    void __stdcall Timer(PTP_CALLBACK_INSTANCE, void* pVoid, PTP_TIMER pTimer)
    {
    	printf("%d
    ", *static_cast<int*>(pVoid));
    }
    
    int main()
    {
    	int nValue2 = 30;
    	auto pTimer = CreateThreadpoolTimer(Timer, &nValue2, nullptr);
    	FILETIME FileTime = {-1};
    	SetThreadpoolTimer(pTimer, &FileTime, 100, NULL);
    	Sleep(1000);
    	WaitForThreadpoolTimerCallbacks(pTimer, FALSE);
    	CloseThreadpoolTimer(pTimer);
    
    	system("pause");
    	return 0;
    	//每100毫秒输出一个30,持续1秒
    }
    
    //4.
    在内核对象触发时调用一个函数
    (A):
    PTP_WAIT WINAPI CreateThreadpoolWait
    (
    	__in        PTP_WAIT_CALLBACK    pfnwa,			//The callback function to call when the wait completes or times out
    	__inout_opt PVOID                pv,
    	__in_opt    PTP_CALLBACK_ENVIRON pcbe
    );
    Creates a new wait object.
    If the function succeeds, it returns a TP_WAIT structure that defines the wait object. Applications do not modify the members of this structure.
    If the function fails, it returns NULL
    
    
    typedef VOID (NTAPI *PTP_WAIT_CALLBACK)
    (
    	__inout     PTP_CALLBACK_INSTANCE Instance,
    	__inout_opt PVOID                 Context,
    	__inout     PTP_WAIT              Wait,
    	__in        TP_WAIT_RESULT        WaitResult	//typedef DWORD TP_WAIT_RESULT;
    );
    WaitResult:回调函数被调用的原因:
    WAIT_OBJECT_0(0):传递给 SetThreadpoolWait 的内核对象在超时前被触发
    WAIT_TIMEOUT(258):传递给 SetThreadpoolWait 的内核对象在超时前被没有被触发
    WAIT_ABANDONED_0(128):传递给 SetThreadpoolWait 的内核对象是互斥量,且在被触发前被遗弃
    
    
    VOID WINAPI SetThreadpoolWait
    (
    	__inout  PTP_WAIT  pwa,
    	__in_opt HANDLE    h,					
    	__in_opt PFILETIME pftTimeout
    );
    Sets the wait object—replacing the previous wait object, if any. 
    A worker thread calls the wait object's callback function after the handle becomes signaled or after the specified timeout expires.
    
    h:
    A handle.
    If this parameter is NULL, the wait object will cease to queue new callbacks (but callbacks already queued will still occur).
    If this parameter is not NULL, it must refer to a valid waitable object.
    If this handle is closed while the wait is still pending, the function's behavior is undefined.
    If the wait is still pending and the handle must be closed, use CloseThreadpoolWait to cancel the wait and then close the handle.
    
    pftTimeout:
    A pointer to a FILETIME structure that specifies the absolute or relative time at which the wait operation should time out. 
    If this parameter points to a positive value, it indicates the absolute time since January 1, 1601 (UTC), in 100-nanosecond intervals. 
    If this parameter points to a negative value, it indicates the amount of time to wait relative to the current time.
    If this parameter points to 0, the wait times out immediately. If this parameter is NULL, the wait will not time out.
    
    A wait object can wait for only one handle. Setting the handle for a wait object replaces the previous handle, if any.
    You must re-register the event with the wait object before signaling it each time to trigger the wait callback.
    一旦线程池的一个线程调用了我们的回调函数,对应的等待项将进入不活跃状态,即使等待的内核对象被触发,也不会调用指定的回调函数,此时必须再次调用 SetThreadpoolWait 来重新注册
    
    
    WaitForThreadpoolWaitCallbacks:类似于 WaitForThreadpoolWorkCallbacks
    (B):
    void __stdcall Wait(PTP_CALLBACK_INSTANCE, void* pVoid, PTP_WAIT pWait, DWORD nWaitResult)
    {
    	printf("%d, %d
    ", *static_cast<int*>(pVoid), nWaitResult);
    }
    
    int main()
    {
    	int nValue3 = 40;
    	auto pWait = CreateThreadpoolWait(Wait, &nValue3, nullptr);
    	HANDLE hEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
    	FILETIME FileTime = {1};
    	SetThreadpoolWait(pWait, hEvent, &FileTime);
    	Sleep(1000);
    	SetThreadpoolWait(pWait, hEvent, nullptr);
    	SetEvent(hEvent);
    	WaitForThreadpoolWaitCallbacks(pWait, FALSE);
    	CloseHandle(hEvent);
    	CloseThreadpoolWait(pWait);
    	/*
    	程序立即输出
    	40, 258
    	然后继续输出
    	40, 0
    	*/
    
    	system("pause");
    	return 0;
    }
    
    //5.
    在异步I/O请求完成时调用一个函数
    (A):
    PTP_IO WINAPI CreateThreadpoolIo
    (
    	__in        HANDLE                fl,		//The file handle to bind to this I/O completion object.
    	__in        PTP_WIN32_IO_CALLBACK pfnio,
    	__inout_opt PVOID                 pv,
    	__in_opt    PTP_CALLBACK_ENVIRON  pcbe
    );
    Creates a new I/O completion object.
    If the function succeeds, it returns a TP_IO structure that defines the I/O object. Applications do not modify the members of this structure.
    If the function fails, it returns NULL
    
    
    VOID WINAPI StartThreadpoolIo(__inout PTP_IO pio);
    Notifies the thread pool that I/O operations may possibly begin for the specified I/O completion object.
    A worker thread calls the I/O completion object's callback function after the operation completes on the file handle bound to this object.
    
    You must call this function before initiating each asynchronous I/O operation on the file handle bound to the I/O completion object.
    Failure to do so will cause the thread pool to ignore an I/O operation when it completes and will cause memory corruption.
    If the I/O operation fails, call the CancelThreadpoolIo function to cancel this notification.
    If the file handle bound to the I/O completion object has the notification mode FILE_SKIP_COMPLETION_PORT_ON_SUCCESS and an asynchronous I/O operation returns 
    immediately with success, the object's I/O completion callback function is not called and threadpool I/O notifications must be canceled.
    
    
    VOID WINAPI CancelThreadpoolIo(__inout PTP_IO pio);
    Cancels the notification from the StartThreadpoolIo function.
    
    
    VOID WINAPI CloseThreadpoolIo(__inout PTP_IO pio);
    Releases the specified I/O completion object.
    
    
    WaitForThreadpoolIoCallbacks:类似于 WaitForThreadpoolWorkCallbacks
    (B):
    void __stdcall DealIo(PTP_CALLBACK_INSTANCE, void* pVoid, void* pOverLapped, unsigned long nRe, unsigned long nNums, PTP_IO)
    {
    	printf("%d, %d
    ", *static_cast<int*>(pVoid), nNums);
    }
    
    int main()
    {
    	FILE* fp = _fsopen("Test.dat", "wb", _SH_DENYWR);
    	if (fp)
    	{
    		char buff[10] = {1, 2, 3};
    		fwrite(buff, 1, 10, fp);
    		fclose(fp);
    	}
    
    	HANDLE hFile = CreateFile(TEXT("Test.dat"), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
    	if (INVALID_HANDLE_VALUE != hFile)
    	{
    		int nValue = 50;
    		auto pIo = CreateThreadpoolIo(hFile, DealIo, &nValue, nullptr);
    
    		char buff[10] = {};
    		OVERLAPPED OverLapped = {};
    		StartThreadpoolIo(pIo);
    		ReadFile(hFile, buff, 10, nullptr, &OverLapped);
    		WaitForThreadpoolIoCallbacks(pIo, FALSE);
    		//输出50, 10
    
    		memset(&OverLapped, 0, sizeof OVERLAPPED);
    		StartThreadpoolIo(pIo);
    		ReadFile(hFile, buff, 10, nullptr, &OverLapped);
    		CancelThreadpoolIo(pIo);
    		WaitForThreadpoolIoCallbacks(pIo, FALSE);
    		//并不进行输出
    
    		memset(&OverLapped, 0, sizeof OVERLAPPED);
    		StartThreadpoolIo(pIo);
    		ReadFile(hFile, buff, 10, nullptr, &OverLapped);
    		WaitForThreadpoolIoCallbacks(pIo, FALSE);
    		//输出50, 10
    
    		CloseThreadpoolIo(pIo);
    		CloseHandle(hFile);
    	}
    
    	system("pause");
    	return 0;
    }
    
    //6.
    回调函数的终止操作:
    (A):
    void LeaveCriticalSectionWhenCallbackReturns(__inout PTP_CALLBACK_INSTANCE pci, __inout PCRITICAL_SECTION pcs);	//回调函数返回时,线程池自动调用 LeaveCriticalSection
    void ReleaseMutexWhenCallbackReturns(__inout PTP_CALLBACK_INSTANCE pci, __in HANDLE mut);						//回调函数返回时,线程池自动调用 ReleaseMutex
    void ReleaseSemaphoreWhenCallbackReturns(__inout PTP_CALLBACK_INSTANCE pci, __in HANDLE sem, __in DWORD crel);	//回调函数返回时,线程池自动调用 ReleaseSemaphore
    void SetEventWhenCallbackReturns(__inout PTP_CALLBACK_INSTANCE pci, __in HANDLE evt);							//回调函数返回时,线程池自动调用 SetEvent
    void FreeLibraryWhenCallbackReturns(__inout PTP_CALLBACK_INSTANCE pci, __in HMODULE mod);						//回调函数返回时,线程池自动调用 FreeLibrary
    对任何一个回调函数的实例,线程池中的线程只会执行以上一种操作
    (B):
    void __stdcall NameWork(PTP_CALLBACK_INSTANCE pInstance, void* pVoid, PTP_WORK pWork)
    {
    	EnterCriticalSection(static_cast<CRITICAL_SECTION*>(pVoid));
    	printf("Bad World
    ");
    	LeaveCriticalSectionWhenCallbackReturns(pInstance, static_cast<CRITICAL_SECTION*>(pVoid));
    }
    
    int main()
    {
    	CRITICAL_SECTION cs;
    	InitializeCriticalSection(&cs);
    
    	auto pWork = CreateThreadpoolWork(NameWork, &cs, nullptr);
    	SubmitThreadpoolWork(pWork);
    	WaitForThreadpoolWorkCallbacks(pWork, FALSE);
    
    	EnterCriticalSection(&cs);
    	printf("Hello World
    ");
    	DeleteCriticalSection(&cs);
    
    	system("pause");
    	/*
    	先输出Bad World 然后输出Hello World
    	若回调函数中不使用 LeaveCriticalSectionWhenCallbackReturns(pInstance, static_cast<CRITICAL_SECTION*>(pVoid)); 
    	则主函数会阻塞在 EnterCriticalSection(&cs); 中
    	*/
    }
    (C):
    VOID WINAPI DisassociateCurrentThreadFromCallback(__inout PTP_CALLBACK_INSTANCE pci);
    回调函数通过此函数告诉线程池,逻辑自己已经完成了任务,这使得由于任何调用 WaitForThreadpoolWorkCallbacks 等系列函数而被挂起的线程得以返回
    
    //7.
    对线程池进行定制与得体的销毁线程池
    (A):CreateThreadpoolWork 系列函数允许我们传入一个 PTP_CALLBACK_ENVIRON 参数用于对线程池进行定制,若此参数为 NULL 则将使用默认的线程池
    	系统默认创建的线程池的线程个数最小为1,最大为500
    (B):自定义线程池
    PTP_POOL WINAPI CreateThreadpool(__reserved PVOID reserved); 创建一个新的线程池,其参数是保留的,应该传 NULL
    Allocates a new pool of threads to execute callbacks.
    If the function succeeds, it returns a TP_POOL structure representing the newly allocated thread pool. 
    Applications do not modify the members of this structure.
    If function fails, it returns NULL
    
    
    VOID WINAPI SetThreadpoolThreadMaximum(__inout PTP_POOL ptpp, __in DWORD cthrdMost);
    BOOL WINAPI SetThreadpoolThreadMinimum(__inout PTP_POOL ptpp, __in DWORD cthrdMic);
    设置自定义的线程池中的线程的最大和最小数目
    
    
    VOID WINAPI CloseThreadpool(__inout PTP_POOL ptpp);
    The thread pool is closed immediately if there are no outstanding work, I/O, timer, or wait objects that are bound to the pool; otherwise, 
    the thread pool is released asynchronously after the outstanding objects are freed.
    (C):回调环境相关API
    VOID InitializeThreadpoolEnvironment(__out PTP_CALLBACK_ENVIRON pcbe);
    Initializes a callback environment.
    
    
    VOID SetThreadpoolCallbackPool(__inout PTP_CALLBACK_ENVIRON pcbe, __in PTP_POOL ptpp);	
    Sets the thread pool to be used when generating callbacks.
    
    
    VOID DestroyThreadpoolEnvironment(__inout PTP_CALLBACK_ENVIRON pcbe);
    Deletes the specified callback environment. Call this function when the callback environment is no longer needed for creating new thread pool objects.
    (D):清理组相关API
    PTP_CLEANUP_GROUP WINAPI CreateThreadpoolCleanupGroup(VOID);
    Creates a cleanup group that applications can use to track one or more thread pool callbacks.
    If the function succeeds, it returns a TP_CLEANUP_GROUP structure of the newly allocated cleanup group. 
    Applications do not modify the members of this structure.
    If function fails, it returns NULL. 
    
    After creating the cleanup group, call SetThreadpoolCallbackCleanupGroup to associate the cleanup group with a callback environment.
    A member is added to the group each time you call one of the following functions:
    CreateThreadpoolIo
    CreateThreadpoolTimer
    CreateThreadpoolWait
    CreateThreadpoolWork
    You use one of the following corresponding close functions to remove a member from the group.
    CloseThreadpoolIo
    CloseThreadpoolTimer
    CloseThreadpoolWait
    CloseThreadpoolWork
    To close all the callbacks, call CloseThreadpoolCleanupGroupMembers.
    
    
    VOID SetThreadpoolCallbackCleanupGroup(__inout  PTP_CALLBACK_ENVIRON pcbe, __in PTP_CLEANUP_GROUP ptpcg, __in_opt PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng);
    Associates the specified cleanup group with the specified callback environment.
    
    pfng:
    The cleanup callback to be called if the cleanup group is canceled before the associated object is released. 
    The function is called when you call CloseThreadpoolCleanupGroupMembers.
    
    typedef VOID (NTAPI *PTP_CLEANUP_GROUP_CANCEL_CALLBACK)
    (
    __inout_opt PVOID ObjectContext,	CreateThreadpoolWork 系列函数传入的参数
    __inout_opt PVOID CleanupContext	CloseThreadpoolCleanupGroupMembers传入的参数
    );
    
    
    VOID WINAPI CloseThreadpoolCleanupGroupMembers
    (
    	__inout     PTP_CLEANUP_GROUP ptpcg,
    	__in        BOOL              fCancelPendingCallbacks,	//效果类似于 WaitForThreadpoolWorkCallbacks 的第二个参数
    	__inout_opt PVOID             pvCleanupContext			//传递给 SetThreadpoolCallbackCleanupGroup 回调函数的参数
    );
    Releases the members of the specified cleanup group, waits for all callback functions to complete, and optionally cancels any outstanding callback functions.
    The CloseThreadpoolCleanupGroupMembers function simplifies cleanup of thread pool callback objects by releasing, in a single operation, all work objects, 
    wait objects, and timer objects that are members of the cleanup group. An object becomes a member of a cleanup group when the object is created with 
    the threadpool callback environment that was specified when the cleanup group was created. 
    The CloseThreadpoolCleanupGroupMembers function blocks until all currently executing callback functions finish. 
    If fCancelPendingCallbacks is TRUE, outstanding callbacks are canceled; 
    otherwise, the function blocks until all outstanding callbacks also finish. After the CloseThreadpoolCleanupGroupMembers function returns, 
    an application should not use any object that was a member of the cleanup group at the time CloseThreadpoolCleanupGroupMembers was called. Also,
    an application should not release any of the objects individually by calling a function such as CloseThreadpoolWork, because the objects have already been released.
    
    
    VOID WINAPI CloseThreadpoolCleanupGroup(__inout PTP_CLEANUP_GROUP ptpcg);
    Closes the specified cleanup group.
    The cleanup group must have no members when you call this function
    (E):
    void __stdcall NameWork(PTP_CALLBACK_INSTANCE pInstance, void* pVoid, PTP_WORK pWork)
    {
    	printf("%d
    ", *static_cast<int*>(pVoid));
    }
    
    void __stdcall CleanUpCallBack(void* pVoid, void* pCleanUp)
    {
    	printf("Hello World, %d, %d
    ", *static_cast<int*>(pVoid), *static_cast<int*>(pCleanUp));
    }
    
    int main()
    {
    	auto pCleanUpGroup = CreateThreadpoolCleanupGroup(); 
    	auto pPool = CreateThreadpool(nullptr);
    
    	SetThreadpoolThreadMinimum(pPool, 5);
    	SetThreadpoolThreadMaximum(pPool, 5);
    
    	TP_CALLBACK_ENVIRON TEnviron;
    	InitializeThreadpoolEnvironment(&TEnviron);
    	SetThreadpoolCallbackPool(&TEnviron, pPool);
    	SetThreadpoolCallbackCleanupGroup(&TEnviron, pCleanUpGroup, CleanUpCallBack);
    
    	int nValue = 60;
    	auto pWork = CreateThreadpoolWork(NameWork, &nValue, &TEnviron);
    	SubmitThreadpoolWork(pWork);
    	WaitForThreadpoolWorkCallbacks(pWork, FALSE);						//输出60
    	SubmitThreadpoolWork(pWork);										//可能输出60 也可能不输出60
    
    	int nTem = 70;
    	CloseThreadpoolCleanupGroupMembers(pCleanUpGroup, TRUE, &nTem);		//输出Hello World, 60 70
    	CloseThreadpoolCleanupGroup(pCleanUpGroup);
    	DestroyThreadpoolEnvironment(&TEnviron);
    	CloseThreadpool(pPool);
    
    	system("pause");
    }
    

      

  • 相关阅读:
    Visual Studio 2017开发环境的安装
    git fetch拉取他人分支(转)
    highchart宽度自适应的问题-图表压缩到一起
    AngularJS官网打不开
    深入解析CSS样式层叠权重值(转)
    angularJS添加form验证:自定义验证
    angular ngRoute小例子
    bower学习(转)
    iframe的打怪1
    清除浮动的方法
  • 原文地址:https://www.cnblogs.com/szn409/p/8485906.html
Copyright © 2011-2022 走看看