1.基本概念
(1)程序和进程:程序是计算机指令的集合,它以文件的形式存储在磁盘上,而进程通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址控件中的一次执行活动(一个程序可以对应于多个线程)
(2)进程组成:操作系统用来管理进程的内核对象(PCB)和地址空间
(3)进程从来不执行任何东西,它只是线程的容器,若要使用进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码,也就是说:真正完成代码执行的是线程,进程只是线程的容器,或者说是线程的执行环境
(4)单个进程可以包含一个(至少一个)或多个线程,当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程,主线程可以创建其他线程
(5)系统赋予每个进程独立的虚拟地址空间
(6)线程组成:线程的内核对象(TCB)和线程栈(用于维护线程在执行代码时需要的所有函数和局部变量,在所属进程的地址空间中分配内存)
(7)线程可以访问进程的所有资源
(8)线程运行:系统通过一种循环的方式为线程提供时间片(调度算法)
(9)线程占的资源比较少,而且切换时不用交换地址空间,只改变执行环境,效率高
2.线程创建函数:CreateThread
(1)函数原型:HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress.
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
//NULL:默认安全性,如果希望它的子进程能继承该线程对象的句柄,则必须设定一个LPSECURITY_ATTRIBUTES结构体,将它的bInheritHandle成员初始化为TRUE
//设置线程初始栈的大小,如果值为0则默认使用与调用该函数的线程相同的栈空间大小
// lpStartAddress指向应用程序定义的LPTHREAD_START_ROUTINE类型的函数的指针,这个函数将由新线程执行,表明新线程的起始地址
该函数名称任意,但函数类型必须遵照下述声明形式:
DWORD WINAPI ThreadProc(LPVOID lpParameter);
// lpParameter通过这个参数给新线程传递参数,该参数提供了一种将初始化值传递给线程函数的手段,这个参数的值既可以是一个数值,也可以是一个指向其他信息的指针
// dwCreationFlags设置用于控制线程创建的附加标记,它可以是CREATE_SUSPENDED或0,若是前一,那么线程创建后处于暂停状态,知道程序调用了ResumeThread函数为止,若为0,那么线程在创建后立即运行
// lpThreadId这个参数是一个返回值,指向一个变量,用来接收线程ID,当创建一个线程时,系统会为该线程分配一个ID(如果不需要这个ID可以设为NULL)
(2)示例:
#include<windows.h>
#include<iostream>
using namespace std;
DWORD WINAPI Func1Proc(LPVOID lpParameter);
int main()
{
HANDLE hThread;
hThread = CreateThread(NULL,0,Func1Proc,NULL,0,NULL);
CloseHandle(hThread);
cout<<"main"<<endl;
return 0;
}
DWORD WINAPI Func1Proc(LPVOID lpParameter)
{
cout<<"Thread"<<endl;
return 0;
}
该程序输出的结果为:mTahirne,maThiren等不确定的字符串(没有进行线程同步)
说明:包含必要的头文件:使用了Windows API函数,所以要包含windows.h文件
创建完线程后,调用CloseHandle函数关闭新线程的句柄,实际上调用closeHandle函数并没有终止新创建的线程,只是表示在主线程中对新创建的线程的引用不感兴趣,因此将它关闭,另一方面,当关闭该句柄时,系统会递减该线程内核对象的使用计数,当创建的这个新线程执行完毕后,系统也会递减该线程内核对象的使用计数,当使用计数为0时,系统会释放该线程内核对象,如果没有关闭线程句柄,系统会一直保持这对线程内核对象的引用,这样该内核对象也就不会被释放,知道进程终止时,系统才会清理这些残留的对象
3.线程同步
(1)利用互斥对象实现线程同步
1.互斥对象属于内核对象,它能够确保线程拥有对单个资源的互斥访问权,互斥对象包含一个使用数量,一个线程ID和一个计数器,其中ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数
2.创建互斥对象:CreateMutex,可以创建或打开一个命名的或匿名的互斥对象
函数原型:HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,//安全属性
BOOL bInitialOwner,//指定互斥对象初始的拥有者,如果该值为真,则创建该互斥对象的线程获得该对象的所有权,否则,该线程将不获得其所有权
LPCTSTR lpName//指定互斥对象的名称,若为NULL则创建一个匿名的互斥对象
);
3.线程对共享资源访问结束后,应释放该对象的所有权:ReleaseMutex函数
函数原型:BOOL ReleaseMutex(HANDLE hMutex);
4.线程必须主动请求共享对象使用权才能获得其所有权:WaitForSingleObject函数
函数原型:DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);
// hHandle,所请求的对象的句柄,若互斥对象处于有信号状态就返回,否则一直等待
//指定等待的时间,若超过时间,函数也返回。若设置为INFINITE,则函数会永远等待
//成功返回WAIT_OBJECT_0 超时返回WAIT_TIMEOUT
5.对互斥对象来说,谁拥有谁释放
6.拥有互斥对象的线程用WaitForSingleObject再次请求该互斥对象,会使该互斥对象的内部计数器为2(1+1),要调用两次ReleaseMutex函数才能使内部计数器变为0,才能被其他线程使用,所以在使用互斥对象时要注意:如果多次在同一线程中请求同一互斥对象,那么需要相应地多次调用RealeaseMutex函数释放该互斥对象
(2)保证应用程序只有一个实例运行
可以通过命名的互斥对象来实现,在调用CreateMutex函数创建一个命名的互斥对象后,如果其返回值是一个有效的句柄,那么可以接着调用GetLastError函数,如果该函数返回的是ERROR_ALREADY_EXISTE,就表明先前已经创建了这个命名的互斥对象,因此就可以知道先前已经有该程序的一个实例在运行了
4.网络聊天室程序的实现(UDP)
(1)基于对话框—添加控件
(2)加载套接字库:AfxSocketInit函数
函数原型:BOOL AfxSocketInit(WSADATA* lpwsaData = NULL);//该函数内部调用WSAStartup函数来加载套接字库,且加载的是1.1版本的套接字库,该函数还可以保证应用程序在终止之前,调用WSACleanup函数终止对套接字的使用,并且利用该函数加载套接字时,不需要为工程链接ws2_32.lib库文件
if (!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
return FALSE;
}
TODO:应该在应用程序类重载的InitInstance函数中调用AfxSocketInit函数
使用该函数,要包含相应的头文件afxsock.h,我们可以在stdafx.h中添加
#include <afxsock.h> // MFC socket extensions
(3)创建并初始化套接字
1.为CChatDlg类增加一个SOCKET类型的成员变量,m_socket(套接字描述符),并将其访问权限设为private,然后为CChatDlg类添加一个BOOL型的成员InitSocket,用来初始化该类的套接字成员变量
函数代码:BOOL CChatDlg::InitSocket()
{
//创建套接字
m_socket = socket(AF_INET,SOCK_DGRAM,0);
if(INVALID_SOCKET == m_socket)
{
MessageBox("Fail To Create Socket!");
return false;
}
//地址
SOCKADDR_IN addrSock;
addrSock.sin_family = AF_INET;
addrSock.sin_port = htons(6000);
addrSock.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
//绑定套接字
int retval = bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR));
if(SOCKET_ERROR == retval)
{
closesocket(m_socket);
MessageBox("Fail To Bind Socket!");
return false;
}
return true;
}
2.在CChatDlg类的OnInitDialog函数中调用InitSocket()函数
(4)实现接收功能:因为在接收端接受数据时,如果没有数据到来,recvfrom函数会阻塞,从而导致程序暂停运行。所以,我们可以将接受数据的操作放置在一个单独的线程中完成,并给这个线程传递两个参数,一个是已经创建的套接字,一个是对话框控件的句柄,这样,在该线程中,当接收到数据后,可以将该数据传回给对话框(将两个参数封装在一个结构体,将地址赋给CreateThread函数的第四个参数)
1.在CChatDlg类的头文件中,该类的外部定义一个结构体
struct RECVPARAM
{
SOCKET sock;
HWND hwnd;
};
2.在CChatDialog类的OnInitDialog函数中,创建线程,并传递所需的参数
InitSocket();
RECVPARAM *pRecvParam = new RECVPARAM;
pRecvParam->sock = m_socket;
pRecvParam->hwnd = m_hWnd;
HANDLE hThread = CreateThread(NULL,0,RecvProc,(LPVOID)pRecvParam,0,NULL);
CloseHandle(hThread);
3.将线程函数声明为类的静态成员函数或全局函数(不可以仅是类的成员函数)
static DWORD WINAPI RecvProc(LPVOID lpParameter);
4.为线程函数添加代码
DWORD WINAPI CChatDlg::RecvProc(LPVOID lpParameter)
{
//获取主线程传递的套接字和窗口句柄
SOCKET sock = ((RECVPARAM*)lpParameter)->sock;
HWND hwnd = ((RECVPARAM*)lpParameter)->hwnd;
delete lpParameter;
SOCKADDR_IN addrFrom;
int len = sizeof(SOCKADDR);
char recvBuf[200];
char tempBuf[300];
int retval;
while(true)
{
//接受数据
retval = recvfrom(sock,recvBuf,200,0,(SOCKADDR*)&addrFrom,&len);
if(SOCKET_ERROR == retval)
break;
sprintf(tempBuf,"%s说:%s",inet_ntoa(addrFrom.sin_addr),recvBuf);
::PostMessage(hwnd,WM_RECVDATA,0,(LPARAM)tempBuf);//自定义消息,处理完后向对话框发送一条自定义消息,由对话框添加响应代码
}
return 0;
}
5.在CChatDlg类的头文件中,定义WM_RECVDATA这个消息的值,即:
#define WM_RECVDATA WM_USER+1
在CChatDlg类的头文件中,编写该消息响应函数原型的声明,即:
afx_msg void OnRecvData(WPARAM wParam,LPARAM lParam);(对于自定义的消息,响应函数原型的声明要写在AFX_MSG宏外)
在CChatDlg类的源文件中,添加WM_RECVDATA消息映射,即:
ON_MESSAGE(WM_PECVDATA,OnRecvData)(注意末尾不要有分号)
在CChatDlg类的源文件中,添加消息响应函数的实现
void CChatDlg::OnRecvData(WPARAM wParam,LPARAM lParam)
{
//取出接受到的数据
CString str = (char*)lParam;
CString strTemp;
//获得已有数据
GetDlgItemText(IDC_EDIT1,strTemp);
str += "\r\n";
str +=strTemp;
//显示所有接收到的数据
SetDlgItemText(IDC_EDIT1,str);
}
(5)实现发送端功能
1.添加发送按钮响应函数
2.添加代码:
void CChatDlg::OnButton1()
{
//获取对方IP
DWORD dwIP;
((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);//获得IP框中数据
SOCKADDR_IN addrTo;
addrTo.sin_family = AF_INET;
addrTo.sin_port = htons(6000);
addrTo.sin_addr.S_un.S_addr = htonl(dwIP);
//获取待发送数据
CString strSend;
GetDlgItemText(IDC_EDIT2,strSend);
//发送数据
sendto(m_socket,strSend,strSend.GetLength()+1,0,(SOCKADDR*)&addrTo,sizeof(SOCKADDR));
//清空发送编辑框中的内容
SetDlgItemText(IDC_EDIT2,"");
}