原文作者:aircraft
原文链接:https://www.cnblogs.com/DOMLX/p/9661012.html
先讲Linux下(windows下在后面可以直接跳到后面看):
前面我们讲过多进程服务器,但我们知道它开销很大,因此我们才引入线程,我们可以把它看成是一种轻量级进程。它相比进程有如下几个优点:
进程:在操作系统构成单独执行流的单位。
线程:在进程构成单独执行流的单位。
它们的包含关系是,操作系统 > 进程 > 线程。进程与线程具体差异其实是这样的,每个进程都有独立的完整内存空间,它包括全局数据区,堆区,栈区,而多进程服务器之所以开销大是因为只是为了区分栈区里的不同函数流执行而把数据区,堆区,栈区内存全部复制了一份。而多线程就高效多了,它只把栈区分离出来,进程中的数据区,堆区则共享。具体内存结构示例图如下:
下面的程序,我们可以用它来创建一个线程:
#includepthread_create (thread, attr, start_routine, arg)
在这里,pthread_create 创建一个新的线程,并让它可执行。下面是关于参数的说明:
参数 | 描述 |
---|---|
thread | 指向线程标识符指针。 |
attr | 一个不透明的属性对象,可以被用来设置线程属性。您可以指定线程属性对象,也可以使用默认值 NULL。 |
start_routine | 线程运行函数起始地址,一旦线程被创建就会执行。 |
arg | 运行函数的参数。它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。 |
创建线程成功时,函数返回 0,若返回值不为 0 则说明创建线程失败。
使用下面的程序,我们可以用它来终止一个线程:
#includepthread_exit (status)
在这里,pthread_exit 用于显式地退出一个线程。通常情况下,pthread_exit() 函数是在线程完成工作后无需继续存在时被调用。
如果 main() 是在它所创建的线程之前结束,并通过 pthread_exit() 退出,那么其他线程将继续执行。否则,它们将在 main() 结束时自动被终止。
以下简单的实例代码使用 pthread_create() 函数创建了 5 个线程,每个线程输出"Hello Runoob!":
#include// 必须的头文件 #include using namespace std; #define NUM_THREADS 5 // 线程的运行函数 void* say_hello(void* args) { cout << "Hello Runoob!" << endl; return 0; } int main() { // 定义线程的 id 变量,多个变量使用数组 pthread_t tids[NUM_THREADS]; for(int i = 0; i < NUM_THREADS; ++i) { //参数依次是:创建的线程id,线程参数,调用的函数,传入的函数参数 int ret = pthread_create(&tids[i], NULL, say_hello, NULL); if (ret != 0) { cout << "pthread_create error: error_code=" << ret << endl; } } //等各个线程退出后,进程才结束,否则进程强制结束了,线程可能还没反应过来; pthread_exit(NULL); }
linux下编译运行后结果为:
Hello Runoob!
Hello Runoob!
Hello Runoob!
Hello Runoob!
Hello Runoob!
以下简单的实例代码使用 pthread_create() 函数创建了 5 个线程,并接收传入的参数。每个线程打印一个 "Hello Runoob!" 消息,并输出接收的参数,然后调用 pthread_exit() 终止线程。
//文件名:test.cpp #include#include #include using namespace std; #define NUM_THREADS 5 void *PrintHello(void *threadid) { // 对传入的参数进行强制类型转换,由无类型指针变为整形数指针,然后再读取 int tid = *((int*)threadid); cout << "Hello Runoob! 线程 ID, " << tid << endl; pthread_exit(NULL); } int main () { pthread_t threads[NUM_THREADS]; int indexes[NUM_THREADS];// 用数组来保存i的值 int rc; int i; for( i=0; i < NUM_THREADS; i++ ){ cout << "main() : 创建线程, " << i << endl; indexes[i] = i; //先保存i的值 // 传入的时候必须强制转换为void* 类型,即无类型指针 rc = pthread_create(&threads[i], NULL, PrintHello, (void *)&(indexes[i])); if (rc){ cout << "Error:无法创建线程," << rc << endl; exit(-1); } } pthread_exit(NULL); }
linux下编译运行后结果为:
main() : 创建线程, 0 main() : 创建线程, 1 Hello Runoob! 线程 ID, 0 main() : 创建线程, Hello Runoob! 线程 ID, 21 main() : 创建线程, 3 Hello Runoob! 线程 ID, 2 main() : 创建线程, 4 Hello Runoob! 线程 ID, 3
这个实例演示了如何通过结构传递多个参数。您可以在线程回调中传递任意的数据类型,因为它指向 void,如下面的实例所示:
#include#include #include using namespace std; #define NUM_THREADS 5 struct thread_data{ int thread_id; char *message; }; void *PrintHello(void *threadarg) { struct thread_data *my_data; my_data = (struct thread_data *) threadarg; cout << "Thread ID : " << my_data->thread_id ; cout << " Message : " << my_data->message << endl; pthread_exit(NULL); } int main () { pthread_t threads[NUM_THREADS]; struct thread_data td[NUM_THREADS]; int rc; int i; for( i=0; i < NUM_THREADS; i++ ){ cout <<"main() : creating thread, " << i << endl; td[i].thread_id = i; td[i].message = (char*)"This is message"; rc = pthread_create(&threads[i], NULL, PrintHello, (void *)&td[i]); if (rc){ cout << "Error:unable to create thread," << rc << endl; exit(-1); } } pthread_exit(NULL); }
linux下编译运行后结果为:
main() : creating thread, 0 main() : creating thread, 1 Thread ID : 0 Message : This is message main() : creating thread, Thread ID : 21 Message : This is message main() : creating thread, 3 Thread ID : 2 Message : This is message main() : creating thread, 4 Thread ID : 3 Message : This is message Thread ID : 4 Message : This is message
我们可以使用以下两个函数来连接或分离线程:
pthread_join (threadid, status) pthread_detach (threadid)
pthread_join() 子程序阻碍调用程序,直到指定的 threadid 线程终止为止。当创建一个线程时,它的某个属性会定义它是否是可连接的(joinable)或可分离的(detached)。只有创建时定义为可连接的线程才可以被连接。如果线程创建时被定义为可分离的,则它永远也不能被连接。
用途:有的人没有在main 函数最后调用 pthread_exit(NULL); 函数等待,而是选择sleep,这里就可以用pthread_join()代替sleep的不可控制,,而有时候线程结束的时候你想做某一些事情需要知道线程是否结束了,也可以调用这个函数。
这个实例演示了如何使用 pthread_join() 函数来等待线程的完成。
#include#include #include #include using namespace std; #define NUM_THREADS 5 void *wait(void *t) { int i; long tid; tid = (long)t; sleep(1); cout << "Sleeping in thread " << endl; cout << "Thread with id : " << tid << " ...exiting " << endl; pthread_exit(NULL); } int main () { int rc; int i; pthread_t threads[NUM_THREADS]; pthread_attr_t attr; void *status; // 初始化并设置线程为可连接的(joinable) pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); for( i=0; i < NUM_THREADS; i++ ){ cout << "main() : creating thread, " << i << endl; rc = pthread_create(&threads[i], NULL, wait, (void *)&i ); if (rc){ cout << "Error:unable to create thread," << rc << endl; exit(-1); } } // 删除属性,并等待其他线程 pthread_attr_destroy(&attr); for( i=0; i < NUM_THREADS; i++ ){ rc = pthread_join(threads[i], &status); if (rc){ cout << "Error:unable to join," << rc << endl; exit(-1); } cout << "Main: completed thread id :" << i ; cout << " exiting with status :" << status << endl; } cout << "Main: program exiting." << endl; pthread_exit(NULL); }
linux下编译运行结果:
main() : creating thread, 0 main() : creating thread, 1 main() : creating thread, 2 main() : creating thread, 3 main() : creating thread, 4 Sleeping in thread Thread with id : 4 ...exiting Sleeping in thread Thread with id : 3 ...exiting Sleeping in thread Thread with id : 2 ...exiting Sleeping in thread Thread with id : 1 ...exiting Sleeping in thread Thread with id : 0 ...exiting Main: completed thread id :0 exiting with status :0 Main: completed thread id :1 exiting with status :0 Main: completed thread id :2 exiting with status :0 Main: completed thread id :3 exiting with status :0 Main: completed thread id :4 exiting with status :0 Main: program exiting.
前面我们知道了怎么创建线程,下面我们再来看看这样一个实例,创建100个线程,它们都访问了同一变量,其中一半对这个变量进行加1操作,一半进行减1操作,按道理其结果会等于0.
#include#include #include #include #define NUM_THREAD 100 void * thread_inc(void * arg); void * thread_des(void * arg); long long num = 0; //long long类型是64位整数型,多线程共同访问 int main(int argc, char *argv[]) { pthread_t thread_id[NUM_THREAD]; int i; //创建100个线程,一半执行thread_inc,一半执行thread_des for(i = 0; i < NUM_THREAD; i++) { if(i %2) pthread_create(&(thread_id[i]), NULL, thread_inc, NULL); else pthread_create(&(thread_id[i]), NULL, thread_des, NULL); } //等待线程返回 for (i = 0; i < NUM_THREAD; i++) pthread_join(thread_id[i], NULL); printf("result: %lld \n", num); //+1,-1按道理结果是0 return 0; } //线程入口函数1 void * thread_inc(void * arg) { for (int i = 0; i < 50000000; i++) num += 1;//临界区(引起问题的语句就是临界区位置) return NULL; } //线程入口函数2 void * thread_des(void * arg) { for (int i = 0; i < 50000000; i++) num -= 1;//临界区 return NULL; }
从运行结果看并不是0,而且每次运行的结果都不同。那这是什么原因引起的呢? 是因为每个线程访问一个变量是这样一个过程:先从内存取出这个变量值到CPU,然后CPU计算得到改变后的值,最后再将这个改变后的值写回内存。因此,我们可以很容易看出,多个线程访问同一变量,如果某个线程还只刚从内存取出数据,还没来得及写回内存,这时其它线程又访问了这个变量,所以这个值就会不正确了。
为什么会出现这种情况呢,来举个例子:
如上图所示:两个线程都要将某一个共同访问的变量加1,
就像上面说的这个运算过程是:线程1先拿到值然后经过cpu的运算在赋值回去,然后线程2在取值运算放回,上图实现的是最理想的情况,假如这时候线程一拿到了值99,同时线程二没间隔的也拿了99,这时候就要出问题了。线程一运算后赋值100回去,然后线程二运算后又赋值100回去,,,注意了哈,这里两个线程都是为了Num++服务,他们这样搞事情不就代表一个做了无用功吗?(我胖虎要是还拿的动刀还不打死你!!!)
这些看完应该就理解了为什么需要线程同步!!!!以及线程同步的重要性了吧!!
接下来我们再来讲讲怎么解决这个问题:线程同步
线程同步用于解决线程访问顺序引发的问题,一般是如下两种情况:
针对这两种可能引发的情况,我们分别使用的同步技术是:互斥量和信号量。
#includeint pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); //创建互斥量 int pthread_mutex_destroy(pthread_mutex_t *mutex);//销毁互斥量 int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁 int pthread_mutex_unlock(pthread_mutex_t *mutex);//释放锁
简言之,就是利用lock和unlock函数围住临界区的两端。当某个线程调用pthread_mutex_lock进入临界区后,如果没有调用pthread_mutex_unlock释放锁退出,那么其它线程就会一直阻塞在临界区之外,我们把这种情况称之为死锁。所以临界区围住一定要lock和unlock一一对应。
接下来看一下代码示例:
#include#include #include #include #define NUM_THREAD 100 void * thread_inc(void * arg); void * thread_des(void * arg); long long num = 0; pthread_mutex_t mutex; int main(int argc, char *argv[]) { pthread_t thread_id[NUM_THREAD]; int i; //互斥量的创建 pthread_mutex_init(&mutex, NULL); for(i = 0; i < NUM_THREAD; i++) { if(i %2) pthread_create(&(thread_id[i]), NULL, thread_inc, NULL); else pthread_create(&(thread_id[i]), NULL, thread_des, NULL); } for (i = 0; i < NUM_THREAD; i++) pthread_join(thread_id[i], NULL); printf("result: %lld \n", num); pthread_mutex_destroy(&mutex); //互斥量的销毁 return 0; } /*扩展临界区,减少加锁,释放锁调用次数,但这样变量必须加满到50000000次后其它线程才能访问. 这样是延长了线程的等待时间,但缩短了加锁,释放锁函数调用的时间,这里没有定论,自己酌情考虑*/ void * thread_inc(void * arg) { pthread_mutex_lock(&mutex); //互斥量锁住 for (int i = 0; i < 1000000; i++) num += 1; pthread_mutex_unlock(&mutex); //互斥量释放锁 return NULL; } /*缩短了线程等待时间,但循环创建,释放锁函数调用时间增加*/ void * thread_des(void * arg) { for (int i = 0; i < 1000000; i++) { pthread_mutex_lock(&mutex); num -= 1; pthread_mutex_unlock(&mutex); } return NULL; }
编译运行可以得到结果为:0
信号量
信号量与互斥量类似,只是互斥量是用锁来控制线程访问而信号量是用二进制0,1来完成控制线程顺序。sem_post信号量加1,sem_wait信号量减1,当信号量为0时,sem_wait就会阻断,因此通过这样让信号量加1减1就能控制线程的执行顺序了。
注释:mac上测试信号量函数返回-1失败,以后还是Linux上整吧,也许这些接口已经过时了…
#includeint sem_init(sem_t *sem, int pshared, unsigned int value);//创建信号量 int sem_destroy(sem_t *sem);//销毁信号量 int sem_post(sem_t *sem);//信号量加1 int sem_wait(sem_t *sem);//信号量减1,为0时阻塞
实例代码:线程A从用户输入得到值后存入全局变量num,此时线程B将取走该值并累加。该过程共进行5次,完成后输出总和并退出程序。
#include#include #include void * read(void * arg); void * accu(void * arg); static sem_t sem_one; static sem_t sem_two; static int num; int main(int argc, char *argv[]) { pthread_t id_t1, id_t2; sem_init(&sem_one, 0, 0); sem_init(&sem_two, 0, 1); pthread_create(&id_t1, NULL, read, NULL); pthread_create(&id_t2, NULL, accu, NULL); pthread_join(id_t1, NULL); pthread_join(id_t2, NULL); sem_destroy(&sem_one); sem_destroy(&sem_two); return 0; } void * read(void * arg) { int i; for (i = 0; i < 5; i++) { fputs("Input num: ", stdout); sem_wait(&sem_two); scanf("%d", &num); sem_post(&sem_one); } return NULL; } void * accu(void * arg) { int sum = 0 , i; for (i = 0; i < 5; i++) { sem_wait(&sem_one); sum+= num; sem_post(&sem_two); } printf("Result: %d \n", sum); return NULL; }
补充:线程的销毁,线程创建后并不是其入口函数返回后就会自动销毁,需要手动销毁,不然线程创建的内存空间将一直存在。一般手动销毁有如下两种方式:1,调用pthread_join函数,其返回后同时销毁线程 ,是一个阻断函数,服务端一般不用它销毁,因为服务端主线程不宜阻断,还要实时监听客服端连接。2,调用pthread_detach函数,不会阻塞,线程返回自动销毁线程,不过要注意调用它后不能再调用pthread_join函数,它与pthread_join主要区别就是一个是阻塞函数,一个不阻塞。
使用多线程实现了一个简单的聊天程序,并对临界区(clnt_cnt,clnt_socks)进行加锁访问.
// // main.cpp // hello_server // // Created by app05 on 15-10-22. // Copyright (c) 2015年 app05. All rights reserved. //临界区是:clnt_cnt和clnt_socks访问处 #include#include #include #include #include #include #include #define BUF_SIZE 100 #define MAX_CLNT 256 void * handle_clnt(void * arg); void send_msg(char *msg, int len); void error_handling(char * msg); int clnt_cnt = 0; int clnt_socks[MAX_CLNT]; pthread_mutex_t mutx; int main(int argc, char *argv[]) { int serv_sock, clnt_sock; struct sockaddr_in serv_adr, clnt_adr; socklen_t clnt_adr_sz; pthread_t t_id; if (argc != 2) { printf("Usage : %s \n", argv[0]); exit(1); } //创建互斥量 pthread_mutex_init(&mutx, NULL); serv_sock = socket(PF_INET, SOCK_STREAM, 0); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if(bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error"); if(listen(serv_sock, 5) == -1) error_handling("listen() error"); while (1) { clnt_adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz); //阻断,监听客服端连接请求 //临界区 pthread_mutex_lock(&mutx); //加锁 clnt_socks[clnt_cnt++] = clnt_sock; //新连接的客服端保存到clnt_socks数组里 pthread_mutex_unlock(&mutx); //释放锁 //创建线程 pthread_create(&t_id, NULL, handle_clnt, (void*) &clnt_sock); pthread_detach(t_id); //销毁线程,线程return后自动调用销毁,不阻断 printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr)); } close(serv_sock); return 0; } //线程执行 void * handle_clnt(void * arg) { int clnt_sock = *((int *)arg); int str_len = 0, i; char msg[BUF_SIZE]; while ((str_len = read(clnt_sock, msg, sizeof(msg))) != 0) send_msg(msg, str_len); //从数组中移除当前客服端 pthread_mutex_lock(&mutx); for (i = 0; i < clnt_cnt; i++) { if (clnt_sock == clnt_socks[i]) { while (i++ < clnt_cnt - 1) clnt_socks[i] = clnt_socks[i + 1]; break; } } clnt_cnt--; pthread_mutex_unlock(&mutx); close(clnt_sock); return NULL; } //向所有连接的客服端发送消息 void send_msg(char * msg, int len) { int i; pthread_mutex_lock(&mutx); for (i = 0; i < clnt_cnt; i++) write(clnt_socks[i], msg, len); pthread_mutex_unlock(&mutx); } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
客户端:
// // main.cpp // hello_client // // Created by app05 on 15-10-22. // Copyright (c) 2015年 app05. All rights reserved. // // #include#include #include #include #include #include #include #define BUF_SIZE 100 #define NAME_SIZE 20 void * send_msg(void * arg); void * recv_msg(void * arg); void error_handling(char *message); char name[NAME_SIZE] = "[DEFAULT]"; char msg[BUF_SIZE]; int main(int argc, const char * argv[]) { int sock; struct sockaddr_in serv_addr; pthread_t snd_thread, rcv_thread; void * thread_return; if(argc != 4) { printf("Usage: %s \n", argv[0]); exit(1); } sprintf(name, "[%s]", argv[3]); //聊天人名字,配置到编译器参数里 sock = socket(PF_INET, SOCK_STREAM, 0); if(sock == -1) error_handling("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) error_handling("connect() error"); //多线程分离输入和输出 pthread_create(&snd_thread, NULL, send_msg, (void *)&sock); pthread_create(&rcv_thread, NULL, recv_msg, (void *)&sock); //阻塞,等待返回 pthread_join(snd_thread, &thread_return); pthread_join(rcv_thread, &thread_return); close(sock); return 0; } //发送消息 void * send_msg(void * arg) { int sock = *((int *)arg); char name_msg[NAME_SIZE + BUF_SIZE]; while (1) { fgets(msg, BUF_SIZE, stdin); if (!strcmp(msg, "q\n") || !strcmp(msg, "Q \n")) { close(sock); exit(0); } sprintf(name_msg, "%s %s", name, msg); write(sock, name_msg, strlen(name_msg)); } return NULL; } //接收消息 void * recv_msg(void * arg) { int sock = *((int *)arg); char name_msg[NAME_SIZE + BUF_SIZE]; int str_len; while (1) { str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1); if(str_len == -1) return (void *)-1; name_msg[str_len] = 0; fputs(name_msg, stdout); } return NULL; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
windows下:
线程是系统内核对象之一。在学习线程之前,应先了解一下内核对象。内核对象是系统内核分配的一个内存块,该内存块描述的是一个数据结构,其成员负责维护对象的各种信息。内核对象的数据只能由系统内核来访问,应用程序无法在内存中找到这些数据结构并直接改变他们的内容。
常用的系统内核对象有事件对象、文件对象、作业对象、互斥对象、管道对象、进程对象和线程对象等。不同类型的内核对象,其数据结构各有不同。
进程被认为是一个正在运行的程序的实例,它也属于系统内核对象。可以将进程简单的理解为一个容器,它只是提供空间,执行程序的代码是由线程来实现的。线程存在于进程中,它负责执行进程地址空间中的代码。当一个进程创建时,系统会自动为其创建一个线程,该线程被称为主线程。在主线程中用户可以通过代码创建其他线程,当进程中的主线程结束时,进程也就结束了。
Windows下,创建线程有多种方式,以下将逐一介绍。注意它们的区别。
Windows API函数。该函数在主线程的基础上创建一个新线程。微软在Windows API中提供了建立新的线程的函数CreateThread。
HANDLECreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//线程安全属性
DWORD dwStackSize,//堆栈大小
LPTHREAD_START_ROUTINE lpStartAddress,//线程函数
LPVOID lpParameter,//线程参数
DWORD dwCreationFlags,//线程创建属性
LPDWORD lpThreadId//线程ID
);
示例代码:
#include "stdafx.h" #include#include using namespace std; DWORD WINAPI Fun1Proc(LPVOID lpParameter) { cout << "thread function Fun1Proc!\n"; return 0; } int main() { HANDLE hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL); CloseHandle(hThread1); Sleep(1000); cout << "main end!\n"; system("pause"); return 0; }
结果图:
除了使用CreateThread API函数创建线程外,还可以用C++语言提供的_beginthreadex函数来创建线程。
uintptr_t _beginthreadex( // NATIVE CODE
void *security, //线程安全属性
unsigned stack_size, //线程的栈大小
unsigned ( *start_address )( void * ),//线程函数
void *arglist, //传递到线程函数中的参数
unsigned initflag, //线程初始化标记
unsigned *thrdaddr //线程ID
);
示例代码:
#include "stdafx.h" #include#include #include using namespace std; unsigned int _stdcall ThreadProc(LPVOID lpParameter) { cout << "thread function ThreadProc!\n"; return 0; } int main() { _beginthreadex(NULL, 0, ThreadProc, 0, 0, NULL); Sleep(1000); cout << "main end!\n"; system("pause"); return 0; }
二.线程同步
在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。正常情况下对这种处理结果的了解应当在其处理任务完成后进行。
如果不采取适当的措施,其他线程往往会在线程处理任务结束前就去访问处理结果,这就很有可能得到有关处理结果的错误了解。例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。
为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。
代码示例:
两个线程同时对一个全局变量进行加操作,演示了多线程资源访问冲突的情况。
#include "stdafx.h" #include#include using namespace std; int number = 1; unsigned long __stdcall ThreadProc1(void* lp) { while (number < 100) { cout << "thread 1 :"<
可以看到有时两个线程计算的值相同,这就跟上面Linux下创建一百个线程将数字加减为0没成功一样的道理,都是访问内存的时候冲突了。
为什么会出现这种情况呢,来举个例子:
如上图所示:两个线程都要将某一个共同访问的变量加1,
就像上面说的这个运算过程是:线程1先拿到值然后经过cpu的运算在赋值回去,然后线程2在取值运算放回,上图实现的是最理想的情况,假如这时候线程一拿到了值99,同时线程二没间隔的也拿了99,这时候就要出问题了。线程一运算后赋值100回去,然后线程二运算后又赋值100回去,,,注意了哈,这里两个线程都是为了Num++服务,他们这样搞事情不就代表一个做了无用功吗?(我胖虎要是还拿的动刀还不打死你!!!)
这些看完应该就理解了为什么需要线程同步!!!!以及线程同步的重要性了吧!!
关于线程同步
线程之间通信的两个基本问题是互斥和同步。
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。 线程互斥是指对于共享的操作系统资源(指的是广义的”资源”,而不是Windows的.res文件,譬如全局变量就是一种共享资源),在各线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥是一种特殊的线程同步。实际上,互斥和同步对应着线程间通信发生的两种情况:
当有多个线程访问共享资源而不使资源被破坏时; 当一个线程需要将某个任务已经完成的情况通知另外一个或多个线程时。从大的方面讲,线程的同步可分用户模式的线程同步和内核对象的线程同步两大类。
用户模式中线程的同步方法主要有原子访问和临界区等方法。其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。 内核对象的线程同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。在WIN32中,同步机制主要有以下几种:
(1)事件(Event);
(2)信号量(semaphore);
(3)互斥量(mutex);
(4)临界区(Critical section)。Win32中的四种同步方式
临界区
临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。
代码示例:
#include "stdafx.h" #include#include using namespace std; int number = 1; //定义全局变量 CRITICAL_SECTION Critical; //定义临界区句柄 unsigned long __stdcall ThreadProc1(void* lp) { while (number < 100) { EnterCriticalSection(&Critical); cout << "thread 1 :"< 问题解决!!!
事件
事件(Event)是WIN32提供的最灵活的线程间同步方式,事件可以处于激发状态(signaled or true)或未激发状态(unsignal or false)。根据状态变迁方式的不同,事件可分为两类:
(1)手动设置:这种对象只可能用程序手动设置,在需要该事件或者事件发生时,采用SetEvent及ResetEvent来进行设置。
(2)自动恢复:一旦事件发生并被处理后,自动恢复到没有事件状态,不需要再次设置。使用”事件”机制应注意以下事项:
(1)如果跨进程访问事件,必须对事件命名,在对事件命名的时候,要注意不要与系统命名空间中的其它全局命名对象冲突;
(2)事件是否要自动恢复;
(3)事件的初始状态设置。由于event对象属于内核对象,故进程B可以调用OpenEvent函数通过对象的名字获得进程A中event对象的句柄,然后将这个句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函数中。此法可以实现一个进程的线程控制另一进程中线程的运行,例如:
HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent"); ResetEvent(hEvent);示例代码:
#include "stdafx.h" #include#include using namespace std; int number = 1; //定义全局变量 HANDLE hEvent; //定义事件句柄 unsigned long __stdcall ThreadProc1(void* lp) { while (number < 100) { WaitForSingleObject(hEvent, INFINITE); //等待对象为有信号状态 cout << "thread 1 :"< 运行结果都一样就不来显示出来了。
信号量
信号量是维护0到指定最大值之间的同步对象。信号量状态在其计数大于0时是有信号的,而其计数是0时是无信号的。信号量对象在控制上可以支持有限数量共享资源的访问。
信号量的特点和用途可用下列几句话定义:
(1)如果当前资源的数量大于0,则信号量有效;
(2)如果当前资源数量是0,则信号量无效;
(3)系统决不允许当前资源的数量为负值;
(4)当前资源数量决不能大于最大资源数量。创建信号量
函数原型为:
HANDLE CreateSemaphore ( PSECURITY_ATTRIBUTE psa, //信号量的安全属性 LONG lInitialCount, //开始时可供使用的资源数 LONG lMaximumCount, //最大资源数 PCTSTR pszName); //信号量的名称释放信号量
通过调用ReleaseSemaphore函数,线程就能够对信标的当前资源数量进行递增,该函数原型为:
BOOL WINAPI ReleaseSemaphore( HANDLE hSemaphore, //要增加的信号量句柄 LONG lReleaseCount, //信号量的当前资源数增加lReleaseCount LPLONG lpPreviousCount //增加前的数值返回 );打开信号量
和其他核心对象一样,信号量也可以通过名字跨进程访问,打开信号量的API为:
HANDLE OpenSemaphore ( DWORD fdwAccess, //access BOOL bInherithandle, //如果允许子进程继承句柄,则设为TRUE PCTSTR pszName //指定要打开的对象的名字 );代码示例:
#include "stdafx.h" #include#include using namespace std; int number = 1; //定义全局变量 HANDLE hSemaphore; //定义信号量句柄 unsigned long __stdcall ThreadProc1(void* lp) { long count; while (number < 100) { WaitForSingleObject(hSemaphore, INFINITE); //等待信号量为有信号状态 cout << "thread 1 :"< 结果一样。
互斥量
采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。
代码示例:
#include "stdafx.h" #include#include using namespace std; int number = 1; //定义全局变量 HANDLE hMutex; //定义互斥对象句柄 unsigned long __stdcall ThreadProc1(void* lp) { while (number < 100) { WaitForSingleObject(hMutex, INFINITE); cout << "thread 1 :"< 结果一样的。
三.多线程+IOPC实现服务端
服务端代码:
#define _CRT_SECURE_NO_WARNINGS #include#include #include #include #include #pragma comment(lib,"ws2_32.lib");//加载ws2_32.dll #define BUF_SIZE 100 #define READ 3 #define WRITE 5 typedef struct // socket info { SOCKET hClntSock; SOCKADDR_IN clntAdr; } PER_HANDLE_DATA, *LPPER_HANDLE_DATA; typedef struct // buffer info { OVERLAPPED overlapped; WSABUF wsaBuf; char buffer[BUF_SIZE]; int rwMode; // READ or WRITE 读写模式 } PER_IO_DATA, *LPPER_IO_DATA; unsigned int WINAPI EchoThreadMain(LPVOID CompletionPortIO); void ErrorHandling(char *message); SOCKET ALLCLIENT[100]; int clientcount = 0; HANDLE hMutex;//互斥量 int main(int argc, char* argv[]) { hMutex = CreateMutex(NULL, FALSE, NULL);//创建互斥量 WSADATA wsaData; HANDLE hComPort; SYSTEM_INFO sysInfo; LPPER_IO_DATA ioInfo; LPPER_HANDLE_DATA handleInfo; SOCKET hServSock; SOCKADDR_IN servAdr; int i; DWORD recvBytes = 0,flags = 0; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) ErrorHandling("WSAStartup() error!"); hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);//创建CP对象 GetSystemInfo(&sysInfo);//获取当前系统的信息 for (i = 0; i < sysInfo.dwNumberOfProcessors; i++) _beginthreadex(NULL, 0, EchoThreadMain, (LPVOID)hComPort, 0, NULL);//创建=CPU个数的线程数 hServSock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);//不是非阻塞套接字,但是重叠IO套接字。 memset(&servAdr, 0, sizeof(servAdr)); servAdr.sin_family = AF_INET; servAdr.sin_addr.s_addr = htonl(INADDR_ANY); servAdr.sin_port = htons(1234); bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)); listen(hServSock, 5); while (1) { SOCKET hClntSock; SOCKADDR_IN clntAdr; int addrLen = sizeof(clntAdr); hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &addrLen); handleInfo = (LPPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));//和重叠IO一样 handleInfo->hClntSock = hClntSock;//存储客户端套接字 WaitForSingleObject(hMutex, INFINITE);//线程同步 ALLCLIENT[clientcount++] = hClntSock;//存入套接字队列 ReleaseMutex(hMutex); memcpy(&(handleInfo->clntAdr), &clntAdr, addrLen); CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (DWORD)handleInfo, 0);//连接套接字和CP对象 //已完成信息将写入CP对象 ioInfo = (LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));//存储接收到的信息 memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED)); ioInfo->wsaBuf.len = BUF_SIZE; ioInfo->wsaBuf.buf = ioInfo->buffer;//和重叠IO一样 ioInfo->rwMode = READ;//读写模式 WSARecv(handleInfo->hClntSock, &(ioInfo->wsaBuf),//非阻塞模式 1, &recvBytes, &flags, &(ioInfo->overlapped), NULL); } CloseHandle(hMutex);//销毁互斥量 return 0; } unsigned int WINAPI EchoThreadMain(LPVOID pComPort)//线程的执行 { HANDLE hComPort = (HANDLE)pComPort; SOCKET sock; DWORD bytesTrans; LPPER_HANDLE_DATA handleInfo; LPPER_IO_DATA ioInfo; DWORD flags = 0; while (1)//大循环 { GetQueuedCompletionStatus(hComPort, &bytesTrans,//确认“已完成”的I/O!! (LPDWORD)&handleInfo, (LPOVERLAPPED*)&ioInfo, INFINITE);//INFINITE使用时,程序将阻塞,直到已完成的I/O信息写入CP对象 sock = handleInfo->hClntSock;//客户端套接字 if (ioInfo->rwMode == READ)//读写模式(此时缓冲区有数据) { puts("message received!"); if (bytesTrans == 0) // 连接结束 { WaitForSingleObject(hMutex, INFINITE);//线程同步 closesocket(sock); int i = 0; while (ALLCLIENT[i] == sock){ i++; } ALLCLIENT[i] = 0;//断开置0 ReleaseMutex(hMutex); free(handleInfo); free(ioInfo); continue; } int i = 0; for (; i < clientcount;i++) { if (ALLCLIENT[i] != 0)//判断是否为已连接的套接字 { if (ALLCLIENT[i] != sock) { LPPER_IO_DATA newioInfo; newioInfo = (LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));//动态分配内存 memset(&(newioInfo->overlapped), 0, sizeof(OVERLAPPED)); strcpy(newioInfo->buffer, ioInfo->buffer);//重新构建新的内存,防止多次释放free newioInfo->wsaBuf.buf = newioInfo->buffer; newioInfo->wsaBuf.len = bytesTrans; newioInfo->rwMode = WRITE; WSASend(ALLCLIENT[i], &(newioInfo->wsaBuf),//回声 1, NULL, 0, &(newioInfo->overlapped), NULL); } else { memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED)); ioInfo->wsaBuf.len = bytesTrans; ioInfo->rwMode = WRITE; WSASend(ALLCLIENT[i], &(ioInfo->wsaBuf),//回声 1, NULL, 0, &(ioInfo->overlapped), NULL); } } } ioInfo = (LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));//动态分配内存 memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED)); ioInfo->wsaBuf.len = BUF_SIZE; ioInfo->wsaBuf.buf = ioInfo->buffer; ioInfo->rwMode = READ; WSARecv(sock, &(ioInfo->wsaBuf),//再非阻塞式接收 1, NULL, &flags, &(ioInfo->overlapped), NULL); } else { puts("message sent!"); free(ioInfo); } } return 0; } void ErrorHandling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
客户端:
#define _CRT_SECURE_NO_WARNINGS #include#include #include #include #include #define BUF_SIZE 1000 #define NAME_SIZE 20 #pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll unsigned WINAPI SendMsg(void * arg);//发送信息函数 unsigned WINAPI RecvMsg(void * arg);//接受信息函数 void ErrorHandling(char * msg);//错误返回函数 int haveread = 0; char NAME[50];//[名字] char ANAME[50]; char msg[BUF_SIZE];//信息 int main(int argc, char *argv[]) { printf("请输入网名:"); scanf("%s", NAME); WSADATA wsaData; SOCKET hSock; SOCKADDR_IN servAdr; HANDLE hSndThread, hRcvThread; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) ErrorHandling("WSAStartup() error!"); hSock = socket(PF_INET, SOCK_STREAM, 0); memset(&servAdr, 0, sizeof(servAdr)); servAdr.sin_family = AF_INET; servAdr.sin_addr.s_addr = inet_addr("127.0.0.1"); servAdr.sin_port = htons(1234); if (connect(hSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR) ErrorHandling("connect() error"); int resultsend; puts("Welcome to joining our chatting room!\n"); sprintf(ANAME, "[%s]", NAME); hSndThread = (HANDLE)_beginthreadex(NULL, 0, SendMsg, (void*)&hSock, 0, NULL);//写线程 hRcvThread = (HANDLE)_beginthreadex(NULL, 0, RecvMsg, (void*)&hSock, 0, NULL);//读线程 WaitForSingleObject(hSndThread, INFINITE);//等待线程结束 WaitForSingleObject(hRcvThread, INFINITE); closesocket(hSock); WSACleanup(); system("pause"); return 0; } unsigned WINAPI SendMsg(void * arg) // send thread main { SOCKET sock = *((SOCKET*)arg); char name_msg[NAME_SIZE + BUF_SIZE]; char padd[2]; fgets(padd, 2, stdin);//多余的'\n' printf("\n send message:"); while (1) { { fgets(msg, BUF_SIZE, stdin); if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n")) { closesocket(sock); exit(0); } sprintf(name_msg, "[%s] %s", NAME, msg); char numofmsg = strlen(name_msg) + '0'; char newmsg[100]; newmsg[0] = numofmsg; newmsg[1] = 0;//第一个字符表示消息的长度 strcat(newmsg, name_msg); int result = send(sock, newmsg, strlen(newmsg), 0); if (result == -1)return -1;//发送错误 } } return NULL; } unsigned WINAPI RecvMsg(void * arg) // read thread main { SOCKET sock = *((SOCKET*)arg); char name_msg[NAME_SIZE + BUF_SIZE]; int str_len = 0; while (1) { { char lyfstr[1000] = { 0 }; int totalnum = 0; str_len = recv(sock, name_msg, 1, 0);//读取第一个字符!获取消息的长度 if (str_len == -1)//读取错误 { printf("return -1\n"); return -1; } if (str_len == 0)//读取结束 { printf("return 0\n"); return 0;//读取结束 } totalnum = name_msg[0] - '0'; int count = 0; do { str_len = recv(sock, name_msg, 1, 0); name_msg[str_len] = 0; if (str_len == -1)//读取错误 { printf("return -1\n"); return -1; } if (str_len == 0) { printf("return 0\n"); return 0;//读取结束 } strcat(lyfstr, name_msg); count = str_len + count; } while (count < totalnum); lyfstr[count] = '\0'; printf("\n"); strcat(lyfstr, "\n"); fputs(lyfstr, stdout); printf(" send message:"); fflush(stdout); memset(name_msg, 0, sizeof(char)); } } return NULL; } void ErrorHandling(char * msg) { fputs(msg, stderr); fputc('\n', stderr); exit(1); }
最后说一句啦。本网络编程入门系列博客是连载学习的,有兴趣的可以看我博客其他篇。。。。
参考博客:https://blog.csdn.net/kaida1234/article/details/79465713
参考博客:http://www.runoob.com/cplusplus/cpp-multithreading.html
参考博客:https://blog.csdn.net/u010223072/article/details/49335867
参考博客:https://blog.csdn.net/wxf2012301351/article/details/73504281
参考书籍:《TCP/IP网络编程 ---尹圣雨》
来源:https://www.cnblogs.com/DOMLX/p/9661012.html