前言
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
——百度百科
正文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <stdio.h> #include <windows.h>
int main(){ while (true){ printf("hello\n"); Sleep(3000); printf("world!\n"); Sleep(5000); printf("hhhh\n"); Sleep(7000); }
return 0; }
|
像上述代码中,我们依次延时输出,但实际走一次要花费3+5+7秒,他是一个顺序执行的过程,而有的时候更希望一个程序,能够同时进行,减少开销。
线程基本概念
进程:是分配资源的基本单位
线程:是cpu调度和分配的基本单位
线程是进程中产生的一个执行单元,一个进程中往往会有多个进程并行运行。
抽象的说:在流水线中,进程表示车间,线程表示工人。
狭义角度,进程就是一个正在运行的程序
广义角度,进程是处于执行期间的程序以及它所包含的资源(如打开的文件、挂起的信号、进程状态、地址空间等)。
为什么要使用多线程
- 避免阻塞
- 单个进程只有一个主线程,当主线程阻塞的时候,整个进程也就处于阻塞状态,无法在处理其他任务
- 避免cpu空转
- 应用程序经常会涉及到RPC,数据库访问,磁盘IO等操作,这些操作的速度远比cpu慢,在处理这些响应的时候,cpu只能原地等待,导致单线程的程序性能低下
- 提升效率
- 一个进程要独立拥有4GB的虚拟地址空间,而多线程可以共享同一块地址空间,线程之间的切换要比进程之间的切换来的快。
CreateThread
CreateThread是微软封装在windows api中提供建立新的线程的函数,该函数在主线程的基础上创建一个新线程。线程终止运行后,线程对象仍然在系统中,必须通过CloseHandle
函数关闭线程对象
1 2 3 4 5 6 7 8 9 10 11 12
| WINBASEAPI _Ret_maybenull_ HANDLE WINAPI CreateThread( _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ SIZE_T dwStackSize, _In_ LPTHREAD_START_ROUTINE lpStartAddress, _In_opt_ __drv_aliasesMem LPVOID lpParameter, _In_ DWORD dwCreationFlags, _Out_opt_ LPDWORD lpThreadId );
|
这是转到定义的结构,也可以在函数上按f1跳转到文档。
英语不好翻译就完事。。虽然机译不一定读的通顺
与其功能相近的还有_beginthread
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| uintptr_t _beginthread( void( __cdecl *start_address )( void * ), unsigned stack_size, void *arglist ); uintptr_t _beginthread( void( __clrcall *start_address )( void * ), unsigned stack_size, void *arglist ); uintptr_t _beginthreadex( void *security, unsigned stack_size, unsigned ( __stdcall *start_address )( void * ), void *arglist, unsigned initflag, unsigned *thrdaddr ); uintptr_t _beginthreadex( void *security, unsigned stack_size, unsigned ( __clrcall *start_address )( void * ), void *arglist, unsigned initflag, unsigned *thrdaddr );
|
使用_beginthreadex
需包含头文件<process.h>
再次补充,函数名后的ex是补充拓展的意思
1 2 3 4
| void printW(int _x){ printf("world!n"); Sleep(_x*1000); }
|
先把之前的封装成函数。
然后传参给_beginthreadex
1
| _beginthreadex(NULL,0,printH,(void*)&x, 0, &xId);
|
不过会有点问题,因为传递的函数类型不太一样。
1
| _In_ _beginthreadex_proc_type _StartAddress,
|
1
| typedef unsigned (__stdcall* _beginthreadex_proc_type)(void*);
|
依次转到定义之后会发现,本质是一个unsinged stdcall的函数,不过形参居然要求是void*就比较蛋疼,还得转换解引用取值。。
至于__stdcall,写起来麻烦,
其实内置了一些宏,都可以替换,比较常见的可能是WINAPI
,就先用着
1 2 3 4 5 6 7 8 9 10
| unsigned WINAPI printH(void *_x){ int n = *((int *)_x);
for (int i = 0; i < n; i++){ printf("hello\n"); Sleep(n * 1000); }
return 0; }
|
额除了void都要返回值,虽然这里也不需要什么特殊的,就随便返回一个。
依次照葫芦画瓢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| unsigned WINAPI printH(void *_x){ int n = *((int *)_x);
for (int i = 0; i < n; i++){ printf("hello\n"); Sleep(n * 1000); }
return 0; }
unsigned WINAPI printW(void *_x){ int n = *((int *)_x); for (int i = 0; i < n; i++){ printf("world!\n"); Sleep(n * 1000); }
return 0; }
unsigned WINAPI printA(void *_x){ int n = *((int *)_x); for (int i = 0; i < n; i++){ printf("ahahahaha!\n"); Sleep(n * 1000); }
return 0; }
|
然后就是创建线程
1 2 3 4 5 6 7 8 9 10
| int main(){ int x = 3, y = 5, z = 7;
unsigned int xId, yId, zId; _beginthreadex(NULL, 0, printH, (void *)&x, 0, &xId); _beginthreadex(NULL, 0, printW, (void *)&y, 0, &yId); _beginthreadex(NULL, 0, printA, (void *)&z, 0, &zId);
return 0; }
|
直接这样写其实一次就运行完了,因为顺序执行之后return 0,主线程main结束了,线程就g了,所以要想办法阻塞主线程,最直接就是给主线程也来个延时。
线程就是可以同步进行,不过这个顺序似乎就第一次正常的,后面的好像全看心情。虽然函数设置的延时不一样,但是就算统一了延时效果也差不多。毕竟这个是系统自己处理的。
可以看下进程号,应该说pid
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <stdio.h> #include <windows.h> #include <process.h>
DWORD WINAPI ThreadFun(LPVOID p){ int imym = *((int *)p); printf("我是子线程,pid = %d, imym = %d\n", GetCurrentThreadId(), imym); return 0; }
int main(){ printf("main start!\n");
HANDLE hThread; DWORD dwThreadId; int m = 100;
hThread = CreateThread(NULL, 0, ThreadFun, &m, 0, &dwThreadId); printf("我是主线程:pid = %d\n", GetCurrentThreadId()); CloseHandle(hThread); Sleep(20000);
return 0; }
|
这个大致看看,现在可能用处不大。
简单多线程示例
内核对象
- 内核对象通过API来创建,每个内核对象是一个数据结构,它对应一块内存,由操作系统内核分配,且只能由操作系统内核访问。在此数据结构中少数成员如安全描述符和使用计数是所有对象都有的,但是其他大多数成员都是不同类型的对象特有的。内核对象的数据结构只能由操作系统提供的API访问,应用程序在内存中不能访问。调用创建内核对象的函数后,该函数会返回一个句柄,它标识了所创建的对象,可以由进程的任何线程使用。
主线程和子线程的声明周期
在最前面举例线程的时候,我们创建了三个子线程,但是子线程的执行顺序是不确定的。
而主线程main,我们当时还加了sleep延时去观察。
那么为什么要延时,当然是要给子线程运行的时间,比较子线程内分别也延时输出。
最重要的阻塞在system("pause");
上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <stdio.h> #include <windows.h> #include <process.h>
DWORD WINAPI ThreadFun(LPVOID p){ int imym = *((int *)p);
for (int i = 0; i < imym; i++){ printf("子线程\n"); Sleep(2000); } return 0; }
int main(){ printf("main start!\n");
HANDLE hThread; DWORD dwThreadId; int m = 10;
hThread = CreateThread(NULL, 0, ThreadFun, &m, 0, &dwThreadId); CloseHandle(hThread); system("pause"); return 0; }
|
可以看到,当我们没有按任意键的时候,子线程还能持续。
可当注释掉之后,他甚至都没进入到子线程就结束了。
初步结论:main函数结束后,整个程序的进程终止,同时结束掉其所包含的所有线程
但不论是通过system("pause");
还是Sleep
都不是很好的解决方法。
WaitForSingleObject
1 2 3 4
| WaitForSingleObject( _In_ HANDLE hHandle, _In_ DWORD dwMilliseconds );
|
简单解释就是等待一个内核对象变为已通知状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| DWORD WINAPI ThreadFun(LPVOID p){ int imym = *((int *)p);
for (int i = 0; i < imym; i++){ printf("子线程\n"); Sleep(2000); }
return 0; }
int main(){ printf("main start!\n");
HANDLE hThread; DWORD dwThreadId; int m = 10; int wr;
hThread = CreateThread(NULL, 0, ThreadFun, &m, 0, &dwThreadId); if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED){ printf("thread wait error\n"); return -1; } CloseHandle(hThread); system("pause");
return 0; }
|
可以看到他会先阻塞在waitforsingleobject那边,等待线程先进行完毕,然后再去执行system的函数。
与我们之前单纯用system阻塞有明显区别。
可以在if前后加个打印区分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| int main(){ printf("main start!\n");
HANDLE hThread; DWORD dwThreadId; int m = 10; int wr;
hThread = CreateThread(NULL, 0, ThreadFun, &m, 0, &dwThreadId); printf("begin!\n");
if ((wr = WaitForSingleObject(hThread, INFINITE)) == WAIT_FAILED){ printf("thread wait error\n"); return -1; } CloseHandle(hThread); printf("end!\n"); system("pause");
return 0; }
|
印证符合描述,他的确等待线程执行完毕。
WaitForSingleObject(hThread, INFINITE))
也就是线程阻塞在这里,等待线程结束后,才会顺便结束线程。
已通知状态,就是说线程执行完毕之后的状态。
1 2 3 4 5 6 7
| WaitForMultipleObjects( _In_ DWORD nCount, _In_reads_(nCount) CONST HANDLE* lpHandles, _In_ BOOL bWaitAll, _In_ DWORD dwMilliseconds );
|
#define INFINITE 0xFFFFFFFF // Infinite timeout
那么当出现多个线程对象的时候,肯定不会说挨个等着。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <stdio.h> #include <windows.h> #include <process.h>
#define NUM_THREAD 50 long long num = 0;
unsigned WINAPI threadInc(void *p){ for (int i = 0; i < 500000; i++){ num += 1; }
return 0; }
unsigned WINAPI threadDes(void *p){ for (int i = 0; i < 500000; i++){ num -= 1; }
return 0; }
int main(){ printf("main start!\n");
HANDLE tHandles[NUM_THREAD];
printf("sizeof long long : %d\n", sizeof(long long));
for (int 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); printf("result: %lld\n", num);
return 0; }
|
跑的时候会发现值是不固定的。
常规思维中,全局变量,通过两个线程挨个调用,会觉得最后的值应该就是固定的。
但是线程是由cpu控制的,而全局变量还是存在与内存之上,于速度而言,肯定是cpu更快,所以当多个线程工作的时候,全局变量被线程取出使用,但是可能没等到改变的值传回全局变量,线程2就启动了,线程2改变的是线程1还没来得及放入的数据,如此反复,谁也不能保证最后到底算真正数学上的加减了几次。
虽然知道了这种特性,但是有的时候就是需要线程之间不要过分干预,就引出互斥对象
互斥对象
互斥对象同属于内核对象,它能保证线程拥有对单个资源的互斥访问权。
创建互斥对象使用:CreatrMutex
.
1 2 3 4 5 6 7 8 9 10 11 12
| HANDLE WINAPI CreateMutexW( _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, _In_ BOOL bInitialOwner, _In_opt_ LPCWSTR lpName ); #ifdef UNICODE #define CreateMutex CreateMutexW #else #define CreateMutex CreateMutexA #endif
|
额,可以看到好像会根据编码环境去做一些调整,不过问题不大,反正看得出来。然后这种线程创建成功返回的都是句柄。
请求互斥对象:WaitForSingleObject
,线程必须主动请求共享对象的所有权才能获得所有权。
是否互斥对象的所有权:ReleaseMutex
,线程访问共享资源结束后,线程要主动释放对互斥对象的所有权,使该对象处于已通知状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #include <stdio.h> #include <windows.h> #include <process.h>
#define NUM_THREAD 50 long long num = 0; HANDLE hMutex;
unsigned WINAPI threadInc(void *p){ WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < 500000; i++){ num += 1; } ReleaseMutex(hMutex); return 0; }
unsigned WINAPI threadDes(void *p){ WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < 500000; i++){ num -= 1; } ReleaseMutex(hMutex); return 0; }
int main(){ printf("main start!\n");
HANDLE tHandles[NUM_THREAD];
hMutex = CreateMutex(NULL, FALSE, NULL); for (int 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; }
|
相较于之前改动不大,关键在于:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| unsigned WINAPI threadInc(void *p){ WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < 500000; i++){ num += 1; } ReleaseMutex(hMutex); return 0; }
unsigned WINAPI threadDes(void *p){ WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < 500000; i++){ num -= 1; } ReleaseMutex(hMutex); return 0; }
|
两个线程在运行的时候,他要等待其中一个线程先ReleaseMutex
。其中谁先开始仍然是随机的。
可以看到这次得到理想的值了。
这个互斥对象的意义就是在线程运行的时候,让他处于等待通知状态,执行完毕之后在释放所有权,这样可以避免多个线程快速对内存的操作的影响
socket+互斥线程同步
简单就是聊天服务器和客户端的low low版本
- socket bind listen accept必不可少
- c/s的模式
- 上线的客户端,通过服务器新起一个线程维护管理
- 收到的消息如何发送给客户端
- 当客户端下线后,需要断开这个线程的连接
服务端
前面的代码其实都差不多,直接copy以前的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| int main(){ printf("Server start!\n");
WORD wVersionRequested; WSADATA wsaData; int err; HANDLE hThread; wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData); if (err != 0){ return err; } if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){ WSACleanup(); return -1; }
hMutex = CreateMutex(NULL, FALSE, NULL);
SOCKET sockSer = socket(PF_INET, SOCK_STREAM, 0); if (INVALID_SOCKET == sockSer){ ErrorHanding("socket error!"); }
SOCKADDR_IN addrSer; addrSer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); addrSer.sin_family = AF_INET; addrSer.sin_port = htons(6000);
if (SOCKET_ERROR == bind(sockSer, (SOCKADDR *)&addrSer, sizeof(SOCKADDR))){ ErrorHanding("bind error"); }
if (SOCKET_ERROR == listen(sockSer, 5)){ ErrorHanding("listen error"); } printf("start listen!\n");
SOCKADDR_IN addrCli; int len = sizeof(SOCKADDR_IN);
while (true){ SOCKET sockConn = accept(sockSer, (SOCKADDR *)&addrCli, &len); clnSocks[clntCnt++] = sockConn;
hThread = (HWND)_beginthreadex(NULL, 0, HandleCln, (void *)&sockConn, 0, NULL); printf("Connect Num = %d, client ip: %s\n",clntCnt, inet_ntoa(addrCli.sin_addr)); } return 0; }
|
循环接收消息上处理会有不同,毕竟要处理多个客户端,还有队列中下线某台,后面的移上来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| void SendMsg(char *szMsg, int iLen){ for (int i = 0; i < clntCnt; i++){ send(clnSocks[i], szMsg, iLen, 0); } }
unsigned WINAPI HandleCln(void *arg){
SOCKET hClntSock = *((SOCKET*)arg); int iLen = 0; char szMsg[MAX_BUF_SIZE] = { 0 };
while (true){ iLen = recv(hClntSock, szMsg, sizeof(szMsg), 0); if (iLen != -1){ SendMsg(szMsg, iLen); } else{ break; } }
printf("此时连接数:%d\n", clntCnt); for (int i = 0; i < clntCnt; i++){ if (hClntSock == clnSocks[i]){ while (i++ < clntCnt){ clnSocks[i] = clnSocks[i + 1]; } break; } }
clntCnt--; printf("断开后,此时连接数为:%d\n", clntCnt); closesocket(hClntSock);
return 0; }
|
线程和网络编程和队列都有了,线程同步的问题还没解决,就是给他上锁。
上锁的核心就是线程对全局变量的处理太快,产生偏差值的问题,所以我们在循环处理中对全局变量做改变的前后加上锁。
1 2 3
| WaitForSingleObject(hMutex, INFINITE); clnSocks[clntCnt++] = sockConn; ReleaseMutex(hMutex);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| void SendMsg(char *szMsg, int iLen){
WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < clntCnt; i++){ send(clnSocks[i], szMsg, iLen, 0); } ReleaseMutex(hMutex); }
unsigned WINAPI HandleCln(void *arg){
SOCKET hClntSock = *((SOCKET*)arg); int iLen = 0; char szMsg[MAX_BUF_SIZE] = { 0 };
while (true){ iLen = recv(hClntSock, szMsg, sizeof(szMsg), 0); if (iLen != -1){ SendMsg(szMsg, iLen); } else{ break; } }
printf("此时连接数:%d\n", clntCnt); WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < clntCnt; i++){ if (hClntSock == clnSocks[i]){ while (i++ < clntCnt){ clnSocks[i] = clnSocks[i + 1]; } break; } }
clntCnt--; printf("断开后,此时连接数为:%d\n", clntCnt); ReleaseMutex(hMutex); closesocket(hClntSock);
return 0; }
|
完整的服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
| #include <stdio.h> #include <windows.h> #include <process.h> #pragma comment(lib,"ws2_32.lib")
#define MAX_CLNT 256 #define MAX_BUF_SIZE 1024
SOCKET clnSocks[MAX_CLNT]; int clntCnt = 0;
HANDLE hMutex;
void ErrorHanding(const char *_msg){ fputs(_msg, stderr); fputc('\n', stderr); exit(1); }
void SendMsg(char *szMsg, int iLen){
WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < clntCnt; i++){ send(clnSocks[i], szMsg, iLen, 0); } ReleaseMutex(hMutex); }
unsigned WINAPI HandleCln(void *arg){
SOCKET hClntSock = *((SOCKET*)arg); int iLen = 0; char szMsg[MAX_BUF_SIZE] = { 0 };
while (true){ iLen = recv(hClntSock, szMsg, sizeof(szMsg), 0); if (iLen != -1){ SendMsg(szMsg, iLen); } else{ break; } }
printf("此时连接数:%d\n", clntCnt); WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < clntCnt; i++){ if (hClntSock == clnSocks[i]){ while (i++ < clntCnt){ clnSocks[i] = clnSocks[i + 1]; } break; } }
clntCnt--; printf("断开后,此时连接数为:%d\n", clntCnt); ReleaseMutex(hMutex); closesocket(hClntSock);
return 0; }
int main(){ printf("Server start!\n");
WORD wVersionRequested; WSADATA wsaData; int err; HANDLE hThread; wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData); if (err != 0){ return err; } if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){ WSACleanup(); return -1; }
hMutex = CreateMutex(NULL, FALSE, NULL);
SOCKET sockSer = socket(PF_INET, SOCK_STREAM, 0); if (INVALID_SOCKET == sockSer){ ErrorHanding("socket error!"); }
SOCKADDR_IN addrSer; addrSer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); addrSer.sin_family = AF_INET; addrSer.sin_port = htons(6000);
if (SOCKET_ERROR == bind(sockSer, (SOCKADDR *)&addrSer, sizeof(SOCKADDR))){ ErrorHanding("bind error"); }
if (SOCKET_ERROR == listen(sockSer, 5)){ ErrorHanding("listen error"); } printf("start listen!\n");
SOCKADDR_IN addrCli; int len = sizeof(SOCKADDR_IN);
while (true){ SOCKET sockConn = accept(sockSer, (SOCKADDR *)&addrCli, &len); WaitForSingleObject(hMutex, INFINITE); clnSocks[clntCnt++] = sockConn; ReleaseMutex(hMutex);
hThread = (HWND)_beginthreadex(NULL, 0, HandleCln, (void *)&sockConn, 0, NULL); printf("Connect Num = %d, client ip: %s\n",clntCnt, inet_ntoa(addrCli.sin_addr)); } return 0; }
|
客户端
常用的框架就很自然的copy
1 2 3 4 5 6 7 8 9 10 11
| #include <stdio.h> #include <windows.h> #include <process.h> #pragma comment(lib,"ws2_32.lib")
int main(){ printf("Client start!\n");
return 0; }
|
客户端要做的事情
- 请求连接上线,发送给客户端
- 等待服务端消息
- 等待用户自己下线消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| #include <stdio.h> #include <windows.h> #include <process.h> #pragma comment(lib,"ws2_32.lib")
#define NAME_SIZE 256 #define MAX_BUF_SIZE 1024
char szName[NAME_SIZE] = "[DEFAULT]"; char szMsg[MAX_BUF_SIZE];
void ErrorHanding(const char *_msg){ fputs(_msg, stderr); fputc('\n', stderr); exit(1); }
int main(int argc, char *argv[]){ printf("Client start!\n");
if (argc != 2){ printf("必须以命令行启动,且输入两个参数,包括昵称!\n"); printf("例如: MyThreadClient.exe name"); system("pause"); return -1; }
WORD wVersionRequested; WSADATA wsaData; int err; SOCKET hSock; SOCKADDR_IN serAdr; HANDLE hSendThread; HANDLE hRecvThread; wVersionRequested = MAKEWORD(2, 2); err = WSAStartup(wVersionRequested, &wsaData); if (err != 0){ return err; } if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){ WSACleanup(); return -1; } SOCKET sockSer = socket(PF_INET, SOCK_STREAM, 0); if (INVALID_SOCKET == sockSer){ ErrorHanding("socket error!"); }
memset(&serAdr, 0, sizeof(serAdr)); serAdr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); serAdr.sin_family = AF_INET; serAdr.sin_port = htons(6000);
return 0; }
|
大部分都是照搬的,在客户端的使用上,用了之前main自带的参数做了命令行启动的效果。
完整的客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
| #include <stdio.h> #include <windows.h> #include <process.h> #pragma comment(lib,"ws2_32.lib")
#define NAME_SIZE 256 #define MAX_BUF_SIZE 1024
char szName[NAME_SIZE] = "[DEFAULT]"; char szMsg[MAX_BUF_SIZE];
void ErrorHanding(const char *_msg){ fputs(_msg, stderr); fputc('\n', stderr); exit(1); }
unsigned WINAPI SendMsg(void *arg){ SOCKET hClntSock = *((SOCKET *)arg); char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 };
while (true){ memset(szMsg, 0, MAX_BUF_SIZE); fgets(szMsg, MAX_BUF_SIZE, stdin);
if (!strcmp(szMsg, "Q\n") || !strcmp(szMsg, "q\n")){ closesocket(hClntSock); exit(0); }
sprintf(szNameMsg, "%s %s", szName, szMsg); send(hClntSock, szNameMsg, strlen(szNameMsg), 0); }
return 0; }
unsigned WINAPI RecvMsg(void *arg){ SOCKET hClntSock = *((SOCKET *)arg); char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 }; int iLen = 0;
while (true){ memset(szNameMsg, 0, NAME_SIZE + MAX_BUF_SIZE); iLen = recv(hClntSock, szNameMsg, sizeof(szNameMsg), 0);
if (iLen == -1){ return 2; }
szNameMsg[iLen] = 0; fputs(szNameMsg, stdout); }
}
int main(int argc, char *argv[]){ printf("Client start!\n");
if (argc != 2){ printf("必须以命令行启动,且输入两个参数,包括昵称!\n"); printf("例如: MyThreadClient.exe name"); system("pause"); return -1; } sprintf(szName, "[%s]:", argv[1]);
WORD wVersionRequested; WSADATA wsaData; int err; SOCKADDR_IN serAdr; HANDLE hSendThread; HANDLE hRecvThread; wVersionRequested = MAKEWORD(2, 2); err = WSAStartup(wVersionRequested, &wsaData); if (err != 0){ return err; } if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2){ WSACleanup(); return -1; } SOCKET sockSer = socket(PF_INET, SOCK_STREAM, 0); if (INVALID_SOCKET == sockSer){ ErrorHanding("socket error!"); }
memset(&serAdr, 0, sizeof(serAdr)); serAdr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); serAdr.sin_family = AF_INET; serAdr.sin_port = htons(6000);
if (connect(sockSer, (SOCKADDR *)&serAdr, sizeof(serAdr)) == SOCKET_ERROR){ ErrorHanding("connect error!"); }
hSendThread = (HANDLE)_beginthreadex(NULL, 0, SendMsg, (void *)&sockSer, 0, NULL); hRecvThread = (HANDLE)_beginthreadex(NULL, 0, RecvMsg, (void *)&sockSer, 0, NULL);
WaitForSingleObject(hSendThread, INFINITE); WaitForSingleObject(hRecvThread, INFINITE);
closesocket(sockSer); WSACleanup();
return 0; }
|
测试
老样子先跑服务端在跑客户端
ok,成功启用,没啥问题。本地回环的设置是这样。
然后多启动一个,也能跑出来。
然后客户端发消息给服务器
服务器接收到之后转发,可以看到客户端同样是接收到了先前发送的消息
再开一台
实际效果都差不多,就是达到一个简易版的聊天室。
关闭其中一台会看到连接数少了。
然后用我们之前写的方法,输入的q或者Q同样代表退出
效果都是没问题的。
值得一提的是,也是小缺点,就是客户端发送的消息,服务器广播会又发下来,简单来说就是客户端a发的消息,会被服务器广播,然后客户a又收到了自己发的消息。
线程同步-事件对象
前面整过互斥对象了,用CreatrMutex
创建对象,作用就是变相的上锁,让线程操作内存的时候有序的进行。
而事件对象也属于内核对象,它有三个成员
- 使用计数
- 用于指明事件是一个自动重置的事件还是一个人工重置的事件,用布尔值表示
- 用于指明该事件处于已通知状态还是未通知状态,同用布尔值表示
事件对象的两种类型
- 人工重置的事件对象
- 自动重置的事件对象
事件对象的方法
- 创建-调用
CreateEvent
函数创建或者打开一个命名的或者匿名的事件对象
- 设置状态-调用
SetEvent
函数把指定的事件对象设置为有信号状态
- 重置状态-调用
ResetEvent
函数把指定的事件对象设置为无信号状态
- 请求事件对象-调用
WaitForSingleObject
函数请求事件对象
1 2 3 4 5 6 7 8 9 10 11 12 13
| HANDLE WINAPI CreateEventW( _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, _In_ BOOL bManualReset, _In_ BOOL bInitialState, _In_opt_ LPCWSTR lpName ); #ifdef UNICODE #define CreateEvent CreateEventW #else #define CreateEvent CreateEventA #endif
|
反正这些要么在编译器里转到定义,要么上文档翻译一下看看大致作用。
用线程去统计字符串
- 一个统计字符串含A
- 一个统计非A
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| #include <stdio.h> #include <windows.h> #include <process.h>
char str[100]; HANDLE hEvent;
unsigned WINAPI NumberOfA(void *arg){ int count = 0;
WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; str[i] != 0; i++){ if (str[i] == 'A') count++; } printf("A count number = %d\n", count);
return 0; }
unsigned WINAPI NumberOfOthers(void *arg){ int count = 0;
WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; str[i] != 0; i++){ if (str[i] != 'A') count++; } printf("others char count number = %d\n", count);
return 0; }
int main(){ HANDLE hThread1, hThread2; fputs("Pleas Input string:\n",stdout); fgets(str, 100, stdin);
hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL); hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE);
return 0; }
|
统计也没问题,其它字符串看似多一个,是因为统计了字符串末尾的0。
如果不想输出这个末尾的0,就改一下就行
1 2 3 4 5 6 7 8 9 10 11 12
| unsigned WINAPI NumberOfOthers(void *arg){ int count = 0;
WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; str[i] != 0; i++){ if (str[i] != 'A') count++; } printf("others char count number = %d\n", count - 1);
return 0; }
|
-1的事。
当然还不是事件对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| #include <stdio.h> #include <windows.h> #include <process.h>
char str[100]; HANDLE hEvent;
unsigned WINAPI NumberOfA(void *arg){ int count = 0;
for (int i = 0; str[i] != 0; i++){ if (str[i] == 'A') count++; } printf("A count number = %d\n", count);
return 0; }
unsigned WINAPI NumberOfOthers(void *arg){ int count = 0;
for (int i = 0; str[i] != 0; i++){ if (str[i] != 'A') count++; } printf("others char count number = %d\n", count - 1);
return 0; }
int main(){ HANDLE hThread1, hThread2; fputs("Pleas Input string:\n",stdout); fgets(str, 100, stdin);
hEvent = CreateEvent(NULL, FALSE, 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);
CloseHandle(hEvent);
return 0; }
|
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
第二个参数提到过,如果为TRUE,则事手动重置事件对象,即该对象需要用ResetEvent去重置为非信号对象。而FALSE则是自动重置事件对象,单个线程被释放之后就会重置为非信号。
像上述代码事实上阻塞的是hTread1和2两个线程让他挨个跑完得到结果,那么个人感觉event没什么关系了。
但是不阻塞这两个线程,就有可能发生只有一个线程来得及输出然后main进程就结束了。
所以按照我的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| #include <stdio.h> #include <windows.h> #include <process.h>
char str[100]; HANDLE hEvent; int count = 0;
unsigned WINAPI NumberOfA(void *arg){ count = 0; WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; str[i] != 0; i++){ if (str[i] == 'A') count++; } printf("A count number = %d\n", count); SetEvent(hEvent); return 0; }
unsigned WINAPI NumberOfOthers(void *arg){ count = 0; WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; str[i] != 0; i++){ if (str[i] != 'A') count++; } printf("others char count number = %d\n", count - 1);
SetEvent(hEvent); return 0; }
int main(){ HANDLE hThread1, hThread2; fputs("Pleas Input string:\n",stdout); fgets(str, 100, stdin);
hEvent = CreateEvent(NULL, FALSE, TRUE, NULL); hThread1 = (HANDLE)_beginthreadex(NULL, 0, NumberOfA, NULL, 0, NULL); hThread2 = (HANDLE)_beginthreadex(NULL, 0, NumberOfOthers, NULL, 0, NULL);
system("pause");
CloseHandle(hEvent);
return 0; }
|
我这么设计的eventhEvent = CreateEvent(NULL, FALSE, TRUE, NULL);
它是自动重置,初始值有信号的事件对象。
那么我两个线程就很好处理了,它默认有信号,就提前wait等待,不管哪个线程先执行,都是有信号的状态,用完之后event自动重置无信号了,我们在线程快结束前给他SetEvent,在设置成有信号的事件,那么下一个线程调用就没有问题了。
效果是到位了,如果我在某个线程wait之后没有set,那么有一个线程就会没法跑了。亲测有效。
那么如果是自动重置,初始无信号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| unsigned WINAPI NumberOfA(void *arg){ count = 0; SetEvent(hEvent); WaitForSingleObject(hEvent, INFINITE); for (int i = 0; str[i] != 0; i++){ if (str[i] == 'A') count++; } printf("A count number = %d\n", count); return 0; }
unsigned WINAPI NumberOfOthers(void *arg){ count = 0;
SetEvent(hEvent); WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; str[i] != 0; i++){ if (str[i] != 'A') count++; } printf("others char count number = %d\n", count - 1);
return 0; }
|
初始无信号,那么进入线程之前就要先设置信号,然后wait。虽然这样操作有点儿比了。但是至少了解一下用法。
不然实在是感受不到这种所谓的信号带来的影响?
ps:阻塞线程和system好像都差不多,学这个我感觉自己越来越糊涂。
这种统计的可能不是很好的体现出事件对象的特点
那么可以来一个卖票的情况。
假设两个窗口,一共10或者100张票,依次卖票,而两个线程谁多谁少只能看cpu。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| #include <stdio.h> #include <windows.h> #include <process.h>
int piao = 100; HANDLE sigHand;
DWORD WINAPI SellTicketA(void *p){ printf("窗口1开始卖票!\n");
while (true){ WaitForSingleObject(sigHand, INFINITE); if (piao > 0){ printf("窗口1售出第%d张票...\n", piao); piao--; Sleep(100); }
SetEvent(sigHand); }
return 0; }
DWORD WINAPI SellTicketB(void *p){ printf("窗口2开始卖票!\n");
while (true){ WaitForSingleObject(sigHand, INFINITE); if (piao > 0){ printf("窗口2售出第%d张票...\n", piao); piao--; Sleep(100); }
SetEvent(sigHand); }
return 0; }
int main(){ printf("开始卖票!\n");
HANDLE hOne, hTwo; hOne = CreateThread(NULL, 0, SellTicketA, NULL, 0, 0); hTwo = CreateThread(NULL, 0, SellTicketB, NULL, 0, 0);
sigHand = CreateEvent(NULL, FALSE, FALSE, NULL); SetEvent(sigHand);
Sleep(20000); system("pause"); CloseHandle(sigHand); CloseHandle(hOne); CloseHandle(hTwo);
return 0; }
|
当然这是sigHand = CreateEvent(NULL, FALSE, FALSE, NULL);
自动重置,初始无信号的情况,所以我们要先手动设置信号,这样两个线程才能wait到信号,然后运行完重新设置信号,以便于另一个进程使用。
100有点长就不截图了。
其余事件对象方式不再做示范,有兴趣自己玩玩容易理解。
结语
tmd,很杂,真的很杂,而且有些地方要看半天调几下~后面能记住多少是另一码事了。