多线程与同步对象
C/C++教程 作者:高亚清
多线程与同步对象
多线程与同步对象 ................................................................................................................................................1 0. Win32及C运行时多线程 ...............................................................................................................................2
0.1. 线程的创建 ............................................................................................................................................2
0.2. 线程的终止 ............................................................................................................................................3
0.3. 线程的暂停(挂起)与恢复运行 ..............................................................................................................6
0.4. 线程的优先级 ........................................................................................................................................7
0.5. C运行时的线程创建 ..............................................................................................................................8
1. 多线程的同步 ..................................................................................................................................................9
1.1. 原子访问,互锁函数...........................................................................................................................10
1.2. 临界区 ..................................................................................................................................................11
1.3. 内核对象 ..............................................................................................................................................12
1.4. 线程的死锁 ..........................................................................................................................................18 2. MFC多线程 ....................................................................................................................................................18
1
C/C++教程 作者:高亚清
0. Win32及C运行时多线程
0.1. 线程的创建
在程序中调用CreateThread函数可以创建一个线程:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
每个参数的含义为:
LPSECURITY_ATTRIBUTES lpThreadAttributes。该参数是指向一个SECURITY_ATTRIBUTES结构的指针。如果要赋予该线程内核对象缺省的安全属性,可以传递一个NULL。如果希望所有的子进程能够继承该线程对象的句柄,须设定一个SECURITY_ATTRIBUTES结构,它的bInheritHandle成员应初始化为TRUE。
DWORD dwStackSize。该参数指定线程栈的大小。每个线程都拥有它自己的堆栈。如果dwStakSize为0,系统默认保留的栈的空间为1MB。
LPTHREAD_START_ROUTINE lpStartAddress。该参数用于指定新建线程的入口函数的地址。可以创建多个线程,使用相同的入口函数地址。该入口函数的原型为:
DWORD WINAPI ThreadFunc(LPVOID lpParameter); 其中的lpParameter的值为创建线程时,CreateThread函数的第4个参数的值 。
LPVOID lpParameter。参见3、由CreateThread传给入口线程入口函数的参数。
DWORD dwCreationFlags。该参数控制创建线程的标志,它可以是0或者CREATE_SUSPENDED。如果是0,则新建立的线程在创建完毕后被系统调度程序调度(可能被执行,也可能不被执行,这取决于系统中其他线程的优先级情况)。如果该值为CREATE_SUSPENDED,系统在创建完新线程后,新线程被系统挂起,直到有其他线程执行了带该新线程句柄的ResumeThread()函数后,新线程才被激活。
LPDWORD lpThreadId。该参数指定新线程的ID。在Windows95中,该参数不能为NULL,否则会引起错误,在Windows 2000中该参数可以为NULL。
例1:
创建线程,并传递参数:
DWORD WINAPI SubThread(LPVOID lpParam)
{
TRACE("SubThread,lpParam is:%d\n",lpParam);
2
C/C++教程 作者:高亚清
return 0;
}
void MainThread()
{
HANDLE hThread=CreateThread(NULL,0,SubThread,(LPVOID)123,0,NULL);
Sleep(1000);
CloseHandle(hThread);
}
例2:
创建挂起的新线程,老线程执行一些工作后,再激活新创建的线程。
DWORD WINAPI SubThread(LPVOID lpParam) {
TRACE("SubThread,lpParam is:%d\n",lpParam);
return 0;
}
void MainThread()
{
HANDLE hThread=CreateThread(NULL,0,SubThread,(LPVOID)123,CREATE_SUSPENDED,NULL);
TRACE("SubThread is created\n");
ResumeThread(hThread);
Sleep(1000);
CloseHandle(hThread);
}
0.2. 线程的终止
可以使用下面方法来终止线程:
, 线程函数的返回;
, 通过调用ExitThread函数,线程将自己撤销;
, 同一个进程或者另一个进程中的线程调用TerminateThread函数终止另外一个线程的运行; , 包含线程的进程终止运行(主线程退出)。
1、 线程函数返回。
这是确保线程的所有资源被正确地清除的唯一办法。当线程函数返回时,如下情况将会发生:
, 在线程函数中创建的所有C++对象将通过它们的析构函数正确地撤销;
, 操作系统将正确地释放线程的堆栈使用的内存;
, 系统将线程的退出代码设置为线程函数的返回值;
, 系统递减线程内核对象的引用计数。
2、 ExitThread函数。
可以通过在线程中调用ExitThread函数,来强制终止自身线程的运行。原型为:
VOID ExitThread(DWORD dwExitCode);
3
C/C++教程 作者:高亚清
该函数将终止自身线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,C++资源(如C++
对象)将不被正确地撤销。由于这个原因,最好从线程函数返回,而不是通过调用ExitThread来返回。 3、 TerminateThread函数。调用TerminateThread函数将终止指定线程的运行,原型为:
BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);
//HANDLE hThread—将要终止的线程的句柄
//DWORD dwExitCode—传递给将要终止的线程的退出代码
与ExitThread不同,ExitThread是撤销自身线程,而TerminateThread能够撤销任何线程。
要注意的是,TerminateThread是异步运行的函数,也就是说,它告诉系统要终止指定线程的运行,但是该函
数返回后,并不能保证指定的线程已经撤销,如果要确切地知道被指定的线程是否已经被撤销,请调用
WaitForSingleObject等函数。
4、 进程终止运行(主线程退出)
5、 线程终止运行时,会发生下列操作:
, 线程拥有的所有USER对象句柄均被释放。在Windows中,线程所创建的大部分对象归它的进程所有。但
是,线程也可以拥有两个USER对象:窗口和钩子。当创建这些对象的线程终止运行时,系统会自动释放
这些对象。其它对对象只有在进程终止时才被释放;
, 线程的退出代码从STILL_ACTIVE改为线程函数返回值或者传递给ExitThread或者TerminateThread的代
码;
, 线程内核对象的状态变为通知(信号)状态;
, 如果线程是进程中最后一个活动线程,进程也被终止;
, 线程内核对象的使用技术递减1。
例1:
线程的正常退出。注意~在线程中new出来的资源并不能随着线程退出而自动释放~~~
class CTest
{
private:
int m_iId;
public:
CTest(int iId)
{
m_iId=iId;
}
virtual ~CTest()
{
TRACE("ID:%d ~CTest()\n",m_iId);
}
};
DWORD WINAPI SubThread(LPVOID lpParam) {
CTest obj(1),*pObj;
pObj=new CTest(2); //注意,堆分配的资源并不随着线程的退出而自动释放
Sleep(500);
return 999;
}
4
C/C++教程 作者:高亚清
void MainThread()
{
DWORD dwRet;
HANDLE hThread=CreateThread(NULL,0,SubThread,NULL,0,NULL);
GetExitCodeThread(hThread,&dwRet);
TRACE("SubThread exitcode:%d\n",dwRet);//很可能为STILL_ACTIVE
Sleep(1000);
GetExitCodeThread(hThread,&dwRet);
TRACE("SubThread exitcode:%d\n",dwRet);//很可能为999
CloseHandle(hThread);
}
例2:
自身调用ExitThread退出线程
class CTest
{
private:
int m_iId;
public:
CTest(int iId)
{
m_iId=iId;
}
virtual ~CTest()
{
TRACE("ID:%d ~CTest()\n",m_iId);
}
};
DWORD WINAPI SubThread(LPVOID lpParam) {
CTest obj(1),*pObj;
pObj=new CTest(2); //注意,堆分配的资源并不随着线程的退出而自动释放
Sleep(500);
ExitThread(1000); //接下来的返回语句将得不到执行,obj也不会被析构
return 999;
}
void MainThread()
{
DWORD dwRet;
HANDLE hThread=CreateThread(NULL,0,SubThread,NULL,0,NULL);
GetExitCodeThread(hThread,&dwRet);
TRACE("SubThread exitcode:%d\n",dwRet);//很可能为STILL_ACTIVE
Sleep(1000);
5
C/C++教程 作者:高亚清
GetExitCodeThread(hThread,&dwRet);
TRACE("SubThread exitcode:%d\n",dwRet);//很可能为1000
CloseHandle(hThread);
}
例3:
调用TerminateThread来终止另外一个线程的运行。
DWORD WINAPI SubThread(LPVOID lpParam) {
CTest obj(1),*pObj;
pObj=new CTest(2); //注意,堆分配的资源并不随着线程的退出而自动释放
Sleep(500);
return 999;
}
void MainThread()
{
DWORD dwRet;
HANDLE hThread=CreateThread(NULL,0,SubThread,NULL,0,NULL);
GetExitCodeThread(hThread,&dwRet);
TRACE("SubThread exitcode:%d\n",dwRet); //很可能为STILL_ACTIVE
TerminateThread(hThread,1001); //强行终止线程hThread的运行
//TerminateThread是异步函数,所以可能此时hThread并没撤销完毕。
Sleep(500);
GetExitCodeThread(hThread,&dwRet);
TRACE("SubThread exitcode:%d\n",dwRet); //很可能为1001
CloseHandle(hThread);
}
0.3. 线程的暂停(挂起)与恢复运行
任何线程都可以调用SuspendThread来暂停另一个线程的运行(只要拥有线程的句柄)。原型为: DWORD SuspendThread(HANDLE hThread); 返回值是前一次暂停计数,一个线程能够被暂停的最多次数是MAXIMUM_SUSPEND_COUNT,
参数HANDLE hThread表示将要被挂起的线程
调用ResumeThread可以让挂起的线程恢复运行。原型为:
DWORD ResumeThread(HANDLE hThread); 返回值是前一次暂停计数,参数HANDLE hThread表示将要被恢复的线程
例:
HANDLE g_hThread=NULL;
DWORD WINAPI SubThread(LPVOID lpParam) {
6
C/C++教程 作者:高亚清
int iLoop=0;
while(TRUE)
{
TRACE("SubThread loop:%d\n",iLoop++);
Sleep(1000);
}
return 0;
}
void CMy0621Dlg::OnOK()
{
g_hThread=CreateThread(NULL,0,SubThread,NULL,0,NULL);
}
void CMy0621Dlg::OnButtonSuspend() {
DWORD dwCount=SuspendThread(g_hThread);
TRACE("Suspend count:%d\n",dwCount); }
void CMy0621Dlg::OnButtonResume() {
DWORD dwCount=ResumeThread(g_hThread);
TRACE("Suspend count:%d\n",dwCount); }
0.4. 线程的优先级
HANDLE g_hThread1=NULL;
HANDLE g_hThread2=NULL;
void ConsumeCPU()
{
char szBuf[8192];
for(int i=0;i<200000;i++)
{
sprintf(szBuf,"%d",rand());
memset(szBuf,0,sizeof(szBuf));
}
}
DWORD WINAPI SubThread(LPVOID lpParam) {
int iLoop=0;
7
C/C++教程 作者:高亚清
while(TRUE)
{
ConsumeCPU();
TRACE("Thread %X loop:%d\n",GetCurrentThreadId(),iLoop++);
}
return 0;
}
void CMy0621Dlg::OnOK()
{
g_hThread1=CreateThread(NULL,0,SubThread,NULL,0,NULL);
g_hThread2=CreateThread(NULL,0,SubThread,NULL,0,NULL);
SetThreadPriority(g_hThread1,THREAD_PRIORITY_LOWEST);
SetThreadPriority(g_hThread2,THREAD_PRIORITY_LOWEST); }
void CMy0621Dlg::OnButtonReduce()
{
SetThreadPriority(g_hThread1,THREAD_PRIORITY_IDLE); }
void CMy0621Dlg::OnButtonRestore()
{
SetThreadPriority(g_hThread1,THREAD_PRIORITY_LOWEST); }
0.5. C运行时的线程创建
C运行时是在Windows操作系统尚未面世时就已经存在的一套C语言的函数库,因为当时并未考虑到多线程的情况,所以在Windows操作系统下用CreateThread创建的线程中调用了某些C运行时函数,如asctime等,则有可能出现问
题
快递公司问题件快递公司问题件货款处理关于圆的周长面积重点题型关于解方程组的题及答案关于南海问题
,为此后来特地增加了CreateThread的C运行时版本 _beginthreadex,原型为:
unsigned int _beginthreadex(
void *security,
unsigned stack_size,
unsigned ( __stdcall *start_address )( void * ),
void *arglist,
unsigned initflag,
unsigned *thrdaddr);
相应的线程入口点函数也要变为:
UINT WINAPI SubThread(LPVOID lpParameter);
例:
UINT WINAPI SubThread(LPVOID lpParameter)
{
8
C/C++教程 作者:高亚清
TRACE("lpParameter is:%d\n",lpParameter);
return 999;
}
void MainThread()
{
DWORD dwRet;
HANDLE hThread=(HANDLE)_beginthreadex(NULL,0,SubThread,(LPVOID)123,0,NULL);
Sleep(1000);
GetExitCodeThread(hThread,&dwRet);
TRACE("ExitCode is:%d\n",dwRet);
CloseHandle(hThread);
}
1. 多线程的同步
当多个线程无限制的在同一段时间内访问同一资源时,有可能导致错误的结果的发生,例: long g_iNum1,g_iNum2;
DWORD WINAPI SubThread(LPVOID lpParam)
{
for(int i=0; i<100000000; i++)
{
g_iNum1++;
g_iNum2=g_iNum2+2;
}
return 0;
}
void Test()
{
HANDLE hThreads[2];
g_iNum1=0;
g_iNum2=0;
hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL);
hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL);
SetThreadPriority(hThreads[0],THREAD_PRIORITY_LOWEST);
SetThreadPriority(hThreads[1],THREAD_PRIORITY_LOWEST);
WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);
TRACE("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2); }
最终TRACE的输出将很可能不是200000000和400000000
9
C/C++教程 作者:高亚清
为解决此类问题,必须引入同步处理机制。常用的同步处理机制包括互锁函数、临界区、和进程、线程、互斥量、信号量、事件等Windows内核对象。
1.1. 原子访问,互锁函数
互锁函数提供了一套多个线程同步访问一个简单变量的处理机制。
, LONG InterlockedIncrement(LONG volatile* lpAddend);
该函数提供多线程情况下,对一个变量以原子操作方式增加1
, LONG InterlockedDecrement(LONG volatile* lpAddend);
该函数提供多线程情况下,对一个变量以原子操作方式减少1
, LONG InterlockedExchange(LONG volatile* lpTarget,LONG lValue);
该函数提供在多线程情况下,以原子操作方式用lValue给lpTarget指向的目标变量赋值,并返回赋值以前的lpTarget指向的值。
, LONG InterlockedExchangeAdd(LONG volatile* lpAddend,LONG lValue)
该函数提供在多线程情况下,以院子的操作方式将lpAddend指向的变量增加lValue,并返回调用前的lpAddend指向的目标变量的值。
示例:
long g_iNum1,g_iNum2;
DWORD WINAPI SubThread(LPVOID lpParam)
{
for(int i=0; i<100000000; i++)
{
InterlockedIncrement(&g_iNum1);
InterlockedExchangeAdd(&g_iNum2,2);
}
return 0;
}
void Test()
{
HANDLE hThreads[2];
g_iNum1=0;
g_iNum2=0;
hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL);
hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL);
SetThreadPriority(hThreads[0],THREAD_PRIORITY_LOWEST);
SetThreadPriority(hThreads[1],THREAD_PRIORITY_LOWEST);
WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);
TRACE("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2); }
10
C/C++教程 作者:高亚清
1.2. 临界区
临界区是一段连续的代码区域,它要求在执行前获得对某些共享数据的独占的访问权。如果一个进程中的所有线程中访问这些共享数据的代码都放在临界区中,就能够实现对该共享数据的同步访问。临界区只能用于同步单个进程中的线程。 例:
//多个线程共享的全局数据
long g_iNum1,g_iNum2;
//实例化临界区对象
CRITICAL_SECTION g_sec;
DWORD WINAPI SubThread(LPVOID lpParam)
{
for(int i=0; i<100000000; i++)
{
//进入临界区,临界区对象的引用计数加1,同一个线程可以多次调用
//EnterCriticalSection,但是如果调用n次EnterCriticalSection以后,
//必须再调用n次的LeaveCriticalSection,使临界区对象的引用计数变为0,
//其它的线程才能进入临界区
EnterCriticalSection(&g_sec);
g_iNum1++;
g_iNum2+=2;
//离开临界区
LeaveCriticalSection(&g_sec);
}
return 0;
}
void Test()
{
HANDLE hThreads[2];
g_iNum1=0;
g_iNum2=0;
//初始化临界区对象
InitializeCriticalSection(&g_sec);
hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL);
hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL);
SetThreadPriority(hThreads[0],THREAD_PRIORITY_LOWEST);
SetThreadPriority(hThreads[1],THREAD_PRIORITY_LOWEST);
WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);
//释放临界区对象
DeleteCriticalSection(&g_sec);
TRACE("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2); }
11
C/C++教程 作者:高亚清
1.3. 内核对象
临界区非常适合于在同一个进程内部以序列化的方式访问共享的数据。然而,有时用户希望一个线程与其他线程执行的某些操作取得同步,这就需要使用内核对象来同步线程。常用的内核对象有进程、线程、互斥量、信号量和事件,其他的还包括文件、控制台输入、文件变化通知、可等待的计时器。
每一个内核对象在任何时候都处于两种状态之一:信号态(signaled)和无信号态(nonsignaled)。线程在等待其中的一个或多个内核对象时,如果在等待的一个或多个内核对象处于无信号态,线程自身将被系统挂起,直到等待的内核对象变为有信号状态时,线程才恢复运行。
常用的等待函数有2个:
, DWORD WaitForSingleObject( //等待单个内核对象
HANDLE hHandle, //指向内核对象的句柄
DWORD dwMilliseconds //等待的毫秒数,如果传入INFINITE,则无限期等待。
);
WaitForSingleObject函数返回值
返回值 含义 WAIT_OBJECT_0 对象处于有信号状态
WAIT_TIMEOUT 对象在指定时间内没有变为有信号状态 WAIT_ABANDONED 对象是一个互斥量,由于被放弃了而变为有信号状态 WAIT_FAILED 发生了错误。调用GetLastError可以得到详细的错误信息
, DWORD WaitForMultipleObjects( //等待多个对象
DWORD nCount, //对象的个数
CONST HANDLE *lpHandles, //对象句柄数组
BOOL bWaitAll, //是否要等到所有的对象都变为信号态
DWORD dwMilliseconds //等待的毫秒数,如果传入INFINITE,则无限期等待。
)
实例可参见1.2的代码
1.3.1. 互斥量
互斥量类似于临界区,但它能够同步多个进程间的数据访问。
示例:
//多个线程共享的全局数据
long g_iNum1,g_iNum2;
//同步对象
12
C/C++教程 作者:高亚清
HANDLE g_hMutex=NULL;
DWORD WINAPI SubThread(LPVOID lpParam)
{
for(int i=0; i<1000000; i++)
{
//等待互斥量,如果互斥量处于信号态,该函数返回,同时
//将g_hMutex变为无信号态
WaitForSingleObject(g_hMutex,INFINITE);
g_iNum1++;
g_iNum2+=2;
//是互斥量重新处于信号态
ReleaseMutex(g_hMutex);
}
return 0;
}
void Test()
{
HANDLE hThreads[2];
g_iNum1=0;
g_iNum2=0;
//创建互斥量
g_hMutex=CreateMutex(NULL,FALSE,"MutexForSubThread");
hThreads[0]=CreateThread(NULL,0,SubThread,NULL,0,NULL);
hThreads[1]=CreateThread(NULL,0,SubThread,NULL,0,NULL);
SetThreadPriority(hThreads[0],THREAD_PRIORITY_LOWEST);
SetThreadPriority(hThreads[1],THREAD_PRIORITY_LOWEST);
WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);
//关闭互斥量
CloseHandle(g_hMutex);
TRACE("g_iNum1:%d g_iNum2:%d\n",g_iNum1,g_iNum2); }
在上例中,主线程创建互斥量,并在创建的同时使互斥量处于信号态。两个子线程在运行过程中,通过调用WaitForSingleObject等待该互斥量,如果此时互斥量处于无信号态,则在等待线程被系统挂起,一直等到互斥量变为信号态才继续运行,WaitForSingleObject返回的同时,将使互斥量再次变为无信号态。最后线程通过调用ReleaseMutex使互斥量变为信号态。
信号量 1.3.2.
信号量内核对象用于资源计数。每当线程调用WaitForSingleObject函数并传入一个信号量对象的句柄,系统将检查该信号量的资源计数是否大于0,如果大于0,系统就将资源计数减去1,并唤醒线程。如果资源计数为0,系统就将线程挂起,直到另外一个线程释放了该对象,释放信号量意味着增加它的资源计数。
信号量与临界区和互斥量不同,它不属于任何线程。因此可以在一个线程中增加信号量的计数,而在另一个线程中
13
C/C++教程 作者:高亚清
减少信号量的计数。
但是在使用过程中,信号量的使用与互斥量非常相似,互斥量可以看作是信号量的一个特殊版本,即可以将互斥量
看作最大资源计数为1的信号量。
通过调用CreateSemaphore函数可以创建一个信号量:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes;
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
);
参数lMaximumCount指定信号量的最大计数。参数lInitialCount指定信号量的初始计数。参数lpName指定对象的
名称。其他进程中的线程使用该名称调用CreateSemaphore函数或OpenSemaphore函数得到信号量的句柄:
HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);
线程使用ReleaseSemaphore函数释放信号量。
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
该函数可以一次增加信号量的大于1的计数,参数lReleaseCount指出一次增加的量。函数在参数lpPreviousCount
中返回该函数调用前的信号量的计数。
信号量常用在如下情况下。M个线程对N个共享资源的访问,其中M>N
下面示例模拟32个线程对4个数据库连接对象的访问:
//数据库连接对象的包装
class CMyConnection
{
private:
BOOL m_bUse;
public:
LPVOID m_pObj; //模拟连接对象
friend class CMyConnectionPool;
};
//数据库缓冲池
class CMyConnectionPool
{
protected:
CMyConnection *m_pConn; //
14
C/C++教程 作者:高亚清
int m_iCount;
HANDLE m_hSemaphore; //信号量 public:
CMyConnectionPool(int iCount)
{
m_iCount=iCount;
m_pConn=new CMyConnection[iCount];
for(int i=0; i
m_bUse=FALSE;
ReleaseSemaphore(m_hSemaphore,1,NULL);
}
};
//实例化4个数据库连接对象,并放进缓冲池中使用。
CMyConnectionPool g_pool(4);
DWORD WINAPI SubThread(LPVOID lpParam)
15
C/C++教程 作者:高亚清
{
CMyConnection *pConn=g_pool.GetConnection();
static long lSubThreadCount=0;
InterlockedIncrement(&lSubThreadCount);
TRACE(_T("当前线程ID:%X, 子线程数:%d\n"),
GetCurrentThreadId(),
lSubThreadCount);
Sleep(2000);//模拟数据库的访问
InterlockedDecrement(&lSubThreadCount);
g_pool.ReleaseConnection(pConn);
return 0;
}
void Test()
{
const int iThreadCount=32;
HANDLE hThreads[iThreadCount];
for(int i=0; i
本文档为【多线程与同步对象】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑,
图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。