Use multi-thread technology to realize communication between threads in VC

xiaoxiao2021-03-17  200

The current popular Windows operating system can run several programs at the same time (independently running program is also known as a process), for the same program, it can be divided into several independent executive flows, we call it thread, thread provides more The ability of task processing. Research software is the general purpose of today's general methods, process and threads in today's general, and important significance for improving the parallelity of software. The current large application software is not a multi-thread multi-task, and the single-threaded software is unimaginable. Therefore, mastered multi-thread multi-task design methods to each programmer must be mastered. This example is aimed at problems that are often encountered in applications in applications, such as communication, synchronization, etc., respectively, and use multithreading techniques to communicate between numbers, and realize simple sorting of numbers.

First, implementation method

1, understand thread

To explain the thread, you have to say the process, the process is an executive example of the application, each process consists of private virtual address space, code, data, and other system resources. The resources created at runtime died as the process terminates. The basic idea of ​​thread is very simple, it is a separate executive stream, a separate executive unit inside the process, which is equivalent to a subroutine, which corresponds to the CwinThread class object in Visual C . When a single executing program is run, a main thread that is default, the main thread appears in the form of a function address, providing the startup point of the program, such as the main () or WinMain () function, and the like. When the main thread is terminated, the process also terminates. According to actual needs, the application can break down into many independent execution threads, and each thread is run in parallel in the same process.

All threads in a process are in the virtual address space of the process, using the global variables and system resources of the process. The operating system assigns different CPU time films to each thread, at a time, the CPU only executes the thread in a time film, and the corresponding thread in multiple time filters is performed in the CPU, because each time the tablet time is very short. Therefore, for the user, it seems to be parallel in the computer in the computer. The operating system arranges the time of the CPU according to the priority of the thread, the priority thread is prioritized, and the priority low-priority thread continues to wait.

Threads are divided into two: user interface threads and working threads (also known as background threads). The user interface thread is usually used to handle the user's input and respond to various events and messages. In fact, the application's main execution thread CWINAPP object is a user interface thread, and automatically creates and starts automatically when the application starts, the same termination means At the end of the program, the process is terminated. Work threads are used to execute the background processing tasks, such as calculation, scheduling, and serial ports, etc., it and the user interface thread is not available from the CWINTHREAD class to create, the most important thing for it is how it is Implement the operating control function of the work thread task. Working thread and user interface thread starts to call different versions of the same function; finally need readers to understand that all threads in a process share their parent's variables, but at the same time each thread can have their own variables.

2, thread management and operation

(1) Start from thread

Create a user interface thread, first generate a derived class from class CWINTHREAD, and you must declare and implement this CwinThread derived class using declare_dyncreate and import_dyncReate. The second step is to overload some of the members of the school, such as: ExitInstance (), InitInstance (), OnIdle (), PretranslateMessage (). Last call AfxBeginThread () function is a version of: CWinThread * AfxBeginThread (CRuntimeClass * pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL) to start the user interface thread, where the first parameter Point to the defined user interface thread class pointer variable, the second parameter is the priority of the thread, the third parameter is the stack size corresponding to the thread, and the fourth parameter is an additional flag when the thread is created, and the default is normal. If the thread is started, the thread is started after CREATE_SUSPENDED. For the working thread, start a thread, first need to write a function that is hoped with the rest of the application in parallel, such as fun1 (), then defines a pointer variable pointing to the CWINTHREAD object * pthread, call AfxBeginthread (Fun1, Param, Priority The function, returns the value to the PTHREAD variable and start the thread to perform the above FUN1 () function, where fun1 is the name of the function to run, and is both the name of the control function mentioned above, Param is ready Any 32-bit value transmitted to the thread function FUN1, Priority is the priority of the thread that is a predefined constant, and the reader can refer to MSDN.

(2) Priority of thread

The following CWINTHREAD class member functions are used for thread priority:

INT getthreadPriority (); Bool setthradpriority () (int nPriority); The above two functions are used to obtain and set the priority of the thread, and the priority here is the priority level relative to the thread. Threads in the same priority level, high priority threads are first run; at different priority levels, who is high, who is high, who runs first. As for the constant required for priority settings, you can refer to MSDN, you have to pay attention to the priority of the thread, this thread must have truth_set_information access. For the setting of the priority level of the thread, the CWINTHREAD class does not provide the corresponding function, but can be implemented by the Win32 SDK function getPriorityClass () and setPriorityClass ().

(3) Thread hanging and recovery

The CWINTHREAD class contains a function of the application suspension and recovery it created, where suspendthread () is used to suspend the thread, suspend the execution of the thread; resumeThread () is used to resume the execution of the thread. If you execute SuspendThread () for a thread, you need to continuously perform the corresponding RESUMETHREAD () to restore the running of the thread. (4) Ending thread

There are three ways to terminate thread, and threads can call AFXendThread () to terminate itself inside; can call Bool TerminateThread (Handle Hthread, DWORD DWORD DWEXITCODE) outside the thread to force a thread run, and then call the closehandle () function. The stack occupied by the release thread; the third method is to change the global variable to return the execution function of the thread, and the thread is terminated. The following is a third method as an example, give some code:

// ctestView Message Handlers / Set To True To End ThreadBool Bend = False; // Defined global variable for controlling the run of thread; // the thread function; uint threadfunction (lpvoid pParam) // thread function {while (! bend) {Beep (100,100); Sleep (1000);} return 0;} / CwinThread * pThread; HWND hWnd; Void CtestView :: OninitialUpdate () {hWnd = GetSafeHwnd (); pThread = AfxBeginThread (ThradFunction, hWnd); / / Start thread pthread-> m_bautode = false; // thread to manually delete cview :: onInitialUpdate ();} void ctestview :: Ondestroy () {bend = true; // change the variable, thread end waitforsingleObject (pthread-> m_hthread, Infinite); // Waiting for the thread ends delete pthread; // Delete thread cView :: ONDESTROY ();} 3, communication between thread

Typically, a secondary thread wants to complete a particular type of task for the primary thread, which implies a channel indicating a communication between the main thread and the secondary thread. In general, there are several ways to implement this communication task: use global variables (the example in the previous section is actually used), using event objects, use messages. Here we mainly introduce the latter two methods.

(1) Communication with user-defined messages

In Windows programming, every thread of the application has its own message queue, and even the work thread is no exception, so that the message is made very simple to deliver information between the threads. First, users want to define a user message, as shown below: #Define WM_USERMSG WMUSER 100; In need, call :: PostMessage ((hwnd) Param, WM_USERMSG, 0, 0) or CWINTHREAD :: PostthradMessage The message is sent to another thread. The four parameters of the above functions are the handle of the destination window to be sent, the message flag, the parameter WPARAM and LParam of the message. The following code is a modification of the previous code, the modified result is a dialog box at the end of the thread, prompting the thread end: uint threadfunction (lpvoid pParam) {while (! "{Beep (100, 100); SLEEP (1000 );} :: PostMessage (hWnd, WM_USERMSG, 0,0); return 0;} WM_USERMSG message response function is OnThreadended (WPARAM wParam, LPARAM lParam) LONG CTestView :: OnThreadended (WPARAM wParam, LPARAM lParam) {AfxMessageBox ( " "); Retrun 0;} The above example is the server thread to send messages to the user interface, for worker threads, if its design mode is also a message driver, then the caller can send it to it, exit, Perform a specific process and other messages, let it complete in the background. You can use it directly in the control function :: getMessage () this SDK function to perform message snualing and processing, implement a message loop yourself. When the message queue that determines the thread, the thread is assigned to it to other threads. If the message queue is not empty, if the message queue is not empty, get this message, judgment The content of this message is processed accordingly.

(2) Implement communication with an event object

The method of transmitting signals between threads is complicated to communicate with an event object, represented by an object of the MFC's CEVENT class. Event objects are one of two states: there are signals and no signals, and threads can monitor events with signal status so as to perform operations on events when appropriate. The above example code is modified as follows:

Cevent threadStart, threadEnd; UINT ThreadFunction (LPVOID pParam) {:: WaitForSingleObject (threadStart.m_hObject, INFINITE); AfxMessageBox ( "Thread start."); While (bend!) {Beep (100,100); Sleep (1000); Int result =: WaitForsingleObject (threadend.m_hobject, 0); // Waiting for the Thieadend event has a signal, no signal When the thread is here hovering if (Result == Wait_Object_0) bend = true;}: postMessage (hwnd, wm_usermsg, 0, 0); return 0;} / Void CtestView :: OninitialUpdate () {hWnd = GetSafeHwnd (); threadStart.SetEvent (); // threadStart signal events pThread = AfxBeginThread (ThreadFunction, hWnd); // start threads pThread-> m_bAutoDelete = FALSE; Cview :: OnInitialUpdate ();} Void CtestView :: OnDestroy () {threadEnd.SetEvent (); WaitForSingleObject (pThread-> m_hThread, INFINITE); delete pThread; Cview :: OnDestroy ();} run this program When the program is closed, the prompt box is displayed to display "Thread end".

4, in front of the thread, before we talked, each thread can access public variables in the process, so the problem that needs attention during multiple threads is how to prevent two or more threads from accessing the same data at the same time. So as not to destroy data integrity. Ensure that each thread can be suitably coordinated as synchronization between threads. The event object introduced in the previous section is actually a synchronization form. Using synchronous classes in Visual C to solve data unsafe problems caused by the operational system, the seven multi-threaded synchronization classes supported by the MFC can be divided into two categories: synchronous objects (CsyncObject, CSemaphore, CMutex, ccriticalsection and cevent ) And synchronize access objects (CMULTILOCK and CSILOCK). This section mainly introduces critical section, mutexe, semaphore, which makes each thread coordinated work, and the program is safer. (1) The critical area of ​​the critical area is to ensure that only one thread can access data at a certain time. In the process of using it, you need to provide a shared critical area object to each thread. No matter which thread occupies the critical area object, you can access the protected data. At this time, the other thread needs to wait until the thread releases the critical area object. After the critical area is released, the additional thread can strongly account for this critical area to access shared data. The critical area corresponds to a ccriticalSECTION object. When the thread needs to access the protection data, the Lock () member function of the critical area object is invoked; when the operation of the protection data is completed, the Unlock () member function of the critical area object is released to the critical area. The ownership of the object so that another thread can capture critical area objects and access protected data. At the same time, start two threads, and their corresponding functions are WriteThread () and readthread () to operate on public array array [], the following code illustrates how to use critical area objects: #include "afxmt.h" int "INT Array [10], Destarray [10]; ccriticalsection section; uint WriteThread (LPVOID param) {section.lock (); for (int x = 0; x <10; x ) array [x] = x; sectionunlock ); Uint readthread (lpvoid param) {section.lock (); for (int x = 0; x <10; x ) destarray [x] = array [x]; sectionUnlock ();} The above code is running The result should be that the elements in the Destarray array are 1-9, rather than messy numbers. If you don't use synchronization, it is not this result, and interested readers can experiment. (2) Mutual exclusion is similar to the critical region, but relatively complex during use, it can not only achieve synchronization between the same application thread, but also achieve synchronization between different processes, thereby achieving resource security sharing. Mutual exclusion corresponds to the CMUTEX class, when using a mutex, you must create a CSILOCK or CMULTILOCK object for actual access control because the examples here only handle a single mutual exclusion, so we can use CSILOCK objects, this object The LOCK () function is used to occupy mutual exclusion, and unlock () is used to release mutual exclusion.

The implementation code is as follows: #include "afxmt.h" int Array [10], Destarray [10]; cmutex section; uint Writethread (lpvoid param) {csinglelock singlerock; singleelock (& ​​section); Singlock.lock (); for (int X); = 0; x <10; x ) array [x] = x; singlerock.unlock ();} uint readthread (lpvoid param) {csinglelock singlerock; singleelock (& ​​section); singleLock.lock (); for (int x = 0 ; x <10; x ) destarray [x] = array [x]; SingLelock.unlock ();} (3) The usage of signal quantity semaphore is very similar, and it can be allowed to allow multiple times. A thread access to the same resource, creating a semaphore requires an object with a CSEMaphore class. Once a semaphore object is created, you can use it to use it to access the resources. To implement the counting process, create a CSILOCK or CMLTILock object, then use the LOCK () function of the object to reduce the count value of this session, unlock () is reversed. The following code initiates three threads, executes the two message boxes, and then the third message box after 10 seconds is displayed. / Csemaphore * semaphore; Semaphore = new Csemaphore (2,2); HWND hWnd = GetSafeHwnd (); AfxBeginThread (threadProc1, hWnd); AfxBeginThread (threadProc2, hWnd); AfxBeginThread (threadProc3, hWnd); UINT ThreadProc1 (LPVOID param) { CSILOCK SINGELLOCK (SINGLOCK.LOCK (); Sleep (10000); :: MessageBox ((hwnd) Param, "Thread1 Had Access", "Thread1", MB_OK; RETURN 0;} uint threadproc2 (lpvoid param) { CSILOCK SINGELLOCK (SINGLOCK.LOCK (); SLEEP (10000); :: MessageBox ((hwnd) Param, "Thread2 Had Access", "Thread2", MB_OK; Return 0;} uint threadproc3 (lpvoid param) { CSILOCK SINGELLOCK (SINGLOCK.LOCK (); SLEEP (10000); :: MessageBox ((hwnd) Param, "Thread3 Had Access", "Thread3", MB_OK; Return 0;} 2, Programming Step 1, start Visual C 6.0 generates a 32-bit console program, named "Sequence" 2, enters the number to serve, declare four sub-threads; 3, enter code, compile the running program.

Third, the program code //// sequence.cpp: defines the entry point for the console application./* Mainly used WinAPI thread control functions, please check the MSDN in detail; thread creation function: Handle CreateThread (LPSecurity_attributes LPthReadattributes, / / Safety attribute structure pointer, can be null; dWord dwstacksize, // thread stack size, if 0 means using the default value; lpthread_start_routine lpstartaddress, // Pointer to thread function; LPVOID LPPARAMETER, // Pass the parameters of the thread function, You can save a pointer value; DWORD DWCREATIONFLAGS, // thread establishment is initial tag, run or hang; LPDWORD LPTHREADID / / Point DWORD variables for receiving the thread number;); multi-threaded signal function controlled by critical resource: Handle CreateEvent (LPSecurity_attributes lpeventattributes, // security attribute structure pointer, can be null; Bool BmanualReset, / / ​​manual clear signal tag, True must manually // call RetEvent clear signal after WaitforsingleObject. If false is WaitforsingleObject //, after the system Automatically clear the event signal; Bool BinitialState, / / ​​Initial state, True has signal, FALSE no signal; the name of the LPCTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTSTED NO] Failure, if you encounter // to the same name of the same number, you should pay attention to changes;); Handle CreateMutex (LPSecurity_Attributes LPMUTEXATTRIBUTES, / / ​​Security Properties Pointer, can be null Bool Binitialowner, // Currently establishing mutual exclusion True Indicates that the occupation, // does not access this mutex, and cannot enter the critical area of ​​// of the mutual amount of control. False means that the number of the mutual amount LPCTSTR LPNAME // selection is not occupied, the number of characters Not more than max _PATH If // The other semolus function of the same name will fail, // If you encounter similar signals, you must pay attention to changes; // Initialize the critical area signal, you must initialize void initializecriticalsection (lpcritical_section lpcriticalsection //// Crown region variable pointer); // Blocking function // If the waiting signal is not available, the thread will hang until the signal can be used // thread to be awakened, the function will automatically modify the signal, such as Event, thread After waking up, // Event signal will become no signal, Mutex, Semaphore, etc. also change.

DWORD WAITFORSINGLEOBJECT (Handle Hhandle, // Waiting for the Object Handle DWMILLILLISECONDS // Waiting for milliseconds, infinite means unlimited waiting); // If you want to wait multiple signals to use WaitFormutiPleObject function * / # include "stdafx.h" #include " STDLIB.H "#include" memory.h "Handle evtterminate; // event signal, the tag is over / * The following three control methods are used below, you can comment two of them, use one. Pay attention to modifying the corresponding control statement * / handle evtprint; // event signal in the critical area printResult, marking the event, whether the tag event has occurred // critical_section csprint; // critical area // handle mtxprint; // mutually exclusive signal, such as Signal indicates that there is already a thread to enter the critical area and have this signal static long threadcompleted = 0; / * Used to mark the number of threads in the four subclones The addition and subtraction-oriented data is used to transmit the sorted data to each sorting sub-thread struct mysafeaRray {long * data; int ostth;}; / Print each thread sort result Void PrintResult (long * array, int = "sort"); // Sort Function Unsigned long __stdcall bubblesort (void * THEARRAY); // Bubbling Unsigned long __stdcall SelectSort (Void * THEARRAY); // Select Sort Unsigned Long __stdcall Heapsort (VOID * THEARRAY); // Heap Sort Unsigned Long __stdcall InsertSort (VOID * THEARRAY); // Insert Sort / * The declaration of the above four functions must be suitable as The necessary conditions for a thread function can use CreateThread to create a thread. (1) The call method must be __stdcall, that is, the function parameter stack order is from right to left, and the recovery of the stack by the function itself itself is __cdecl, so the explicit statement is __stdcall (2) Return The value must be a unsigned long (3) parameter must be a 32-bit value, such as a pointer value or long type (4) If the function is a class member function, you must declare a Static function, and the function pointer is a special way of writing when CreateThread. As follows (function is a member class CThreadTest's): static unsigned long _stdcall MyThreadFun (void * pParam); handleRet = CreateThread (NULL, 0, & CThreadTestDlg :: MyThreadFun, NULL, 0, & ThreadID); reason for declared as static is Because this function must be used independently of the object instance, even if there is no declaration example.

* / int Quicksort (Long * Array, Int iLow, Int ihigh); // Quick Sorting Int Main (int Argc, char * argv []) {long data [] = {123, 34, 546, 754, 34, 74, 3, 56 }; Int idatalen = 8; // In order to sort and save the raw data separately // / / / / Data5; MysafeArray structdata1, structdata2, structdata3, structdata4; data1 = new long [idata]; memcpy (data1, data, idata << 2); // copy data in DATA to DATA1 // Memory Copy Memcpy (Target Memory) The pointer, source memory pointer, replication byte), because the length of the LONG is 4 bytes, so the number of bytes replicated is IDATALEN << 2, that is, Idata1.ilength; StructData1.ilength = Idatalen; Data2 = new long [idata]; memcpy (data2, data, idata << 2); structdata2.data = data2; structdata2.ileth = idatalen; data3 = new long [iDataLen]; Memcpy (Data3, Data, iDatalen < <2); structdata3.data = data3; structdata3.ilength = iDataLen; data4 = new long [iDataLen]; Memcpy (data4, data, idata << 2); structdata4.Data = data4; structdata4.ilength = iDataLen; data5 = New long [idatalen]; memcpy (data5, data, idatalen << 2); unsigned long tid1, ti D2, TID3, TID4; // Initialize emtterminate = CreateEvent (NULL, FALSE, FALSE, "TERMINATE"); evtprint = createevent (NULL, FALSE, TRUE, "PrintResult"); // Establish each sub-thread CreateThread (NULL, 0, & BubbleSort, & StructData1, NULL, & TID1); CreateThread (NULL, 0, & SelectSort, & StructData2, NULL, & TID2); CreateThread (NULL, 0, & HeapSort, & StructData3, NULL, & TID3); CreateThread (NULL, 0 & INSERTSORT, & STRUCTDATA4, NULL, & TID4); // Perform line rapid sorting in the main thread, other sorting in sub-threads (Data5, 0, Idatalen - 1); PrintResult (Data5, Idatalen, "Quick Sort") WaitforsingleObject (esvtterminate, infinite);

// Waiting for all sub-threads End // After all sub-threads are over, the main thread can end the delete [] Data1; Delete [] Data2; Delete [] Data3; Delete [] Data4; CloseHandle (EVTPRINT); RETURN 0; } / * Bubbling sorting ideology (ascending, descending, the same is as an ascended algorithm): Two two two comparisons are exchanged from the head until tail, and the small placement is large. This time, the biggest element will be exchanged, then the next loop does not have to be exchanged for the last element, so every time the exchange rate is less than the last cycle, this N times After the data becomes ascended, * / unsigned long __stdcall bubblesort (void * THEARRAY) {long * array = ((mysafeaRray *) THEARRAY) -> data; int = ((mysafearray *) THEARRAY) -> iles; int; int tent i, j = 0; long swap; for (i = ilength-1; i> 0; I -) {for (j = 0; j array [j 1]) / / Reward before, exchange {swap = array [j]; array [j] = array [j 1]; array [j 1] = swap;}} }printresult (array, ionge, " Bubble sort "); // Print Sampling Results to Console InterlockedInCrement (& ThreadCompleted); // Returns the thread completion number tag plus 1 if (threadCompleted == 4) STEVENT (EVTTERMINATE); / / Check if other threads have been executed End // If you implement the completion setup program end signal amount Return 0;} / * Select Sort Thought: Every time you find the smallest element from the disorderd data, then and the next one of the previously orderly elements Elements are exchanged, so that the entire source sequence is divided into two parts, the front portion is a sequenceful sequence that has already been sequenced, and the rear portion is disordered for selecting the smallest element. After the cycle N times, the front ordered sequence is lengthened as long as the source sequence, and the length of the rear of the rear is changed to 0, and the sort is completed.

* / Unsigned long __stdcall SelectSort (void * theArray) {long * Array = ((MySafeArray *) theArray) -> data; int iLength = ((MySafeArray *) theArray) -> iLength; long lMin, lSwap; int i, j , iMinpos; for (i = 0; i

* / unsigned long __stdcall Heapsort (Void * THEARRAY) {long * array = ((mysafeaRray *) THEARRAY) -> Data; int = ((mysafearray *) THEARRAY) -> inglen; int I, j, p; long swap ; For (i = 0; i {for (j = Ilength - 1; j> i; j -) // from the last reciprocal to compare the word node and parent node {P = (J - i - 1) / 2 i; // Calculate the parent node array subscript // Notes that the tree node order is not equivalent to the array, because the number of elements of the buildlis is decremented by IF (Array [J] Data; int = ((mysafeaRray *) THEARRAY) -> ilength; int = 1, j = 0; long temp; For (i = 1; i {temp = array [i]; // Remove the first element value for the sequence behind the sequence (j = i; j> 0; j -) // and the front The order data is compared one by one to find the appropriate insertion position {IF (Array [J - 1]> Temp) // If the element is broken by the insert value, Array [J] = Array [J - 1]; ELSE // If the element is smaller than the insert value, the latter position of this position is the position of the insertion element Break;} Array [J] = Temp;} PrintResult (Array, Ilength, "INSERT SORT"); // Print Sort by the console Result InterlockedInCrement; // Returns the thread completion number tag plus 1 if (threadCompleted == 4) STEVENT (EVTTERMINATE); / / Check if other threads have been executed // If all implement the full setup program end signal RETURN 0;} / * Rapid Sort Thought: Rapid Sort is an application of the grahe idea, which first select a fulcrum, then exchange the element smaller than the fulcrum to the front of the fulcrum, exchange the element larger than the fulcrum to the right side of the fulcrum . Then, the left part of the fulcrum and the right side are then processed, and the data will become orderable after several times.

The following implementation uses recursion to establish two cursors: ilow, ihigh; iLow pointing the first element of the sequence, iHigh points to the last first element as the fulcrum, and store its value in an auxiliary variable. Then the first position is empty and other elements can be placed. This begins to move the cursor forward from IHIGH, IHIGH looks for a small element, if found, put it into the vacant location (now the first location), then the IHIGH cursor stops moving, then Ihigh The point to which is vacant, then moving the ilow cursor looks to the vacant location than the fulcrum to the vacant position, so that it is adjacent to ILOW equal to IHIGH. Finally, use recursive to the left and right part of the same handle * / int □ {if (ilow> = ihigh) Return 1; // Recursive end condition long pivot = array [ilow]; int iLowsaved = iLow, IHIGHSAVED = IHIGH; / / ILOW, IHIGH value saves while (ilow = pivot && high> iLow) // Looking for a large element of the fuller element iHigh -; array [ilow] = array [ihigh]; // put the found element to the vacant location while (array [iow]

转载请注明原文地址:https://www.9cbs.com/read-129567.html

New Post(0)