windows编程-系统编程入门

1.进程线程概念(简略版)

1.1 进程

1.1.1 概念 

我们编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为「进程」(Process)

windows编程-系统编程入门_第1张图片

1.1.2 并行与并发

 当进程要从硬盘读取数据时,CPU 不需要阻塞等待数据的返回,而是去执行另外的进程。当硬盘数据返回时,CPU 会收到个中断,于是 CPU 再继续运行这个进程。

这种多个程序、交替执行的思想,就有 CPU 管理多个进程的初步想法。

对于一个支持多进程的系统,CPU 会从一个进程快速切换至另一个进程,其间每个进程各运行几十或几百个毫秒。

虽然单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发

windows编程-系统编程入门_第2张图片

1.1.3 进程的状态 

 我们知道了进程有着「运行 - 暂停 - 运行」的活动规律。一般说来,一个进程并不是自始至终连续不停地运行的,它与并发执行中的其他进程的执行是相互制约的

它有时处于运行状态,有时又由于某种原因而暂停运行处于等待状态,当使它暂停的原因消失后,它又进入准备运行状态。

windows编程-系统编程入门_第3张图片

windows编程-系统编程入门_第4张图片

1.1.4 进程控制结构 

 在操作系统中,是用进程控制块process control block,PCB)数据结构来描述进程的。

 PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。

(1)PCB 具体包含什么信息呢?

进程控制和管理信息:

  • 进程当前状态,如 new、ready、running、waiting 或 blocked 等;
  • 进程优先级:进程抢占 CPU 时的优先级;

资源分配清单:

  • 有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。

CPU 相关信息:

  • CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。

 (2)每个 PCB 是如何组织的呢?

通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:

  • 将所有处于就绪状态的进程链在一起,称为就绪队列
  • 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列
  • 另外,对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序。

windows编程-系统编程入门_第5张图片

1.2 线程 

1.2.1  线程来源

在早期的操作系统中都是以进程作为独立运行的基本单位,直到后面,计算机科学家们又提出了更小的能独立运行的基本单位,也就是线程

 为什么使用线程?

对于下面的程序,使用单进程的这种方式,存在以下问题:

  • 播放出来的画面和声音会不连贯,因为当 CPU 能力不够强的时候,Read 的时候可能进程就等在这了,这样就会导致等半天才进行数据解压和播放;
  • 各个函数之间不是并发执行,影响资源的使用效率;

windows编程-系统编程入门_第6张图片

改进成多进程的方式:

windows编程-系统编程入门_第7张图片

依然会存在问题:

  • 进程之间如何通信,共享数据?
  • 维护进程的系统开销较大,如创建进程时,分配资源、建立 PCB;终止进程时,回收资源、撤销 PCB;进程切换时,保存当前进程的状态信息;

我们需要有一种新的实体,满足以下特性:

  • 实体之间可以并发运行;
  • 实体之间共享相同的地址空间;

这个新的实体,就是线程( Thread ),线程之间可以并发运行且共享相同的地址空间 

 1.2.2  线程概念

同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的

windows编程-系统编程入门_第8张图片

线程的优点:

  • 一个进程中可以同时存在多个线程;
  • 各个线程之间可以并发执行;
  • 各个线程之间可以共享地址空间和文件等资源;

线程的缺点:

  • 当进程中的一个线程崩溃时,会导致其所属进程的所有线程崩溃(这里是针对 C/C++ 语言,Java语言中的线程奔溃不会造成进程崩溃

2. 线程操作

2.1 常用关键函数

2.1.1 创建线程(CreateThread)

HANDLE CreateThread(
	LPSECURITY_ATTRIBUTES lpThreadAttributes,//指向SECURITY_ATTRIBUTES结构的指针,该结构确定返回的句柄是否可以由子进程继承
	SIZE_T dwStackSize,//堆栈的初始化大小(以字节为单位)
	LPTHREAD_START_ROUTINE lpStartAddress,//指向由进程执行的应用程序定义函数的指针
	LPVOID lpParmeter,//指向要传递给线程的变脸指针
	DWORD dwCreationFlags,//控制线程创建的标志
	LPDWORD lpThreadID //指向接收线程标识(ID)的变量指针
);

参数说明:
lpThreadAttributes::安全属性,一般传入NULL
dwStackSize:线程栈的大小,传入0则默认1MB
lpStartAddress:线程处理函数的函数地址, 表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。
lpParameter:传递给线程处理函数的参数(这里我们想传给线程处理函数什么参数,我就就可以填什么参数,但是要注意类型的转换)
dwCreationFlags:线程的创建方式(立即创建(0)或挂起(CREATE_SUSPENDED)等)
lpThreadID:这里我们只需要填上变量的地址,该函数会自动填入线程ID,传入NULL表示不需要返回该线程ID号
返回值:如果创建成功,则返回线程句柄(一个编号)

 2.1.1 创建线程 (beginthread() & beginthreadex())

beginthread() 

unsigned long _beginthread(
  void(_cdecl *start_address)(void *), //声明为void (*start_address)(void *)形式
  unsigned stack_size, //是线程堆栈大小,一般默认为0
  void *arglist //向线程传递的参数,一般为结构体
);
 

 beginthreadex()

unsigned long _beginthreadex( //推荐使用
  void *security,	//安全属性,NULL表示默认安全性
  unsigned stack_size, //是线程堆栈大小,一般默认为0
  unsigned(_stdcall  *start_address)(void *),	//声明为unsigned(*start_address)(void *)形式
  void *argilist,	//向线程传递的参数,一般为结构体
  unsigned initflag, //新线程的初始状态,0表示立即执行,CREATE_SUSPEND表示创建后挂起。
  unsigned *thrdaddr //该变量存放线程标识符,它是CreateThread函数中的线程ID。
); //创建成功条件下的将线程句柄转化为unsigned long型返回,创建失败条件下返回0

线程结束:

//释放线程空间、释放线程TLS空间、调用ExiteThread结束线程。
void _endthread(void); 	
// retval:设定的线程结束码,与ExiteThread函数的参数功能一样,
//其实这个函数释放线程TLS空间,再调用ExiteThread函数,但没有释放线程空间。
void _endthreadex(unsigned retval);	

在windows开发中,我们有两种创建线程的方式:

第一种:Windows API CreateThread() 来创建线程;用 ExitThread() 来退出线程;
第二种:调用MSVC CRT的函数 _beginthread() 或 _beginthreadex() 来创建线程;用 CRT的 _endthread() 来退出线程。

而实际上,_beginthread()  的内部实现,也是通过调用 CreateThread()  来实现的创建线程。
这两种方式的区别在于,CreateThread() 在静态链接时,会无法释放 _tiddata 导致内存泄漏。

 

 2.2 线程创建

 2.2.1 _beginthreadex

 一般都使用_beginthreadex创建,参数如下

	unsigned int threadID1, threadID2;
	int param1 = 5,param2 =5;
	_beginthreadex(
		NULL,
		0,
		&mythreadfunc1, //传入要操作的函数名
		(void*)¶m1, //传入函数的参数
		0, 
		&threadID1 //用来接收创建的新线程的threadid线程号
	);

注意 第三个参数,我们搜索其定义发现,对应的函数只能是这样的结构:

typedef unsigned (__stdcall* _beginthreadex_proc_type)(void*);

windows编程-系统编程入门_第9张图片windows编程-系统编程入门_第10张图片

所以,线程函数为:

unsigned WINAPI mythreadfunc1(void* param) {
	int count =*((int*)param);
	while (count--) {
		printf("thread 1 dance!\n");
		Sleep(2000);
	}
	return 0;
}

完整代码:

#include
#include 
#include 
#include 

//定义线程要操作的函数
//unsigned WINAPI 在这里指的是一个使用 __stdcall 调用约定的函数,
//返回一个无符号整数。这种格式的函数签名是创建Windows线程所必需的,
//确保了线程函数与Windows的线程管理机制兼容。
//(void*)¶m表示转换为任意指针
unsigned WINAPI mythreadfunc1(void* param) {
	int count =*((int*)param);
	while (count--) {
		printf("thread 1 dance!\n");
		Sleep(2000);
	}
	return 0;
}

unsigned WINAPI mythreadfunc2(void* param) {
	int count = *((int*)param);
	while (count--) {
		printf("thread 2 sing!\n");
		Sleep(2000);
	}
	return 0;
}
int main() {

	unsigned int threadID1, threadID2;
	int param1 = 5,param2 =5;
	_beginthreadex(
		NULL,
		0,
		&mythreadfunc1, //传入要操作的函数名
		(void*)¶m1, //传入函数的参数
		0, 
		&threadID1 //用来接收创建的新线程的threadid线程号
	);

	_beginthreadex(
		NULL,
		0,
		&mythreadfunc2, //传入要操作的函数名
		(void*)¶m2, //传入函数的参数
		0,
		&threadID2 //用来接收创建的新线程的threadid线程号
	);
	//因为_beginthreadex是非阻塞的,所以不会等到它执行完再执行后面,因此我们设置一个sleep让后面等一下
	Sleep(20000);
	system("pause");
	return 0;
}

 效果展示:
windows编程-系统编程入门_第11张图片

两个程序同时执行,没有先后顺序 

 2.2.2 createthread

代码如下:

#include 
#include 
#include 

DWORD WINAPI ThreadFun(LPVOID p)
{
	int iMym = *((int*)p);
	printf("我是子线程,PID = %d,iMym = %d\n", GetCurrentThreadId(), iMym);
	return 0;
}

int main()
{
	printf("main begin\n");

	HANDLE hThread;
	DWORD dwThreadID;
	int m = 100;
	hThread = CreateThread(NULL, 0, ThreadFun, &m, 0, &dwThreadID);
	printf("我是主线程,PID = %d\n", GetCurrentThreadId());
	CloseHandle(hThread);
	Sleep(2000);
	system("pause");
	return 0;
}

 效果展示:

windows编程-系统编程入门_第12张图片

 2.3 多线程

2.3.1 等待函数

 多线程问题:

线程非阻塞,那么如果主函数结束,子线程没有结束,就会报错,这是我们需要避免的

1.WaitForSingleObject

 来等待一个内核对象变为已通知状态

WaitForSingleObject(
In HANDLE hHandle, //指明一个内核对象的句柄
In DWORD dwMilliseconds //等待时间
);
#include 
#include 
#include 


unsigned int __stdcall ThreadFun(LPVOID p)
{
	int cnt = *((int*)p);
	for (int i = 0; i < cnt; i++)
	{
		Sleep(1000);
		puts("running thread");
	}
	return 0;
}


int main()
{
	printf("main begin\n");
	int iParam = 5;
	unsigned int dwThreadID;
	DWORD wr;

	HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFun,
		(void*)&iParam, 0, &dwThreadID);

	if (hThread == NULL)
	{
		puts("_beginthreadex() error");
		return -1;
	}
	// 
	printf("WaitForSingleObject begin\n");
	if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED)
	{
		puts("thread wait error");
		return -1;
	}
	printf("WaitForSingleObject end\n");

	printf("main end\n");
	system("pause");
	return 0;
}

 运行结果:

windows编程-系统编程入门_第13张图片

2. WaitForMultipleObjects

 我们定义两个线程,创建子线程

WaitForMultipleObjects
WaitForMultipleObjects(
In DWORD nCount, // 要监测的句柄的组的句柄的个数
In_reads(nCount) CONST HANDLE* lpHandles, //要监测的句柄的组
In BOOL bWaitAll, // TRUE 等待所有的内核对象发出信号, FALSE 任意一个内核对象发出信号
In DWORD dwMilliseconds //等待时间
);
#include 
#include 
#include 

#define NUM_THREAD	50
unsigned WINAPI threadInc(void * arg);
unsigned WINAPI threadDes(void * arg);
long long num=0;

int main(int argc, char *argv[]) 
{
	HANDLE tHandles[NUM_THREAD];
	int i;

	printf("sizeof long long: %d \n", sizeof(long long));
	for(i=0; i

运行结果:

按理说加减线程数目相同,虽然可能执行的顺序不定,但是对num的总操作是一样的,结果就该为0,但是结果不为0,且每次不一样,这个问题留到了下一节!
 windows编程-系统编程入门_第14张图片

windows编程-系统编程入门_第15张图片 2.3.2 线程同步-互斥对象(互斥锁)

 由于所有线程都在访问同一个资源num,那么有可能一个线程还没结束操作返回修改的num,但是另一个线程也开始调用num,那么就会导致一个num被从不同途径修改两次,导致结果的不确定性

windows编程-系统编程入门_第16张图片

要解决这个问题需要用到互斥锁

互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。

互斥对象包含一个使用数量,一个线程 ID 和一个计数器。

其中线程 ID 用于 标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。

创建互斥对象:调用函数 CreateMutex。

HANDLE CreateMutexW(
In_opt LPSECURITY_ATTRIBUTES lpMutexAttributes, //指向安全属性
In BOOL bInitialOwner, //如果这个值为 TRUE,调用线程会立即成为互斥量的第一个拥有者。
//如果线程成功创建了互斥量并且 bInitialOwner 是 TRUE,线程在互斥量上拥有初始的所有权,意味着它必须最终调用 ReleaseMutex 来释放互斥量。
//如果这个值为 FALSE(如你的例子所示),互斥量在创建时没有拥有者。第一个调用 WaitForSingleObject 或 WaitForMultipleObjects 函数的线程将可以成为互斥量的拥有者(假设互斥量是未锁定的)。
In_opt LPCWSTR lpName //LPCSTR 是指向常量字符串的指针,用于标识互斥量。这个名称对于所有进程都是可见的,可以用于进程间的互斥量访问。
//如果 lpName 是 NULL,互斥量是匿名的,即它没有名字,不能被其他进程直接访问。匿名互斥量只能通过程序内部传递句柄来访问。
);

调用成功,该函数返回所创建的互斥对象的句柄。

使用方法:

请求互斥对象所有权:调用函数 WaitForSingleObject 函数。线程必须主动请求共享对象的所有权才能获得所有权。

释放指定互斥对象的所有权:调用 ReleaseMutex 函数。线程访问共享资源结 束后,线程要主动释放对互斥对象的所有权,使该对象处于已通知状态。  

unsigned WINAPI threadInc(void * arg) 
{
	int i;

	WaitForSingleObject(hMutex, INFINITE);
	for(i=0; i<500000; i++)
		num+=1;
	ReleaseMutex(hMutex);

	return 0;
}

只要是操作了全局变量的地方我们都应该加锁 

 完整代码:

#include 
#include 
#include 

#define NUM_THREAD	50
unsigned WINAPI threadInc(void* arg);
unsigned WINAPI threadDes(void* arg);

long long num = 0;
HANDLE hMutex;

int main(int argc, char* argv[])
{
	HANDLE tHandles[NUM_THREAD];
	int i;

	hMutex = CreateMutex(NULL, FALSE, NULL);
	for (i = 0; i < NUM_THREAD; i++)
	{
		if (i % 2)
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
		else
			tHandles[i] = (HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
	}

	WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
	CloseHandle(hMutex);
	printf("result: %lld \n", num);
	return 0;
}

unsigned WINAPI threadInc(void* arg)
{
	int i;

	WaitForSingleObject(hMutex, INFINITE);//阻塞
	//此时,共享的数据没有线程访问,我们可以访问了
	for (i = 0; i < 500000; i++)
		num += 1;
	ReleaseMutex(hMutex);//表示这一次的互斥量又进入等待状态了,要其他线程检测进入工作

	return 0;
}
unsigned WINAPI threadDes(void* arg)
{
	int i;

	WaitForSingleObject(hMutex, INFINITE);
	for (i = 0; i < 500000; i++)
		num -= 1;
	ReleaseMutex(hMutex);

	return 0;
}

注意:

互斥量一般创建为全局变量,由于hmutex是全局变量,因此它限制上锁时间段的所有全局变量的访问,而num也是全局变量,那么就被限制了 

 2.4 多线程群聊通信实例

windows编程-系统编程入门_第17张图片

2.4.1服务器端

#include 
#include 
#include 
#pragma comment(lib,"ws2_32.lib")
//多线程+socket编程+队列
//1每一个上线的客户端,都创建一个线程去维护
//2将从一个客户端收到的消息转发给其他客户端
//3当某个客户端断开需要处理断开的链接(怎么处理?)


#define MAX_CLI 256
#define MAX_BUF_SIZE 1024

SOCKET  clisockets[MAX_CLI];//用数组表示多个客户端
int clicount = 0;//客户端链接的数量
HANDLE hMutex;//用于上锁的互斥量

SOCKET* removeElement(SOCKET* nums, SOCKET val) {
    int slowIndex = 0;
    for (int fastIndex = 0; fastIndex < clicount; fastIndex++) {
        if (val != nums[fastIndex]) {
            nums[slowIndex++] = nums[fastIndex];
        }
    }
    return nums;
}


void SendMsg(char* msg,SOCKET hClntSock, int len) {
    WaitForSingleObject(hMutex, INFINITE);
    for (int i = 0; i < clicount; i++) {
        if(clisockets[i]!= hClntSock)
        send(clisockets[i], msg, len, 0);
    }
    ReleaseMutex(hMutex);
}
//处理消息的线程
unsigned WINAPI clifunc(void* param) {
    SOCKET hClntSock = *((SOCKET*)param);
    int len = 0;
    char msg[MAX_BUF_SIZE] = {0};
    while (1) {
        len = recv(hClntSock, msg,sizeof(msg),0);
        //if (len != -1) {
        //    sendmsg(msg, len);//将收到的转发给其他客户端,实现多客户端通信
        //}
        //else {
        //    printf("recv error : %d\n", WSAGetLastError());
        //}
        //上面这种写法是否错误
        if (len > 0)
        {
            // 成功接收到数据,将数据发送给所有客户端
            SendMsg(msg, hClntSock, len);
        }
        else if (len == 0)
        {
            // 连接被对方正常关闭
            printf("The client has closed the connection.\n");
            break;
        }
        else
        {
            // recv 函数返回 -1,表示发生了错误
            int errCode = WSAGetLastError();
            printf("recv() failed with error code: %d\n", errCode);
            // 可以根据错误代码进一步处理不同类型的错误
            // 例如,如果是因为客户端意外断开导致的错误,可以做相应处理
            break;
        }
    }
    printf("此时链接数目:%d\n", clicount);
    // 确定是哪一个链接下线
    暴力删除
    //for (int i = 0; i < clicount; i++) {
    //    if (hClntSock == clisockets[i]) {
    //        while (1)i++ < clicount{
    //            clisockets[i] = clisockets[i + 1];
    //        }
    //        break;
    //    }
    //}
    //双指针删除
    WaitForSingleObject(hMutex, INFINITE);
    removeElement(clisockets, hClntSock);
    clicount--;
    ReleaseMutex(hMutex);
    printf("此客户端断开后,剩下的链接数为:%d\n", clicount);
    return 0;
}

int main() {
	//开始创建服务器
	printf("server start !\n");
    HANDLE hThread;//收到消息后立马转发给其他客户端,因此收发是同步的,只需要一个线程即可
	//1
    WORD wVersion;
    WSADATA wsaData;
    int err;
    wVersion = MAKEWORD(1, 1);
    err = WSAStartup(wVersion, &wsaData);
    if (err != 0)
    {
        return err;
    }
    if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
    {
        WSACleanup();
        return -1;
    }

    //创建无名互斥对象
    hMutex = CreateMutex(NULL, FALSE, NULL);
    //server socket
    SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
    if (sockSrv == INVALID_SOCKET) {
        printf("Socket creation failed with error: %d\n" , WSAGetLastError());
        WSACleanup();
        return -1;
    }
    SOCKADDR_IN addrSrv;
    addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    addrSrv.sin_family = AF_INET;
    addrSrv.sin_port = htons(6005);//6000被占用了

    // 绑定套接字到本地IP地址,端口号6000
    if (SOCKET_ERROR == bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)))
    {
        printf("bind errorNum = %d\n", GetLastError());
        return -1;
    }
    if (SOCKET_ERROR == listen(sockSrv, MAX_CLI)) {
        printf("listen errorNum = %d\n", GetLastError());
        return -1;
    }
    printf("start listen!");
    //开始多线程接收处理消息
    SOCKADDR_IN addrcli;
    int len = sizeof(SOCKADDR_IN);
    while (1) {
        SOCKET sockcli = accept(sockSrv, (SOCKADDR*)&addrcli, &len);
        WaitForSingleObject(hMutex, INFINITE);
        clisockets[clicount++] = sockcli;//clicount++;//连接数+1
        ReleaseMutex(hMutex);
        //HANDLE hThread;
        hThread = (HANDLE)_beginthreadex(NULL, 0, clifunc, (void*)&sockcli, 0, NULL);
        printf("connect cli ip:%s\n,num = %d \n", inet_ntoa(addrcli.sin_addr),clicount);//客户端用sock表示,但是信息在addr中

    }
    closesocket(sockSrv);
    system("pause");
    return 0;
}

2.4.2客户端 

#include 
#include 
#include 
#pragma comment(lib,"ws2_32.lib")
//客户端
//1:请求链接上线,发消息
//2:等待服务端的消息
//3:等待自己的下线操作
#define NAME_SIZE 256//当前客户端的名字
#define MAX_BUF_SIZE 1024

char NAME[NAME_SIZE] = "ly:";
char msg[MAX_BUF_SIZE] = { 0 };//由于客户端只针对一个buf进行收发,那么直接定义为全局即可

unsigned WINAPI senfunc(void* param) {
    SOCKET hClntSock = *((SOCKET*)param);
    int len = 0;
    char name_msg[NAME_SIZE+MAX_BUF_SIZE] = { 0 };
    while (1) {
        memset(msg, 0, MAX_BUF_SIZE);
        //char *fgets(char *str, int n, FILE *stream);
        //控制台输入信息到msg中
        fgets(msg,MAX_BUF_SIZE,stdin);
        if (!strcmp(msg, "quit\n") || !strcmp(msg, "QUIT\n")) {
            closesocket(hClntSock);
            exit(0);
        }
        sprintf(name_msg, "%s:%s", NAME, msg);
        //将stdin加上用户名发送给服务器
        len = send(hClntSock, name_msg, sizeof(name_msg), 0);
        if (len > 0)
        {
        }
        else if (len == 0)
        {
            // 连接被对方正常关闭
            printf("The client has closed the connection.\n");
            break;
        }
        else
        {
            // recv 函数返回 -1,表示发生了错误
            int errCode = WSAGetLastError();
            printf("recv() failed with error code: %d\n", errCode);
            // 可以根据错误代码进一步处理不同类型的错误
            // 例如,如果是因为客户端意外断开导致的错误,可以做相应处理
            break;
        }
    }
    return 0;
}

unsigned WINAPI recvfunc(void* param) {
    SOCKET hClntSock = *((SOCKET*)param);
    int len = 0;
    char name_msg[NAME_SIZE+MAX_BUF_SIZE] = { 0 };
    while (1) {
        //接收消息,由于要接收用户名+消息,这里使用name_msg
        memset(name_msg, 0, NAME_SIZE+MAX_BUF_SIZE);

        len =recv(hClntSock, name_msg, sizeof(name_msg), 0);
        if (len > 0)
        {
            name_msg[len] = '\0';
            fputs(name_msg, stdout);
        }
        else if (len == 0)
        {
            // 连接被对方正常关闭
            printf("The client has closed the connection.\n");
            break;
        }
        else
        {
            // recv 函数返回 -1,表示发生了错误
            int errCode = WSAGetLastError();
            printf("recv() failed with error code: %d\n", errCode);
            // 可以根据错误代码进一步处理不同类型的错误
            // 例如,如果是因为客户端意外断开导致的错误,可以做相应处理
            break;
        }
    }
    return 0;
}
//如果你从命令行运行程序
//./myprogram arg1 arg2
//那么:
//argc 将会是 3。
//argv[0] 将会是 "./myprogram"。
//argv[1] 将会是 "arg1"。
//argv[2] 将会是 "arg2"。
int main(int argc,char *argv[]) {
    if (argc != 2) {
        printf("输入参数不正确,必须输入两个参数\n");
        printf("例如:client1.exe  ly\n");
        system("pause");
        return -1;
    }
    //开始创建服务器
    printf("client start !\n");
    sprintf(NAME, "[%s]", argv[1]);//存储name
    //这里和客户端不一样,收发不同步,需要两个独立线程
    HANDLE hThreadrecv, hThreadsend;//客户端有两个线程,一个发送一个接收
    //1
    WORD wVersion;
    WSADATA wsaData;
    int err;
    wVersion = MAKEWORD(1, 1);
    err = WSAStartup(wVersion, &wsaData);
    if (err != 0)
    {
        return err;
    }
    if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
    {
        WSACleanup();
        return -1;
    }
    //建立客户端链接
    SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);
    if (sockCli == INVALID_SOCKET) {
        printf("Socket creation failed with error: %d\n", WSAGetLastError());
        WSACleanup();
        return -1;
    }
    SOCKADDR_IN addrSrv;
    addrSrv.sin_addr.S_un.S_addr = inet_addr("113.54.129.186");
    addrSrv.sin_family = AF_INET;
    addrSrv.sin_port = htons(6005);

    //向服务器发起连接请求
    if (SOCKET_ERROR == connect(sockCli, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR))) {
        printf("Socket connect failed with error: %d\n", WSAGetLastError());
        closesocket(sockCli);
        WSACleanup();
        return -1;
    }
    printf("开始链接服务器!\n");
    hThreadsend = (HANDLE)_beginthreadex(NULL, 0, senfunc, (void*)&sockCli, 0, NULL);
    hThreadrecv = (HANDLE)_beginthreadex(NULL, 0, recvfunc, (void*)&sockCli, 0, NULL);
    WaitForSingleObject(hThreadsend, INFINITE);
    WaitForSingleObject(hThreadrecv, INFINITE);
    closesocket(sockCli);
    system("pause");
    return 0;
}

 2.4.3 运行结果

windows编程-系统编程入门_第18张图片

windows编程-系统编程入门_第19张图片

3.线程同步 

3.1 内核对象 

(1)内核对象

1) Windows中每个内核对象都只是一个内存块,它由操作系统内核分配,并只能由操作系统内核进行访问,应用程序不能在内存中定位这些数据结构并直接更改其内容。这个内存块是一个数据结构,其成员维护着与对象相关的信息。少数成员(安全描述符和使用计数)是所有内核对象都有的,但大多数成员都是不同类型对象特有的。

2)CreateFile
如:file文件对象、event事件对象、process进程、thread线程、iocompletationport完成端口(windows服务器)、mailslot邮槽、mutex互斥量和 registry注册表 等

(2)内核对象的使用计数与生命期

内核对象的所有者是操作系统内核,而非进程。换言之也就是说当进程退出,内核对象不一定会销毁

操作系统内核通过内核对象的使用计数,知道当前有多少个进程正在使用一个特定的内核对象。初次创建内核对象,使用计数为1。当另一个进程获得该内核对象的访问权之后,使用计数加1。如果内核对象的使用计数递减为0,操作系统内核就会销毁该内核对象。也就是说内核对象在当前进程中创建,但是当前进程退出时,内核对象有可能被另外一个进程访问。这时,进程退出只会减少当前进程对引用的所有内核对象的使用计数,而不会减少其他进程对内核对象的使用计数(即使该内核对象由当前进程创建)。那么内核对象的使用计数未递减为0,操作系统内核不会销毁该内核对象。

在Windows操作系统中

1)每当一个进程创建或打开一个内核对象(如事件、互斥量、信号量等)时,该对象的引用计数会增加。

2)当进程通过调用相应的函数(如 CloseHandle)关闭对该内核对象的句柄时,或者当进程退出时,它持有的所有内核对象句柄都会被自动关闭,相应的内核对象的引用计数会减少。

举例如下: 

windows编程-系统编程入门_第20张图片

进程1、进程2都不退出则内核对象ABCD的使用计数为:1,1,2,2;

(3)操作内核对象

Windows提供了一组函数进行操作内核对象。成功调用一个创建内核对象的函数后,会返回一个句柄,它表示了所创建的内核对象,可由进程中的任何线程使用。在32位进程中,句柄是一个32位值,在64位进程中句柄是一个64位值。我们可以使用唯一标识内核对象的句柄,调用内核操作函数对内核对象进行操作

(4)内核对象与其他类型的对象

Windows进程中除了内核对象还有其他类型的对象,比如窗口,菜单,字体等,这些属于用户对象和GDI对象。要区分内核对象与非内核对象,最简单的方式就是查看创建这个对象的函数,几乎所有创建内核对象的函数都有一个允许我们指定安全属性的参数。

(5)注意细节:

  • 一个对象是不是内核对象,通常可以看创建此对象API的参数中是否需要:PSECURITY_ATTRIBUTES 类型(安全属性)的参数。

  • 内核对象只是一个内存块,这块内存位于操作系统内核的地址空间,内存块中存放一个数据结构(此数据结构的成员有如:安全描述符、使用计数等)。

  • 内核对象只是一个内存块,这块内存位于操作系统内核的地址空间,内存块中存放一个数据结构(此数据结构的成员有如:安全描述符、使用计数等)。

windows编程-系统编程入门_第21张图片

(6)CreateThread && CloseHandle()

hThread = CreateThread(… , &threadId);

此时内核对象被创建,其数据结构中的引用计数初始为1(这样理解:只要内核对象被创建,其引用计数被初始化为1),但这里实则发生两件事:创建了一个内核对象和创建线程的函数打开(访问)了此对象,所以内核对象的引用计数加1,这时引用计数就为2了。

CloseHandle(hThread);

系统通过hThread计算出此句柄在句柄表中的索引,然后把那一项处理后标注为空闲可用的项,内核对象的引用计数减1即此时此内核对象的引用计数为1,之后这个线程句柄与创建时产生的内核对象已经没有任何关系了。不能通过hThread句柄去访问内核对象了

 3.2 进程同步-事件对象

事件对象也属于内核对象,它包含以下三个成员: 

● 使用计数;
● 用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值;
● 用于指明该事件处于已通知状态还是未通知状态的布尔值。

事件对象有两种类型:

  • 人工重置的事件对象和自动重置的事件对象。这两种事件对象的区别在于当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。
HANDLE CreateEvent(   
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性   
BOOL bManualReset,   // 复位方式  TRUE 必须用ResetEvent手动复原  FALSE 自动还原为无信号状态
BOOL bInitialState,   // 初始状态   TRUE 初始状态为有信号状态  FALSE 无信号状态
LPCTSTR lpName     //对象名称  NULL  无名的事件对象 
);

使用方法:

1)创建事件对象
调用CreateEvent函数创建或打开一个命名的或匿名的事件对象。
2)设置事件对象状态
调用SetEvent函数把指定的事件对象设置为有信号状态。
3)重置事件对象状态
调用ResetEvent函数把指定的事件对象设置为无信号状态。
4)请求事件对象
线程通过调用WaitForSingleObject函数请求事件对象

使用案例如下:



#if 1
#include 
#include 
#include  
#define STR_LEN		100

unsigned WINAPI NumberOfA(void* arg);
unsigned WINAPI NumberOfOthers(void* arg);

static char str[STR_LEN];
static HANDLE hEvent;

int main(int argc, char* argv[])
{
	HANDLE  hThread1, hThread2;
	fputs("Input string: ", stdout);
	fgets(str, STR_LEN, stdin);
	//NUll 默认的安全符   手动   FALSE 初始状态为无信号状态
	hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
	hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL);
	hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);
	WaitForSingleObject(hThread1, INFINITE);
	WaitForSingleObject(hThread2, INFINITE);
	//直到2个线程执行完之后,再把事件设置为无信号状态
	ResetEvent(hEvent);
	CloseHandle(hEvent);
	system("pause");

	return 0;
}

unsigned WINAPI NumberOfA(void* arg)
{
	int i, cnt = 0;
	//再没有执行fputs("Input string: ", stdout);
	//fgets(str, STR_LEN, stdin);SetEvent(hEvent);之前,卡在
	//WaitForSingleObject
	WaitForSingleObject(hEvent, INFINITE);
	for (i = 0; str[i] != 0; i++)
	{
		if (str[i] == 'A')
			cnt++;
	}
	printf("Num of A: %d \n", cnt);
	return 0;
}
unsigned WINAPI NumberOfOthers(void* arg)
{
	int i, cnt = 0;
	//再没有执行fputs("Input string: ", stdout);
	//fgets(str, STR_LEN, stdin);SetEvent(hEvent);之前,卡在
	//WaitForSingleObject
//	WaitForSingleObject(hEvent, INFINITE);
	for (i = 0; str[i] != 0; i++)
	{
		if (str[i] != 'A')
			cnt++;
	}
	printf("Num of others: %d \n", cnt - 1);
	//把事件对象设置为有信号状态
	SetEvent(hEvent);
	return 0;
}
#endif


// 火车站卖票   A工人     B工人

#include 
#include 
#include  


int iTickets = 100;
HANDLE g_hEvent;

DWORD WINAPI SellTicketA(void* lpParam)
{
	while (1)
	{
		WaitForSingleObject(g_hEvent, INFINITE);
		if (iTickets > 0)
		{
			Sleep(1);
			iTickets--;
			printf("A remain %d\n", iTickets);
		}
		else
		{
			break;
		}
		SetEvent(g_hEvent);
	}
	return 0;
}

DWORD WINAPI SellTicketB(void* lpParam)
{
	while (1)
	{
		WaitForSingleObject(g_hEvent, INFINITE);
		if (iTickets > 0)
		{
			Sleep(1);
			iTickets--;
			printf("B remain %d\n", iTickets);
		}
		else
		{
			break;
		}
		SetEvent(g_hEvent);
	}
	return 0;//0  内核对象被销毁
}

int main()
{
	HANDLE hThreadA, hThreadB;
	hThreadA = CreateThread(NULL, 0 ,SellTicketA, NULL, 0, 0);// 2

	hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, 0);
	CloseHandle(hThreadA);  //1
	CloseHandle(hThreadB);
	g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
	SetEvent(g_hEvent);
	Sleep(4000);
	CloseHandle(g_hEvent);
	system("pause");
	return 0;
}

 互斥量和事件对象的区别:

互斥量(Mutex):

互斥量用于防止多个线程并发访问同一共享资源,以保证数据执行的安全性。例如,假设有两个线程 A 和 B,它们同时写入同一个文件。此时,可以使用互斥量保证一次只有一个线程能够写入文件。具体做法是当线程 A 开始写入文件的时候,就获取互斥量,阻止其他线程访问文件;当线程 A 完成写入操作后,释放互斥量,允许其他线程访问文件。

HANDLE hMutex = CreateMutex(NULL, FALSE, "testMutex");

for (int i = 0; i < 100; i++)
{
    WaitForSingleObject(hMutex, INFINITE);
    // 执行需要互斥的操作
    ReleaseMutex(hMutex);
}

事件对象(Event):

事件对象通常用于线程间的信号通知。例如,如果线程 A 需要线程 B 完成一项操作后再继续执行,可以使用事件对象制定信号。具体做法是线程 A 在合适的地方等待事件,而线程 B 在完成特定操作后设定事件。当事件设定后,线程 A 就会停止等待,继续执行。

HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, "testEvent");

//线程A
WaitForSingleObject(hEvent, INFINITE);
// 执行接下来的操作

//线程B
// 完成一项操作
SetEvent(hEvent);

3.3 信号量 

信号量的规则如下:
(1)如果当前资源计数大于0,那么信号量处于触发状态(有信号状态),表示有可用资源。
(2)如果当前资源计数等于0,那么信号量属于未触发状态(无信号状态),表示没有可用资源。
(3)系统绝对不会让当前资源计数变为负数
(4)当前资源计数绝对不会大于最大资源计数 

使用流程:

4.进程操作

4.1 创建进程-CreateProcessW

 CreateProcessW

CreateProcessW(

//这是你想要运行的程序的完整路径和文件名。比如说,如果你想运行记事本程序,你就可以在这里写上记事本的完整路径。
    _In_opt_ LPCWSTR lpApplicationName,

//这里你可以写一些命令行参数。比如,如果你想打开记事本并立即打开一个特定的文件,你就可以在这里指定那个文件的路径。
_Inout_opt_ LPWSTR lpCommandLine,


//这两个参数涉及到安全性和权限。它们决定了新进程是否可以“继承”(使用)一些特定的句柄(就像是文件或者设备的指针)。通常,如果你不需要特别处理安全性或权限问题,可以把它们设置为NULL。
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,

//这个参数是一个简单的是或否(TRUE或FALSE)。如果你设置为TRUE,新进程可以使用当前进程的句柄;如果设置为FALSE,它就不能用。
_In_ BOOL bInheritHandles,

//这个参数用来控制新进程的一些行为,比如它应该以怎样的优先级运行。例如,CREATE_NEW_CONSOLE 这个标志会为新进程创建一个新的控制台窗口。
_In_ DWORD dwCreationFlags,

//这是关于新进程运行环境的设置。如果你不需要特别的环境设置,可以把它设置为NULL,这样新进程就会使用当前进程的环境。
_In_opt_ LPVOID lpEnvironment,

//这里你可以指定新进程的工作目录,就是新进程“认为”自己在哪个文件夹中开始运行。
_In_opt_ LPCWSTR lpCurrentDirectory,


//这个参数包含了一些额外的信息,比如新进程的窗口应该长什么样子。你需要先设置一个 STARTUPINFO 结构体,然后把它传给这个参数。
_In_ LPSTARTUPINFOW lpStartupInfo,

//这个参数是用来接收新进程的一些信息的,比如它的进程ID和线程ID。你需要先创建一个 PROCESS_INFORMATION 结构体,然后函数会填充这个结构体。
_Out_ LPPROCESS_INFORMATION lpProcessInformation // 该 结构接收有关新进程的标识//信息
    );

创建进程打开一个网页:

//创建一个用谷歌浏览器打开百度
#include 
#include 
#include 

void RunExe()
{
	STARTUPINFO strStartupInfo;
	memset(&strStartupInfo, 0, sizeof(strStartupInfo));
	strStartupInfo.cb = sizeof(strStartupInfo);

	PROCESS_INFORMATION szProcessInformation;
	memset(&szProcessInformation, 0, sizeof(szProcessInformation));
	TCHAR szCommandLine[] = _T("\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\" http://www.baidu.com");

	//TCHAR szCommandLine[] = _T("\"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe");

	int iRet = CreateProcess(
		NULL,
		szCommandLine,
		NULL,
		NULL,
		false,
		CREATE_NEW_CONSOLE,
		NULL,
		NULL,
		&strStartupInfo,
		&szProcessInformation
	);
	if (iRet)
	{
		//创建成功
		printf_s("Create Success iRet = %d\n", iRet);
		WaitForSingleObject(szProcessInformation.hProcess, 3000);
//记得关闭句柄
		CloseHandle(szProcessInformation.hProcess);
		CloseHandle(szProcessInformation.hThread);
		szProcessInformation.dwProcessId = 0;
		szProcessInformation.dwThreadId = 0;
		szProcessInformation.hThread = NULL;
		szProcessInformation.hProcess = NULL;
	}
	else
	{
		printf_s("Create Success iRet = %d\n", iRet);
		printf_s("errorcode = %d\n", GetLastError());

	}
}



int main()
{
	printf("This is Process\n");
	RunExe();
	system("pause");
	return 0;
}

 效果:

windows编程-系统编程入门_第22张图片

 5.进程通信

  • 1. socket编程 IP和端口 server client
  • 2. 剪切板 剪切板的内核对象
  • 3. 邮槽 邮槽的内核对象
  • 4. 匿名管道(无名管道)
  • 5. 命名管道
  • 6. Copy_data findwindows wm_copydata 很多书籍都没有 消息Sendmessage

 5.1 网络通信

参考如下,不多赘述 

写文章-CSDN创作中心

5.2 剪切板

1.调用MFC控件

windows编程-系统编程入门_第23张图片

2.编写按键功能实现编辑框通信 

代码如下:
发送端:

void CMFCApplication2Dlg::OnBnClickedSendBuuton()
{
	// 1 打开剪切板
	if (OpenClipboard())
	{
		//2 清空剪切板
		EmptyClipboard();
		char* szSendBuf;
		//3 获取编辑框的内容
		CStringW strSendW;
		GetDlgItemText(IDC_SEND_EDIT, strSendW);

		CStringA strSend = (CStringA)strSendW;

		//4 分配一个内存对象,内存对象的句柄就是hClip
		HANDLE hClip = GlobalAlloc(GMEM_MOVEABLE, strSend.GetLength() + 1);
		//5 将剪切板句柄加锁
		szSendBuf = (char*)GlobalLock(hClip);

		strcpy_s(szSendBuf, sizeof(szSendBuf), strSend);

		TRACE("szSendBuf = %s", szSendBuf);
		GlobalUnlock(hClip);
		//6 将数据放入剪切板
		SetClipboardData(CF_TEXT, hClip);
		//关闭剪切板
		CloseClipboard();
	}
}

接收端:

void CMFCApplication2Dlg::OnBnClickedRecvButton()
{
	if (OpenClipboard())
	{
		//确认剪切板是否可用
		if (IsClipboardFormatAvailable(CF_TEXT))
		{
			HANDLE hClip;
			char* pBuf;
			//向剪切板要数据
			hClip = GetClipboardData(CF_TEXT);
			pBuf = (char*)GlobalLock(hClip);
			USES_CONVERSION;
			LPCWSTR strBuf = A2W(pBuf);
			GlobalUnlock(hClip);
			SetDlgItemText(IDC_RECV_EDIT, strBuf);
		}
		CloseClipboard();
	}
}

 效果:

windows编程-系统编程入门_第24张图片

 5.3 邮槽Mailslot

 5.4 匿名管道

 

 5.5 有名管道

参考博客

5.1 进程、线程基础知识 | 小林coding

【Windows线程开发】线程基础_windows创建线程-CSDN博客

[C++] CreateThread() 与 _beginthread() 的区别__beginthread 和 createthread 区别-CSDN博客

(C/C++)Windows编程-CSDN博客

你可能感兴趣的:(windows)