前面有进行了并发服务器的多进程实现和IO复用实现,现在进行多线程实现的并发服务器。
一、linux下的多线程
进程作为系统下最小的执行单位,多进程模型中每个进程都具有独立的内存空间,从而给进程间通信带来极大阻碍,需要使用进程间通信技术(IPC:管道、信号量、套接字、共享存储等),而且由于创建进程带来的开销使得该程序效率不高。
其实最大的问题是多进程的实现–“上下文切换”带来的开销,所以后面才有了轻量级进程–“线程”,线程的上下文切换比进程快,而且它们共享进程内空间,数据交换不用其他技术支持。
1.1 多线程的实现
进程内存空间由保存全局变量的数据区、malloc等动态申请空间的堆区和函数运行所用的栈区(大致是这样,细致的分别可以不用追究)。对于线程,它们之间共享的就是数据区和堆区,自己拥有自己的栈区来执行代码,如下是多进程运行起来的效果模型,不代表实际物理硬件中状态:
下面是多线程模型:
多线程和多进程其实都是为了获得多个代码执行流,而进程是操作系统内的单独执行流,线程则是进程内的单独执行流单位。
1.2 线程的创建和销毁
对于线程的创建方法,其实是依据POSIX的API规范实现的,这是一个适用于linux和大部分UNIX系统的一个可移植操作系统接口规范。
#include <pthread.h>
int pthread_create(pthread_t * thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
//成功时返回0,失败返回出错编号
上面是linux等系统中创建线程的函数原型,在编译时需要链接外部库,加上-lpthread即可。
参数解读:
thread,用来保存新线程的ID;
attr,传递线程属性,一般使用NULL创建默认属性线程即可;
start_routine,用来分配给新线程的函数执行流,相当于main函数;
arg,用来传递参数到start_routine中的变量地址。
其实关于上面的函数原型还有另一个版本,就是在参数前面添加关键字restrict是,这是用来告诉编译器,不能通过指针以外的直接或间接的方式修改对象内容,只用于指针。
通过上面的模型我们可以知道,多线程之间都可以访问同一份全局数据区,所以全局数据存在被两个线程同时访问的情况,但这是非常危险的。不过,首先要讲明一点,我们在创建线程以后,其他线程会随着进程的终止而终止,就是main函数返回的时候。但如果其他线程还没来得及完成任务就结束了呢?正常来说,我们可以用sleep函数使得main函数暂停一下下,使得其他线程有足够的时间来运行,但这需要对程序走向有着绝对的预测,是很不合理的。让我们看看其他选择。
#include <pthread.h>
int pthread_join(pthread_t thread, void **status);
//成功时返回0,失败时返回错误号
该函数可以使得我们的main函数负责的主流程在等待thread对应线程结束后才返回,且能得到线程的函数返回值,就像main返回值那样。不过切记不能返回指向栈上局部变量的指针,具体原因大家应该都懂。让我们看一个多线程访问共享数据的例子:
#include <stdio.h>
#include <pthread.h>
//volatile关键字用于通知计算机代理可以改变该值,这里表示共享数据,不加static volatile也行
static volatile long long sum = 0;
void *incr(void *arg) {
printf("[%s] begin\n", (char *)arg);
for (int i = 0;i < 1e7; i++)
sum += 1;
printf("[%s] done\n", (char *)arg);
//线程结束
return NULL;
}
void *des(void *arg) {
printf("[%s] begin\n", (char *)arg);
for (int i = 0; i < 1e7; i++)
sum -= 1;
printf("[%s] done\n", (char *)arg);
//线程结束
return NULL;
}
int main() {
//创建两个线程,传入不同字符作为another_main的参数
pthread_t p1, p2;
printf("main begin, sum = %lld\n", sum);
pthread_create(&p1, NULL, incr, "A");
pthread_create(&p2, NULL, des, "B");
//等待p1、p2对应的线程结束才结束main运行
pthread_join(p1, NULL);
pthread_join(p2, NULL);
printf("[main] sum: %lld\n", sum);
return 0;
}
上面的程序就我们日常的角度来看,就是两个线程都对sum自加或自减相同的次数,然后最后在main函数中输出,所以结果应该是0,对吧?让我们看看运行结果:
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ gcc thread_simple.c -o thread_simple -lpthread
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./thread_simple
main begin, sum = 0
[A] begin
[B] begin
[A] done
[B] done
[main] sum: -1727789
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./thread_simple
main begin, sum = 0
[A] begin
[B] begin
[A] done
[B] done
[main] sum: -3319087
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./thread_simple
main begin, sum = 0
[A] begin
[B] begin
[A] done
[B] done
[main] sum: 2994460
哦豁?结果怎么是这样的?这是因为共享数据区的sum不是只存在一个的物体,当我们对sum进行计算时,需要把值传给CPU,运算完毕再写回来新值。而我们有两个线程,理想情况下是你拿去sum放进CPU加一,写回去我再拿来减一,但实际上可能你还没写进去,它就已经切换线程2了,这时候线程2读到的就是之前没自加的值,然后它拿去减一,然后写进sum中去,结果就变成只进行了减一运算了;同样的思路,线程1和线程2的工作反过来,就只进行了加一运算了。
所以结果就变成难以预测的了,对于这种数据,我们应该设定一个时间内对其进行访问的只能有一个线程,另外的线程要访问只能等待当前线程用完,这就是同步。
1.3 线程同步
同步对于解决线程访问顺序的问题,一方面,指的是同时访问同一内存空间发生的情况,另一方面指的是需要指定访问同一内存空间的线程执行顺序的情况。第一种情况,大家都用它,后面这种情况是一个塞东西进去另一个拿东西出来,就是很多人都知道的生产者-消费者模型。下面介绍一下同步所用技术:互斥量和信号量。
1.3.1 基于互斥量的同步
互斥量就是给需要限定访问的代码区上锁,我要用的时候我就锁上,悄咪咪地用,用完再开锁,让其他人用。具体实现就是下面两个函数:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *lock);
int pthread_mutex_unlock(pthread_mutex_t *lock);
//成功时返回0,失败返回其他值
上面锁的使用需要用到pthread_mutex_t变量,很多情况下都是定义为全局变量,然后在main函数中进行初始化,要注意,当你只调用了pthread_mutex_lock函数上锁,而没有pthread_mutex_unlock解锁,那代码临界区就会无法摆脱阻塞状态,这种状态被称为死锁。
#include <stdio.h>
#include <pthread.h>
long long sum = 0;
pthread_mutex_t lock;
void *incr(void *arg) {
printf("[%s] begin\n", (char *)arg);
//上锁
pthread_mutex_lock(&lock);
for (int i = 0;i < 1e7; i++)
sum += 1;
//开锁
pthread_mutex_unlock(&lock);
printf("[%s] done\n", (char *)arg);
//线程结束
return NULL;
}
void *des(void *arg) {
printf("[%s] begin\n", (char *)arg);
for (int i = 0; i < 1e7; i++) {
//上锁
pthread_mutex_lock(&lock);
sum -= 1;
//开锁
pthread_mutex_unlock(&lock);
}
printf("[%s] done\n", (char *)arg);
//线程结束
return NULL;
}
int main() {
pthread_t p1, p2;
//初始化pthread_mutex_t变量
pthread_mutex_init(&lock, NULL);
printf("main begin, sum = %lld\n", sum);
pthread_create(&p1, NULL, incr, "A");
pthread_create(&p2, NULL, des, "B");
pthread_join(p1, NULL);
pthread_join(p2, NULL);
printf("[main] sum: %lld\n", sum);
//销毁pthread_mutex_t变量
pthread_mutex_destroy(&lock);
return 0;
}
使用如上,然后运行时就可以发现没啥问题了,pthread_mutex_t变量主要使用pthread_mutex_init进行初始化,没有特别需要就给NULL作为第二参数即可,然后最后记得销毁,养成好习惯(pthread_mutex_destroy干掉lock)。
1.3.2 基于信号量的同步
信号量和互斥量类似,不过我们完成这里的需要,用二进制信号量完成即可。和上面pthread_mutex_lock和pthread_mutex_unlock实现锁机制,完成临界区的保护类似,这里使用sem_post和sem_wait来实现锁机制。函数原型如下:
#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
//成功时返回0,失败返回其他值
//sem为保存信号量变量,传递给sem_post加一,传递给sem_wait时减一
它的初始化和销毁同样使用特定函数在main中进行,声明为全局变量。注意,信号量的值不能小于0,由于sem_wait对其进行减一运算,如果参数sem对应信号量为0值,函数会阻塞直至其他线程增加了信号量。初始化和销毁信号量的函数原型为:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
//sem,创建信号量时保存信号量变量地址值
//pshared,值为0表示只允许进程内部使用,其他值表示多个进程间共享
//value,新创建信号量初始值
int sem_destroy(sem_t *sem);
上面函数成功时返回0,失败时返回其他值。简单例子如下:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
int num = 0;
sem_t sem1, sem2;
void *read(void *arg) {
int i;
for (i = 0;i < 3;i++) {
sem_wait(&sem2);
fputs("输入:", stdout);
scanf("%d", &num);
sem_post(&sem1);
}
return NULL;
}
void *inc(void *arg) {
int i, sum = 0;
for (i = 0;i < 3; i++) {
sem_wait(&sem1);
printf("输入值加1:%d\n", num + 1);
sem_post(&sem2);
}
//线程结束
return NULL;
}
int main() {
pthread_t p1, p2;
//初始化信号量变量
sem_init(&sem1, 0, 0);
sem_init(&sem2, 0, 1);
pthread_create(&p1, NULL, read, NULL);
pthread_create(&p2, NULL, inc, NULL);
pthread_join(p1, NULL);
pthread_join(p2, NULL);
//销毁信号量变量
sem_destroy(&sem1);
sem_destroy(&sem2);
return 0;
}
信号量的使用主要是用来确保线程间的合作,使之输出如下:
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./thread_simple
输入:2
输入值加1:3
输入:4
输入值加1:5
输入:5
输入值加1:6
一个负责读取输入信息存储到变量,一个负责读取变量值进行输出,这个就是同步的第二种情况的例子。
二、多线程服务器的实现:简单聊天室
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/socket.h>
#include <pthread.h>
void errorHandling(char *message);
void *handlerClnt(void *arg);
void sendMsg(char *msg, int len);
#define BUF 100
#define MAXCLNT 256
int clnt_num = 0;
int clnt_socks[MAXCLNT];
pthread_mutex_t mutex;
int main(int argc, char *argv[]) {
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t addr_size;
pthread_t thread_id;
if(argc != 2) {
printf("Usage: %s <port>\n", argv[0]);
exit(-1);
}
//初始化一个没有特别属性的互斥量
pthread_mutex_init(&mutex, NULL);
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
errorHandling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
errorHandling("bind() error!");
if (listen(serv_sock, 5) == -1)
errorHandling("listen() error!");
while(1) {
addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &addr_size);
if (clnt_sock == -1)
continue;
pthread_mutex_lock(&mutex);
clnt_socks[clnt_num++] = clnt_sock;
pthread_mutex_unlock(&mutex);
pthread_create(&thread_id, NULL, handlerClnt, (void*)&clnt_sock);
pthread_detach(thread_id);
printf("已连接客户端%d,IP:%s\n", clnt_sock, inet_ntoa(clnt_addr.sin_addr));
}
close(serv_sock);
return 0;
}
//服务客户端信息传输
void *handlerClnt(void *arg) {
int clnt_sock = *((int*)arg);
int i, str_len = 0;
char msg[BUF];
//当收到信息,群发出去
while ((str_len = read(clnt_sock, msg, sizeof(msg))) != 0)
sendMsg(msg, str_len);
//客户端执行close,主动断开
pthread_mutex_lock(&mutex);
for(i = 0; i < clnt_num; i++) {
//从客户端连接套接字集合中删除当前连接客户端的记录
if (clnt_sock == clnt_socks[i]) {
while(i++ < clnt_num - 1)
clnt_socks[i] = clnt_socks[i + 1];
break;
}
}
clnt_num--;
pthread_mutex_unlock(&mutex);
close(clnt_sock);
return NULL;
}
//发送信息给所有客户端
void sendMsg(char *msg, int len) {
int i;
pthread_mutex_lock(&mutex);
//逐个客户端发送接收到的信息
for(i = 0; i < clnt_num; i++)
write(clnt_socks[i], msg, len);
pthread_mutex_unlock(&mutex);
}
void errorHandling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
上面是我们的服务器运行程序,接收所有客户端的信息,然后把收到的信息逐个发出去,创造一个群聊效果。和前面的一样,该服务端运行在云服务器上,下面是客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
void *sendMsg(void *arg);
void *readMsg(void *arg);
void errorHandling(char *message);
#define BUF 100
#define NAME 20
char name[NAME] = "[DEFAULT]";
char msg[BUF];
int main(int argc, char *argv[]) {
int sock;
struct sockaddr_in serv_addr;
pthread_t send_thread, recv_thread;
void *thread_back;
if (argc != 4) {
printf("Usage: %s <IP> <port> <ID>\n", argv[0]);
exit(-1);
}
sprintf(name, "[%s]", argv[3]);
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
errorHandling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
errorHandling("connect() error!");
pthread_create(&send_thread, NULL, sendMsg, (void*)&sock);
pthread_create(&recv_thread, NULL, readMsg, (void*)&sock);
pthread_join(send_thread, &thread_back);
pthread_join(recv_thread, &thread_back);
close(sock);
return 0;
}
//发送信息给客户端
void *sendMsg(void *arg) {
int sock = *((int *)arg);
char message[NAME + BUF];
while(1) {
fgets(msg, BUF, stdin);
if (!strcmp(msg, "q\n")||!strcmp(msg, "Q\n")) {
close(sock);
exit(0);
}
sprintf(message, "%s %s", name, msg);
write(sock, message, strlen(message));
}
return NULL;
}
//接收服务端信息
void *readMsg(void *arg) {
int sock = *((int *)arg);
char message[BUF + NAME];
int str_len;
while(1) {
str_len = read(sock, message, NAME + BUF -1);
if (str_len == -1)
return (void*)-1;
message[str_len] = 0;
fputs(message, stdout);
}
return NULL;
}
void errorHandling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
然后运行结果如下:
#centos7云服务器
[root@VM-0-17-centos ~]# ./chat_server 9999
已连接客户端4,IP:203.168.14.66
已连接客户端5,IP:203.168.14.66
#ubuntu子系统下两个客户端
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./chat_client 121.5.47.242 9999 Ben
Hi, I'm Ben.What's your name?
[Ben] Hi, I'm Ben.What's your name?
[Jack] I' m Jack.Nice to meet you.
q
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./chat_client 121.5.47.242 9999 Jack
[Ben] Hi, I'm Ben.What's your name?
I' m Jack.Nice to meet you.
[Jack] I' m Jack.Nice to meet you.
q
可以看到效果,然后我们看看他们的后台访问如何
[root@VM-0-17-centos ~]# pstree -p 30517
chat_server(30517)─┬─{
chat_server}(30589)
└─{
chat_server}(30683)
[root@VM-0-17-centos ~]# pstack 30517
Thread 3 (Thread 0x7f2dfb13a700 (LWP 30589)):
#0 0x00007f2dfb51775d in read () from /lib64/libpthread.so.0
#1 0x0000000000400d65 in handlerClnt ()
#2 0x00007f2dfb510ea5 in start_thread () from /lib64/libpthread.so.0
#3 0x00007f2dfb239b0d in clone () from /lib64/libc.so.6
Thread 2 (Thread 0x7f2dfa939700 (LWP 30683)):
#0 0x00007f2dfb51775d in read () from /lib64/libpthread.so.0
#1 0x0000000000400d65 in handlerClnt ()
#2 0x00007f2dfb510ea5 in start_thread () from /lib64/libpthread.so.0
#3 0x00007f2dfb239b0d in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7f2dfb93a740 (LWP 30517)):
#0 0x00007f2dfb5179dd in accept () from /lib64/libpthread.so.0
#1 0x0000000000400c94 in main ()
#客户端断开连接后
[root@VM-0-17-centos ~]# pstack 30517
#0 0x00007f2dfb5179dd in accept () from /lib64/libpthread.so.0
#1 0x0000000000400c94 in main ()
上面是云服务器上面的查看信息,使用pstree -p查看实在无法区分线程进程(因为linux中似乎不论进程线程都会分配task_struct,线程没有专属自己的描述符),所以使用pstack来查看,pstack可以查看特定进程ID的线程情况。原本想在ubuntu子系统使用pstack来查看线程情况的,结果它报错,pstack是个脚本,具体问题我也还没排查,先记录吧。
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ pstack 124
124: ./chat_client 121.5.47.242 9999 Ben
pstack: Input/output error
failed to read target.
三、windows下多线程
windows和linux有很大的不同,就目前而言,linux中一切皆文件,什么都可以统一为文件进行管理,而windows中是区分开的。这个是我现在知道的最大不同。对于windows中的线程、进程以及文件等资源的管理,都统一联系在内核对象。
3.1 句柄和内核对象
为了管理各种资源,windows系统在内存中分配内存块,针对对应成员(进程等资源)进行信息记录,这个内存块就是一个数据结构,记录下文件数据IO、打开模式等信息。
注:内核对象是操作系统所有,操作系统创建分配并且只有操作系统可以访问的内存,由于装载有各种信息,因此是一个数据结构。
操作系统封装好了一系列API,我们可以使用这些函数申请创建内核对象,函数会返回一个句柄(根据操作系统位数而定的整数),我们对内核对象的操作都是通过句柄进行,操作系统识别句柄可以识别对应内核对象。学习过面向对象特性的程序猿应该知道,所谓封装就是隐藏内部特征暴露功能,我们虽说可以通过句柄操作内核对象,但内核里面是怎样我们是不知道的,因为我们是通过句柄调用系统API进行的操作,中间隔了一个操作系统。
3.2 线程创建
和之前的pthread_create一样,windows的线程创建函数名字也有一个create,如下:
#include <windows.h>
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
//成功时返回线程句柄,失败返回NULL
上面线程创建函数和pthread_create非常类似,我们如果只定义一个默认线程,关注lpStartAddress和lpParameter即可,前者是传入一个函数指针使得新开线程有个执行流,后者是传入函数指针对应函数的参数,其他的传入0或者NULL即可。
参数说明:
lpThreadAttributes,线程安全相关信息,默认传入NULL(指针的意思啦);
dwStackSize,分配给线程的栈大小,默认传入0;
lpStartAddress,传递给线程执行的函数的函数指针;
lpParameter,上面函数需要的参数;
dwCreationFlags,指定线程创建后的行为,传递0则线程马上执行;
lpThreadId,保存线程ID的变量地址
上面是很多问某乎朋友如何创建windows线程都会回复的一个函数,可见非常基础和常见,但创造的线程调用c/c++函数会不稳定,所以安全起见,有这么一个函数:
#include <process.h>
uintptr_t _beginthreadex(
void *security,
unsigned stack_size,
unsigned (*start_address)(void *),
void *arg_list,
unsigned initflag,
unsigned *threadaddr
);
//成功返回线程句柄,失败返回0
这个函数参数意义和上面的CreateThread一致,对应使用即可,而且上面应该看得更加明了参数信息。
3.3类似pthread_join的函数
前面linux中有pthread_join这样的函数,通过阻塞main等待函数参数指定的线程结束再运行,这样的函数windows中也有。我们知道线程在windows中是归于内核对象管理,所以线程是否终结,可以通过内核对象得到信息,终止状态即为"signaled",未终止就是"non_signaled"。
关注内核对象这两个状态的函数如下:
#include <windows.h>
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
//进入signaled状态就返回WAIT_OBJECT_0,超时返回WAIT_FAILED
DWORD WaitForMultipleObjects(DWORD count, const HANDLE *lpHandleds, BOOL bWaitAll, DWORD dwMilliseconds);
//失败返回WAIT_FAILED,成功则返回事件信息
前者针对单个内核对象,后者关注多个内核对象,主要是lpHandleds所指定的HANDLE型数组地址中的众多内核对象句柄。注意,这两个函数主要是用来关注内核对象的状态,如果内核对象没有进入这个状态,那函数在dwMilliseconds参数为INFINITE的情况下会一直阻塞程序进行。
3.4 线程同步
windows系统比较贴合日常,大家基本都用过,那应该知道程序运行有个用户模式和内核模式吧。用户模式下,应用程序基本模式,访问物理设备会有权限限制,对于内存也是;内核模式下没有用户模式下的限制,可以自由访问物理设备和内存。
windows的线程同步也根据两个模式分为两种,用户模式下线程同步是基于"CRITICAL_SECTION",内核模式下则是基于事件、信号量、互斥量的同步。
3.4.1 基于CRITICAL_SECTION的同步
和前面的互斥量使用一样,CRITICAL_SECTION对象作为钥匙存在,定义为全局变量,在main函数中初始化,在main中被销毁,并在上锁和解锁时都使用该对象作为参数。如下:
#include <windows.h>
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
LPCRITICAL_SECTION是指CRITICAL_SECTION对象地址值,该类型的初始化函数和销毁函数原型如下:
#include <windows.h>
void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
总结一下上面的linux使用锁机制,CRITICAL_SECTION对象及锁的使用场景如下:
void *thread_main(void *arg);
CRITICAL_SECTION cs;
int main() {
...
InitializeCriticalSection(&cs);
//创建线程等动作
DeleteCriticalSection(&cs);
}
void *thread_main(void *arg) {
...
EnterCriticalSection(&cs);
//临界区
LeaveCriticalSection(&cs);
}
上面介绍过,临界区其实是指示进行访问全局变量等操作的代码段,这里重提一下。
#include <stdio.h>
#include <windows.h>
#include <process.h>
long long sum = 0;
CRITICAL_SECTION cs;
//WINAPI是一个宏,代表__stdcall,具体含义可以自己查一下,这个是一个windows编程约定来的
unsigned WINAPI another_main(void *arg) {
//输出传入参数作为线程标志
printf("[%s] begin.\n", (char *)arg);
int i;
EnterCriticalSection(&cs);
for (i = 0; i < 1e7; i++)
sum += 1;
LeaveCriticalSection(&cs);
printf("[%s] done.\n", (char *)arg);
return 0;
}
int main() {
HANDLE handles[2];
printf("main, begin.\n");
//初始化CRITICAL_SECTION对象
InitializeCriticalSection(&cs);
//创建两个线程,传入参数A和B
handles[0] = (HANDLE)_beginthreadex(NULL, 0, another_main, "A", 0, NULL);
handles[2] = (HANDLE)_beginthreadex(NULL, 0, another_main, "B", 0, NULL);
WaitForMultipleObjects(2, handles, TRUE, INFINITE);
DeleteCriticalSection(&cs);
printf("At last, the sum is %lld\n", sum);
system("pause");
return 0;
}
然后编译运行,哦豁出错了,我以为是我的环境没设置好,因为很多材料都说要设置运行时库为"Multithread DLL"。后来看代码才发现,哦豁,数组访问越界了,handles数组一共就两个元素,handles[2]却是访问数组第三个成员就是尾部NULL,那当然就出错了。改正后运行结果如下:
main, begin.
[A] begin.
[B] begin.
[A] done.
[B] done.
At last, the sum is 20000000
请按任意键继续. . .
//不上锁的话,结果如下
main, begin.
[A] begin.
[B] begin.
[A] done.
[B] done.
At last, the sum is 10820740
请按任意键继续. . .
可以看到,上面结果和linux中多线程基本一致,而且很多资料都可以在windows中使用pthread函数,对应实现大家可以去查一下然后自己做一下。
3.4.2 内核模式中基于互斥量的同步
windows中的互斥量使用和linux中的互斥量的使用一致,不过上锁的机制是WaitForSingleObject函数和ReleaseMutex函数实现,前者上锁后者解锁,为何?因为互斥量也是一个内核对象,而互斥量被线程拥有时为non-signaled状态,释放时为signaled状态。signaled状态的互斥量使用WaitForSingleObject函数调用后自动进入non-signaled状态,如果是non-signaled状态,调用WaitForSingleObject函数就会处于阻塞状态。对于可以再次进入non-signaled状态的内核对象,称为内核对象的"auto-reset"模式,不会自动跳转的称为"manual-reset"模式,两种模式区分不同内核对象。创建互斥量和释放互斥量的函数原型如下:
#include <windows.h>
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner, LPCTSTR lpName);
//成功时返回创建的互斥量对象句柄,失败返回NULL
//lpMutexAttributes为NULL默认安全设置
//lpName为NULL默认创建无名互斥量对象
//bInitialOwner为TRUE,创建互斥量属于调用函数线程,为non-signaled状态;
//为FALSE,创建的互斥量对象不属于任何线程,状态为signaled
BOOL ReleaseMutex(HANDLE hMutex);
//成功时返回TRUE, 失败时返回FALSE
//这个函数用来释放互斥量,对应WaitForSingleObject的获取互斥量,这样就相当于上锁了
BOOL CloseHandle(HANDLE hObject);
//成功时返回TRUE, 失败返回FALSE
//用来销毁内核对象,可用于销毁互斥量和后面的信号量
同样是上面的例子,修改一下看看用法即可:
#include <stdio.h>
#include <windows.h>
#include <process.h>
long long sum = 0;
HANDLE mutex;
unsigned WINAPI another_main(void *arg) {
//输出传入参数作为线程标志
printf("[%s] begin.\n", (char *)arg);
int i;
WaitForSingleObject(mutex, INFINITE);
for (i = 0; i < 1e7; i++)
sum += 1;
ReleaseMutex(mutex);
printf("[%s] done.\n", (char *)arg);
return 0;
}
int main() {
HANDLE handles[2];
printf("main, begin.\n");
//初始化互斥量对象
mutex = CreateMutex(NULL, FALSE, NULL);
//创建两个线程,传入参数A和B
handles[0] = (HANDLE)_beginthreadex(NULL, 0, another_main, "A", 0, NULL);
handles[1] = (HANDLE)_beginthreadex(NULL, 0, another_main, "B", 0, NULL);
WaitForMultipleObjects(2, handles, TRUE, INFINITE);
printf("At last, the sum is %lld\n", sum);
CloseHandle(mutex);
system("pause");
return 0;
}
结果和上面的是一样的,可以对照来看。
3.4.3 内核模式中基于信号量的同步
上面linux中基于信号量的例子已经介绍过了信号量,可以知道的是,信号量的使用可以确保线程间的流程顺序能够按照我们的意愿来进行,而windows下的信号量除了注册于内核对象以外,其他和linux信号量基本一致。创建信号量对象和释放信号量对象使用的函数原型如下:
#include <windows.h>
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCT STRlpName
);
//成功时返回信号量对象的句柄,失败返回NULL
//lpSemaphoreAttributes传入NULL默认安全
//lInitialCount,指定信号量初始值;lMaximumCount指定信号量最大值
//STRlpName命名信号量对象,默认传入NULL
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
//成功时返回TRUE,失败时返回FALSE
//hSemaphore,释放信号量对象
//lReleaseCount,可以指定信号量增加的值,不能大于前面设定的信号量最大值
//lpPreviousCount,保存修改之前值的变量地址,不需要就传入NULL
由于销毁信号量同样可以使用前面用过的CloseHandle函数,所以这里就不做介绍了。关于windows信号量实现锁的方法如下:
WaitForSingleObject(sem, INFINITE);
//临界区
ReleaseSemaphore(sem, 1, NULL);
再把上面linux中信号量的例子实现一下:
#define _CRT_SECURE_NO_WARNINGS
//使得scanf的使用不报错
#include <stdio.h>
#include <process.h>
#include <windows.h>
int num = 0;
HANDLE sem1, sem2;
unsigned WINAPI read(void *arg) {
int i;
for (i = 0; i < 3; i++) {
WaitForSingleObject(sem2, INFINITE);
fputs("输入:", stdout);
scanf("%d", &num);
ReleaseSemaphore(sem1, 1, NULL);
}
return 0;
}
unsigned WINAPI inc(void *arg) {
int i, sum = 0;
for (i = 0; i < 3; i++) {
WaitForSingleObject(sem1, INFINITE);
printf("输入值加1:%d\n", num + 1);
ReleaseSemaphore(sem2, 1, NULL);
}
//线程结束
return 0;
}
int main() {
HANDLE thread1, thread2;
//初始化信号量变量
sem1 = CreateSemaphore(NULL, 0, 1, NULL);
sem2 = CreateSemaphore(NULL, 1, 1, NULL);
thread1 = (HANDLE)_beginthreadex(NULL, 0, read, NULL, 0, NULL);
thread2 = (HANDLE)_beginthreadex(NULL, 0, inc, NULL, 0, NULL);
WaitForSingleObject(thread1, INFINITE);
WaitForSingleObject(thread2, INFINITE);
//销毁信号量变量
CloseHandle(sem1);
CloseHandle(sem2);
system("pause");
return 0;
}
运行结果可以参考上面的例子,互斥量在被线程拥有时为non-signaled状态,释放时为signaled状态;调用WaitForSingleObject会阻塞直到信号量大于0。函数返回会将信号量减一并进入non-signaled状态,这适用于二进制信号量的情况下。
3.5 windows多线程服务器的实现
实现例子是个聊天室项目,收到客户端连接就新开线程单独服务,收到信息就群发出去。
服务端实现代码如下:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <Windows.h>
#include <process.h>
#define BUF 100
#define MAX 256
#pragma comment(lib, "ws2_32.lib")
unsigned WINAPI handler(void *arg);
void sendMsg(char *msg, int len);
void errorHandling(char* message);
int clnt_num = 0;
SOCKET clnt_socks[MAX];
HANDLE lock;
int main(int argc, char* argv[]) {
WSADATA wsa_data;
SOCKET serv_sock, clnt_sock;
SOCKADDR_IN serv_addr, clnt_addr;
HANDLE thread;
char message[BUF];
int addr_size;
if (argc != 2) {
printf("Usage: %s <port>\n", argv[0]);
exit(-1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
errorHandling("WSAStartup() error!");
//初始化互斥量和监听套接字
lock = CreateMutex(NULL, FALSE, NULL);
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == INVALID_SOCKET)
errorHandling("socket() error!");
//构建用于绑定监听套接字的本机地址信息
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
//绑定监听套接字和服务器地址信息,并且开始监听客户端连接请求
if (bind(serv_sock, (SOCKADDR*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
errorHandling("bind() error!");
if (listen(serv_sock, 5) == SOCKET_ERROR)
errorHandling("listen() error!");
printf("服务器socket%d\n", serv_sock);
while (1) {
addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (SOCKADDR*) &clnt_addr, &addr_size);
//接受连接后,在客户端套接字数组中对现在新连接的客户端套接字进行更新,这个是互斥量,所以需要上锁
WaitForSingleObject(lock, INFINITE);
clnt_socks[clnt_num++] = clnt_sock;
ReleaseMutex(lock);
//新开线程服务,为新连接的客户端提供信息传输服务
thread = (HANDLE)_beginthreadex(NULL, 0, handler, (void*)&clnt_sock, 0, NULL);
printf("客户端%d已连接,IP: %d\n", clnt_sock, inet_ntoa(clnt_addr.sin_addr));
}
closesocket(serv_sock);
WSACleanup();
system("pause");
return 0;
}
//读取客户端发送信息并回送
unsigned WINAPI handler(void *arg) {
SOCKET sock = *((SOCKET*)arg);
char msg[BUF];
int i, str_len = 0;
//只要客户端没断开,就一直调用sendMsg向所有客户端发送本线程收到的对应客户端发送过来的信息
while ((str_len = recv(sock, msg, sizeof(msg), 0)) != 0)
sendMsg(msg, str_len);
WaitForSingleObject(lock, INFINITE);
for (i = 0; i < clnt_num; i++) {
if (sock == clnt_socks[i]) {
while (i++ < clnt_num - 1)
clnt_socks[i] = clnt_socks[i + 1];
break;
}
}
clnt_num--;
ReleaseMutex(lock);
closesocket(sock);
return 0;
}
//负责给各个客户端发送信息
void sendMsg(char *msg, int len) {
int i;
WaitForSingleObject(lock, INFINITE);
for (i = 0; i < clnt_num; i++)
send(clnt_socks[i], msg, len, 0);
ReleaseMutex(lock);
}
void errorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
客户端代码如下:
#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <Windows.h>
#include <process.h>
#define BUF 100
#define NAME 20
#pragma comment(lib, "ws2_32.lib")
unsigned WINAPI sendMsg(void *arg);
unsigned WINAPI recvMsg(void *arg);
void errorHandling(char* message);
char name[NAME] = "[God]";
char msg[BUF];
int main(int argc, char* argv[]) {
WSADATA wsa_data;
SOCKET sock;
SOCKADDR_IN serv_addr;
HANDLE send_thread, recv_thread;
char message[BUF];
int addr_size;
if (argc != 4) {
printf("Usage: %s <IP> <port> <NAME>\n", argv[0]);
exit(-1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
errorHandling("WSAStartup() error!");
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET)
errorHandling("socket() error!");
//构建用于连接的服务端地址信息
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
sprintf(name, "[%s]", argv[3]);
//发起连接请求
if (connect(sock, (SOCKADDR*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
errorHandling("connect() error!");
send_thread = (HANDLE)_beginthreadex(NULL, 0, sendMsg, (void*)&sock, 0, NULL);
recv_thread = (HANDLE)_beginthreadex(NULL, 0, recvMsg, (void*)&sock, 0, NULL);
WaitForSingleObject(send_thread, INFINITE);
WaitForSingleObject(recv_thread, INFINITE);
closesocket(sock);
WSACleanup();
system("pause");
return 0;
}
//发送信息给服务端
unsigned WINAPI sendMsg(void *arg) {
SOCKET sock = *((SOCKET*)arg);
char message[NAME + BUF];
while (1) {
fgets(msg, BUF, stdin);
if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n")) {
closesocket(sock);
exit(0);
}
sprintf(message, "%s %s", name, msg);
send(sock, message, strlen(message), 0);
}
return 0;
}
//接收服务端信息
unsigned WINAPI recvMsg(void *arg) {
SOCKET sock = *((SOCKET*)arg);
char message[NAME + BUF];
int str_len;
while (1) {
str_len = recv(sock, message, NAME + BUF -1, 0);
if (str_len == -1)
exit(-1);
message[str_len] = 0;
fputs(message, stdout);
}
}
void errorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
使用本机通信实在没意思,所以我把云服务器和win10互为服务端-客户端来进行连接通信,结果,win10作为服务端时,云服务器会连接失败,但反过来倒是可以,哎头疼。先告一段落吧。
上一集