前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MFC多线程

MFC多线程

作者头像
拾点阳光
发布2018-05-10 18:10:53
2.4K0
发布2018-05-10 18:10:53
举报
文章被收录于专栏:码云1024

当前流行的Windows操作系统能同时运行几个程序(独立运行的程序又称之为进程),对于同一个程序,它又可以分成若干个独立的执行流,我们称之为线程,线程提供了多任务处理的能力。用进程和线程的观点来研究软件是当今普遍采用的方法,进程和线程的概念的出现,对提高软件的并行性有着重要的意义。现在的大型应用软件无一不是多线程多任务处理,单线程的软件是不可想象的。因此掌握多线程多任务设计方法对每个程序员都是必需要掌握的。本实例针对多线程技术在应用中经常遇到的问题,如线程间的通信、同步等,分别进行探讨,并利用多线程技术进行线程之间的通信,实现了数字的简单排序。   一、 实现方法 1、理解线程   要讲解线程,不得不说一下进程,进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、 数据和其它系统资源组成。进程在运行时创建的资源随着进程的终止而死亡。线程的基本思想很简单,它是一 个独立的执行流,是进程内部的一个独立的执行单元,相当于一个子程序,它对应于Visual C++中的CwinThread 类对象。单独一个执行程序运行时,缺省地包含的一个主线程,主线程以函数地址的形式出现,提供程序的启动点 ,如main()或WinMain()函数等。当主线程终止时,进程也随之终止。根据实际需要,应用程序可以分解成许 多独立执行的线程,每个线程并行的运行在同一进程中。   一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。操作系统给每个 线程分配不同的CPU时间片,在某一个时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮 流执行,由于每个时间片时间很短,所以对用户来说,仿佛各个线程在计算机中是并行处理的。操作系统是根据线 程的优先级来安排CPU的时间,优先级高的线程优先运行,优先级低的线程则继续等待。   线程被分为两种:用户界面线程和工作线程(又称为后台线程)。用户界面线程通常用来处理用户的输入并 响应各种事件和消息,其实,应用程序的主执行线程CWinAPP对象就是一个用户界面线程,当应用程序启动时自动 创建和启动,同样它的终止也意味着该程序的结束,进程终止。工作线程用来执行程序的后台处理任务,比如计 算、调度、对串口的读写操作等,它和用户界面线程的区别是它不用从CWinThread类派生来创建,对它来说最重 要的是如何实现工作线程任务的运行控制函数。工作线程和用户界面线程启动时要调用同一个函数的不同版本;最 后需要读者明白的是,一个进程中的所有线程共享它们父进程的变量,但同时每个线程可以拥有自己的变量。   2、线程的管理和操作   (一)线程的启动   创建一个用户界面线程,首先要从类CwinThread产生一个派生类,同时必须使用DECLARE_DYNCREATE和 IMPLEMENT_DYNCREATE来声明和实现这个CwinThread派生类。 第二步是根据需要重载该派生类的一些成员函数如: ExitInstance()、 InitInstance()、 OnIdle()、 PreTranslateMessage()等函数。最后调用AfxBeginThread()函数的一个版本: CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ) 启动该用户界面线程,其中第一个参数为指向定义的用户界面线程类 指针变量,第二个参数为线程的优先级,第三个参数为线程所对应的堆栈大小,第四个参数为线程创建时的附加标志,缺省为正 常状态,如为CREATE_SUSPENDED则线程启动后为挂起状态。   对于工作线程来说,启动一个线程,首先需要编写一个希望与应用程序的其余部分并行运行的函数如Fun1(),接着定义一个指向CwinThread对象的指针变量*pThread,调用AfxBeginThread(Fun1,param,priority)函数,返回值赋给pThread变量的同时一并启动该线程来执行上面的Fun1()函数,其中Fun1是线程要运行的函数的名字,也既是上面所说的控制函数的名字,param是准备传送给线程函数Fun1的任意32位值,priority则是定义该线程的优先级别,它是预定义的常数,读者可参考MSDN。 (二)线程的优先级   以下的CwinThread类的成员函数用于线程优先级的操作: int GetThreadPriority(); BOOL SetThradPriority()(int nPriority);   上述的二个函数分别用来获取和设置线程的优先级,这里的优先级,是相对于该线程所处的优先权层次 而言的,处于同一优先权层次的线程,优先级高的线程先运行;处于不同优先权层次上的线程,谁的优先权 层次高,谁先运行。至于优先级设置所需的常数,自己参考MSDN就可以了,要注意的是要想设置线程的优先 级,这个线程在创建时必须具有THREAD_SET_INFORMATION访问权限。对于线程的优先权层次的设置, CwinThread类没有提供相应的函数,但是可以通过Win32 SDK函数GetPriorityClass()和SetPriorityClass() 来实现。 (三)线程的悬挂和恢复   CWinThread类中包含了应用程序悬挂和恢复它所创建的线程的函数,其中SuspendThread()用来悬挂线程,暂停线程的执行;ResumeThread()用来恢复线程的执行。如果你对一个线程连续若干次执行SuspendThread(),则需要连续执行相应次的ResumeThread()来恢复线程的运行。 (四)结束线程   终止线程有三种途径,线程可以在自身内部调用AfxEndThread()来终止自身的运行;可以在线程的外部调用 BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode )来强行终止一个线程的运行,然后调用 CloseHandle()函数释放线程所占用的堆栈;第三种方法是改变全局变量,使线程的执行函数返回,则该线程终止。 下面以第三种方法为例,给出部分代码: //////////////////////////////////////////////////////////////// //////CtestView message handlers ///// Set to True to end thread Bool bend=FALSE;//定义的全局变量,用于控制线程的运行; // The Thread Function; UINT ThreadFunction(LPVOID pParam)//线程函数 {   while(!bend)   {    Beep(100,100);    Sleep(1000);   }   return 0; } ///////////////////////////////////////////////////////////// CwinThread *pThread; HWND hWnd; Void CtestView::OninitialUpdate() {   hWnd=GetSafeHwnd();   pThread=AfxBeginThread(ThradFunction,hWnd);//启动线程   pThread->m_bAutoDelete=FALSE;//线程为手动删除   Cview::OnInitialUpdate(); } //////////////////////////////////////////////////////////////// Void CtestView::OnDestroy() {   bend=TRUE;//改变变量,线程结束   WaitForSingleObject(pThread->m_hThread,INFINITE);//等待线程结束   delete pThread;//删除线程   Cview::OnDestroy(); }   3、线程之间的通信   通常情况下,一个次级线程要为主线程完成某种特定类型的任务,这就隐含着表示在主线程 和次级线程之间需要建立一个通信的通道。一般情况下,有下面的几种方法实现这种通信任务: 使用全局变量(上一节的例子其实使用的就是这种方法)、使用事件对象、使用消息。这里我们 主要介绍后两种方法。   (一) 利用用户定义的消息通信   在Windows程序设计中,应用程序的每一个线程都拥有自己的消息队列,甚至工作线程也不 例外,这样一来,就使得线程之间利用消息来传递信息就变的非常简单。首先用户要定义一个用 户消息,如下所示:#define WM_USERMSG WMUSER+100;在需要的时候,在一个线程中调用:: PostMessage((HWND)param,WM_USERMSG,0,0)或CwinThread::PostThradMessage()来向另外 一个线程发送这个消息,上述函数的四个参数分别是消息将要发送到的目的窗口的句柄、要发送 的消息标志符、消息的参数WPARAM和LPARAM。下面的代码是对上节代码的修改,修改后的结果 是在线程结束时显示一个对话框,提示线程结束:

UINT ThreadFunction(LPVOID pParam) {  while(!bend)  {   Beep(100,100);   Sleep(1000);  }  ::PostMessage(hWnd,WM_USERMSG,0,0);  return 0; } ////////WM_USERMSG消息的响应函数为OnThreadended(WPARAM wParam, LPARAM lParam) LONG CTestView::OnThreadended(WPARAM wParam,LPARAM lParam) {  AfxMessageBox("Thread ended.");  Retrun 0; }   上面的例子是工作者线程向用户界面线程发送消息,对于工作者线程,如果它的设计模式也 是消息驱动的,那么调用者可以向它发送初始化、退出、执行某种特定的处理等消息,让它在后 台完成。在控制函数中可以直接使用::GetMessage()这个SDK函数进行消息分检和处理,自己 实现一个消息循环。GetMessage()函数在判断该线程的消息队列为空时,线程将系统分配给它的 时间片让给其它线程,不无效的占用CPU的时间,如果消息队列不为空,就获取这个消息,判断这 个消息的内容并进行相应的处理。   (二)用事件对象实现通信   在线程之间传递信号进行通信比较复杂的方法是使用事件对象,用MFC的Cevent类的对象来 表示。事件对象处于两种状态之一:有信号和无信号,线程可以监视处于有信号状态的事件,以 便在适当的时候执行对事件的操作。上述例子代码修改如下:

//////////////////////////////////////////////////////////////////// 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);   //等待threadEnd事件有信号,无信号时线程在这里悬停   If(result==Wait_OBJECT_0)    Bend=TRUE;  }  ::PostMessage(hWnd,WM_USERMSG,0,0);  return 0; } ///////////////////////////////////////////////////////////// Void CtestView::OninitialUpdate() {  hWnd=GetSafeHwnd();  threadStart.SetEvent();//threadStart事件有信号  pThread=AfxBeginThread(ThreadFunction,hWnd);//启动线程  pThread->m_bAutoDelete=FALSE;  Cview::OnInitialUpdate(); } //////////////////////////////////////////////////////////////// Void CtestView::OnDestroy() {  threadEnd.SetEvent();  WaitForSingleObject(pThread->m_hThread,INFINITE);  delete pThread;  Cview::OnDestroy(); }   运行这个程序,当关闭程序时,才显示提示框,显示"Thread ended"。

  4、线程之间的同步   前面我们讲过,各个线程可以访问进程中的公共变量,所以使用多线程的过程中需要注意的 问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏数据的完整性。保证各个 线程可以在一起适当的协调工作称为线程之间的同步。前面一节介绍的事件对象实际上就是一种 同步形式。Visual C++中使用同步类来解决操作系统的并行性而引起的数据不安全的问题,MFC 支持的七个多线程的同步类可以分成两大类:同步对象(CsyncObject、Csemaphore、Cmutex、 CcriticalSection和Cevent)和同步访问对象(CmultiLock和CsingleLock)。本节主要介绍临 界区(critical section)、互斥(mutexe)、信号量(semaphore),这些同步对象使各个线 程协调工作,程序运行起来更安全。   (一) 临界区   临界区是保证在某一个时间只有一个线程可以访问数据的方法。使用它的过程中,需要给 各个线程提供一个共享的临界区对象,无论哪个线程占有临界区对象,都可以访问受到保护的数 据,这时候其它的线程需要等待,直到该线程释放临界区对象为止,临界区被释放后,另外的线 程可以强占这个临界区,以便访问共享的数据。临界区对应着一个CcriticalSection对象,当线 程需要访问保护数据时,调用临界区对象的Lock()成员函数;当对保护数据的操作完成之后,调 用临界区对象的Unlock()成员函数释放对临界区对象的拥有权,以使另一个线程可以夺取临界区 对象并访问受保护的数据。同时启动两个线程,它们对应的函数分别为WriteThread()和 ReadThread(),用以对公共数组组array[]操作,下面的代码说明了如何使用临界区对象:

#include "afxmt.h" int array[10],destarray[10]; CCriticalSection Section; UINT WriteThread(LPVOID param) {  Section.Lock();  for(int x=0;x<10;x++)   array[x]=x;  Section.Unlock(); } UINT ReadThread(LPVOID param) {  Section.Lock();  For(int x=0;x<10;x++)   Destarray[x]=array[x];   Section.Unlock(); }   上述代码运行的结果应该是Destarray数组中的元素分别为1-9,而不是杂乱无章的数,如 果不使用同步,则不是这个结果,有兴趣的读者可以实验一下。   (二)互斥   互斥与临界区很相似,但是使用时相对复杂一些,它不仅可以在同一应用程序的线程间实 现同步,还可以在不同的进程间实现同步,从而实现资源的安全共享。互斥与Cmutex类的对象 相对应,使用互斥对象时,必须创建一个CSingleLock或CMultiLock对象,用于实际的访问控 制,因为这里的例子只处理单个互斥,所以我们可以使用CSingleLock对象,该对象的Lock() 函数用于占有互斥,Unlock()用于释放互斥。实现代码如下:

#include "afxmt.h" int array[10],destarray[10]; CMutex Section; UINT WriteThread(LPVOID param) {  CsingleLock singlelock;  singlelock (&Section);  singlelock.Lock();  for(int x=0;x<10;x++)   array[x]=x;  singlelock.Unlock(); } UINT ReadThread(LPVOID param) {  CsingleLock singlelock;  singlelock (&Section);  singlelock.Lock();  For(int x=0;x<10;x++)   Destarray[x]=array[x];   singlelock.Unlock(); }   (三)信号量   信号量的用法和互斥的用法很相似,不同的是它可以同一时刻允许多个线程访问同一个 资源,创建一个信号量需要用Csemaphore类声明一个对象,一旦创建了一个信号量对象,就 可以用它来对资源的访问技术。要实现计数处理,先创建一个CsingleLock或CmltiLock对 象,然后用该对象的Lock()函数减少这个信号量的计数值,Unlock()反之。下面的代码分别 启动三个线程,执行时同时显示二个消息框,然后10秒后第三个消息框才得以显示。 ============================================================================ |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ---------------------------------------------------------------------------- Csemaphore *semaphore; Semaphore=new Csemaphore(2,2); HWND hWnd=GetSafeHwnd(); AfxBeginThread(threadProc1,hWnd); AfxBeginThread(threadProc2,hWnd); AfxBeginThread(threadProc3,hWnd); UINT ThreadProc1(LPVOID param) {  CsingleLock singelLock(semaphore);  singleLock.Lock();  Sleep(10000);  ::MessageBox((HWND)param,"Thread1 had access","Thread1",MB_OK);  return 0; } UINT ThreadProc2(LPVOID param) {  CSingleLock singelLock(semaphore);  singleLock.Lock();  Sleep(10000);  ::MessageBox((HWND)param,"Thread2 had access","Thread2",MB_OK);  return 0; } UINT ThreadProc3(LPVOID param) {  CsingleLock singelLock(semaphore);  singleLock.Lock();  Sleep(10000);  ::MessageBox((HWND)param,"Thread3 had access","Thread3",MB_OK);  return 0; }

二、 编程步骤   1、 启动Visual C++6.0,生成一个32位的控制台程序,将该程序命名为"sequence"   2、 输入要排续的数字,声明四个子线程;   3、 输入代码,编译运行程序。 三、 程序代码 ////////////////////////////////////////////////////////////////////////////////////// // sequence.cpp : Defines the entry point for the console application. #include "stdafx.h" #include "stdlib.h" #include "memory.h" HANDLE evtTerminate; //事件信号,标记是否所有子线程都执行完

HANDLE evtPrint; //事件信号,标记事件是否已发生 //CRITICAL_SECTION csPrint; //临界区 //HANDLE mtxPrint; //互斥信号,如有信号表明已经有线程进入临界区并拥有此信号 static long ThreadCompleted = 0; //下面的结构是用于传送排序的数据给各个排序子线程 struct MySafeArray {  long* data;  int iLength; }; //打印每一个线程的排序结果 void PrintResult(long* Array, int iLength, const char* HeadStr = "sort"); //排序函数 unsigned long __stdcall BubbleSort(void* theArray); //冒泡排序 unsigned long __stdcall SelectSort(void* theArray); //选择排序 unsigned long __stdcall HeapSort(void* theArray); //堆排序 unsigned long __stdcall InsertSort(void* theArray); //插入排序 int QuickSort(long* Array, int iLow, int iHigh); //快速排序 int main(int argc, char* argv[]) {  long data[] = {123,34,546,754,34,74,3,56};  int iDataLen = 8;  //为了对各个子线程分别对原始数据进行排序和保存排序结果  //分别分配内存对data数组的数据进行复制  long *data1, *data2, *data3, *data4, *data5;  MySafeArray StructData1, StructData2, StructData3, StructData4;  data1 = new long[iDataLen];  memcpy(data1, data, iDataLen << 2); //把data中的数据复制到data1中  //内存复制 memcpy(目标内存指针, 源内存指针, 复制字节数), 因为long的长度  //为4字节,所以复制的字节数为iDataLen << 2, 即等于iDataLen*4  StructData1.data = data1;  StructData1.iLength = iDataLen;  data2 = new long[iDataLen];  memcpy(data2, data, iDataLen << 2);  StructData2.data = data2;  StructData2.iLength = iDataLen;  data3 = new long[iDataLen];  memcpy(data3, data, iDataLen << 2);  StructData3.data = data3;  StructData3.iLength = iDataLen;  data4 = new long[iDataLen];  memcpy(data4, data, iDataLen << 2);  StructData4.data = data4;  StructData4.iLength = iDataLen;  data5 = new long[iDataLen];  memcpy(data5, data, iDataLen << 2);  unsigned long TID1, TID2, TID3, TID4;  //对信号量进行初始化  evtTerminate = CreateEvent(NULL, FALSE, FALSE, "Terminate");  evtPrint = CreateEvent(NULL, FALSE, TRUE, "PrintResult");  //分别建立各个子线程  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);  //在主线程中执行行快速排序,其他排序在子线程中执行  QuickSort(data5, 0, iDataLen - 1);  PrintResult(data5, iDataLen, "Quick Sort");  WaitForSingleObject(evtTerminate, INFINITE); //等待所有的子线程结束  //所有的子线程结束后,主线程才可以结束  delete[] data1;  delete[] data2;  delete[] data3;  delete[] data4;  CloseHandle(evtPrint);  return 0; }

unsigned long __stdcall BubbleSort(void* theArray) {  long* Array = ((MySafeArray*)theArray)->data;  int iLength = ((MySafeArray*)theArray)->iLength;  int i, j=0;  long swap;  for (i = iLength-1; i >0; i--)  {   for(j = 0; j < i; j++)   {    if(Array[j] >Array[j+1]) //前比后大,交换    {     swap = Array[j];     Array[j] = Array[j+1];     Array[j+1] = swap;    }   }  }  PrintResult(Array, iLength, "Bubble Sort"); //向控制台打印排序结果  InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1  if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完  //若都执行完则设置程序结束信号量  return 0; }

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 < iLength-1; i++)  {   lMin = Array[i];   iMinPos = i;   for(j=i + 1; j <= iLength-1; j++) //从无序的元素中找出最小的元素   {    if(Array[j] < lMin)    {     iMinPos = j;     lMin = Array[j];    }   }   //把选出的元素交换拼接到有序序列的最后   lSwap = Array[i];   Array[i] = Array[iMinPos];   Array[iMinPos] = lSwap;  }  PrintResult(Array, iLength, "Select Sort"); //向控制台打印排序结果  InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1  if(ThreadCompleted == 4) SetEvent(evtTerminate);//检查是否其他线程都已执行完  //若都执行完则设置程序结束信号量  return 0; }

unsigned long __stdcall HeapSort(void* theArray) {  long* Array = ((MySafeArray*)theArray)->data;  int iLength = ((MySafeArray*)theArray)->iLength;  int i, j, p;  long swap;  for(i=0; i {   for(j = iLength - 1; j>i; j--) //从最后倒数上去比较字节点和父节点   {    p = (j - i - 1)/2 + i; //计算父节点数组下标    //注意到树节点序数跟数组下标不是等同的,因为建堆的元素个数逐个递减    if(Array[j] < Array[p]) //如果父节点数值大则交换父节点和字节点    {     swap = Array[j];     Array[j] = Array[p];     Array[p] = swap;    }   }  }  PrintResult(Array, iLength, "Heap Sort"); //向控制台打印排序结果  InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1  if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完  //若都执行完则设置程序结束信号量  return 0; }

unsigned long __stdcall InsertSort(void* theArray) {  long* Array = ((MySafeArray*)theArray)->data;  int iLength = ((MySafeArray*)theArray)->iLength;  int i=1, j=0;  long temp;  for(i=1; i {   temp = Array[i]; //取出序列后面无序数据的第一个元素值   for(j=i; j>0; j--) //和前面的有序数据逐个进行比较找出合适的插入位置   {    if(Array[j - 1] >temp) //如果该元素比插入值大则后移     Array[j] = Array[j - 1];    else //如果该元素比插入值小,那么该位置的后一位就是插入元素的位置     break;   }   Array[j] = temp;  }  PrintResult(Array, iLength, "Insert Sort"); //向控制台打印排序结果  InterlockedIncrement(&ThreadCompleted); //返回前使线程完成数标记加1  if(ThreadCompleted == 4) SetEvent(evtTerminate); //检查是否其他线程都已执行完   //若都执行完则设置程序结束信号量  return 0; } int QuickSort(long* Array, int iLow, int iHigh) {  if(iLow >= iHigh) return 1; //递归结束条件  long pivot = Array[iLow];  int iLowSaved = iLow, iHighSaved = iHigh; //保未改变的iLow,iHigh值保存起来  while (iLow < iHigh)  {   while (Array[iHigh] >= pivot && iHigh >iLow) //寻找比支点大的元素    iHigh -- ;   Array[iLow] = Array[iHigh]; //把找到的元素放置到空置的位置   while (Array[iLow] < pivot && iLow < iHigh) //寻找比支点小的元素    iLow ++ ;   Array[iHigh] = Array[iLow]; //把找到的元素放置到空置的位置 }  Array[iLow] = pivot; //把支点值放置到支点位置,这时支点位置是空置的  //对左右部分分别进行递归处理  QuickSort(Array, iLowSaved, iHigh-1);  QuickSort(Array, iLow+1, iHighSaved);  return 0; } //每一个线程都要使用这个函数进行输出,而且只有一个显示器,产生多个线程 //竞争对控制台的使用权。 void PrintResult(long* Array, int iLength, const char* HeadStr) {  WaitForSingleObject(evtPrint, INFINITE); //等待事件有信号  //EnterCriticalSection(&csPrint); //标记有线程进入临界区  //WaitForSingleObject(mtxPrint, INFINITE); //等待互斥量空置(没有线程拥有它)  int i;  printf("%s: ", HeadStr);  for (i=0; i {   printf("%d,", Array[i]);   Sleep(100); //延时(可以去掉)

 }  printf("%d\n", Array[i]);  SetEvent(evtPrint); //把事件信号量恢复,变为有信号 } 四、 小结   对复杂的应用程序来说,线程的应用给应用程序提供了高效、快速、安全的数据处理能力。本实例讲述了线程处理中经常遇到的问题,希望对读者朋友有一定的帮助,起到抛砖引玉的作用。 本栏文章均来自于互联网,版权归原作者和各发布网站所有,本站收集这些文章仅供学习参考之用。任何人都不能将这些文章用于商业或者其他目的。( ProgramFan.Com )

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2016-07-27 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档