什么是线程
每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程,可以把线程称作程序执行流的最小单位。每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。可以用下面的图表示,更加直观,协程是更加轻量级的线程,这里就不叙述了。
单线程与多线程
单线程:单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。单线程就是一个进程只有一个线程。
多线程:多线程就是在一个进程内,多个线程并发执行,但并非同时执行,其中存在线程的切换,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换。
了解了多线程的概念,还需要知道多个线程是共享一个进程内的 代码段、堆、全局变量、静态变量、文件等共用资源的,而每个线程又拥有自己的栈区和寄存器。如下图
线程安全:
在多线程环境下,需要保证线程安全,也就是内存的安全。我们知道,多线程是共享进程内的堆区的,也就是任何一个线程都可以访问同一块堆区的内存,试想: 某个线程把堆内某个数据处理到一半就转去干别的事,等到回来重新处理这个数据的时候,可能这个数据已经变化了(因为其他进程也可以同时访问这块区域)。即堆内存空间在没有保护机制的情况下,对多线程来说是不安全的地方,因为你放进去的数据,可能被别的线程“破坏”。
解决这个问题的方法有很多,这里用的是互斥锁。
什么是锁呢,顾名思义,比如:一个线程在操作一个全局变量val时,不想要其他线程进来捣乱,那就可以在操作这个变量时加锁,等到访问完了再放锁,这个时候其他线程才可以拿锁,再进去操作这个全局变量,操作完了再放锁,以此类推。
这样子就可以保证数据的安全性,谁先拿到锁,就可以进去操作共享数据,完了再放锁。
就是类似一群人上卫生间,一个人进去把门锁了,上完后再开门,接着再进去一个人,等等。
线程创建函数为
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
第一个参数thread为指向线程标识符的地址。(可以定义一个pthread_t pthid,传入其地址);
第二个参数attr用于设置线程属性,一般为空,表示使用默认属性。
第三个参数start_routine是线程运行函数的地址。(自己写一个线程执行函数传入)
最后一个参数是线程执行函数的参数。
下面程序是多客户端与服务端的socket通信,为每一个线程客户端与服务端的数据交互 上锁,也就是多线程下同一时段只能有一个客户端和服务端通信。
socket服务端程序(部分)
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
#include <signal.h>
#include <pthread.h>
pthread_mutex_t g_lock; //定义锁变量(锁初始化函数的第一个参数)
void* pthmain(void *arg); //线程执行函数的声明
class TCPserver{
public:
TCPserver(){
listen_sock = 0;
Toclient_sock = 0;
}
~TCPserver(){
if(listen_sock!=0){
close(listen_sock);
}
if(Toclient_sock!=0){
close(Toclient_sock);
}
}
public:
bool initserver(int port);
bool Accept();
ssize_t Send(const void *buf, int len);
ssize_t Recv(void *buf, int len);
void closeListen_sock(){
if(listen_sock > 0){
close(listen_sock);
listen_sock=0;
}
}
void closeClient_sock(){
if(Toclient_sock > 0){
close(Toclient_sock);
Toclient_sock=0;
}
}
public:
int listen_sock;
int Toclient_sock;
};
/*
这里忽略服务端初始化、数据收发函数等函数的实现,因为形式是固定的。主要实现多线程。
注意编译的时候要在末尾加上-lpthread
如:g++ server.cpp -g -o server.out -lpthread
*/
TCPserver test02; // 全局共享对象(多线程共享监听一个socket,连接socket会随着accept更新)
int main(){
if(test02.initserver(5000) == false){
printf("服务端初始化失败\n");
return -1;
}
pthread_t pthid; //线程创建函数第一个参数
pthread_mutex_init(&g_lock,0); //创建线程之前初始化锁
while(1){
if(test02.Accept() == false){ //一旦有新的客户端连接成功,就会启动一个新的线程
printf("服务端接受客户端的连接请求失败\n");
pthread_mutex_destroy(&g_lock); //如果连接失败,要把锁摧毁,防止资源浪费
return -1;
}
pthread_create(&pthid,NULL,pthmain,(void*)test02.Toclient_sock); // 创建一个线程
}
void* pthmain(void *arg){
int m_clientf = (long)arg; //客户端的连接socket
//与客户端通信,接收客户端发过来的报文后 回复"我收到了"
char strbuffer[1024];
while(1){
pthread_mutex_lock(&g_lock); //加锁
if(recv(m_clientf,strbuffer,sizeof(strbuffer),0)<=0){ //为什么不用test02.Recv呢
//因为每个线程是独立的,将连接socket传给该进程后
//该进程调用recv自己处理数据
pthread_mutex_unlock(&g_lock);
close(m_clientf);
pthread_exit(0); //不能用exit退出线程,因为exit会直接把整个进程退了,不只是一个线程
}
printf("接收: %s\n",strbuffer);
strcpy(strbuffer,"我收到了.");
if(send(m_clientf,strbuffer,strlen(strbuffer),0)<=0)
{
pthread_mutex_unlock(&g_lock); //防止线程在这里终止,需要解锁
close(m_clientf);
pthread_exit(0); //不能用exit退出线程,因为exit会直接把整个进程退了,不只是一个线程
}
printf("发送: %s\n",strbuffer);
printf("\n");
pthread_mutex_unlock(&g_lock);
}
close(m_clientf);
pthread_exit(0);
}
客户端程序
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
class TCPclient{
public:
TCPclient(){
sock_clientfp=0;
}
~TCPclient(){
if(sock_clientfp > 0 ){
close(sock_clientfp); //在析构函数中关闭socket
}
}
public:
bool connect_Server(const char *serverIP,int port); //连接服务端需要做的事情,也就是一些函数的使用和初始化
ssize_t Send(const void *buf, size_t len); //发送数据
ssize_t Recv(void *buf, size_t len); //接收数据
//这两个函数都会等待服务端的回复
private:
int sock_clientfp;
};
bool TCPclient::connect_Server(const char *serverIP,int port){
sock_clientfp = socket(AF_INET,SOCK_STREAM,0); //创建客户端的socket
struct hostent *h; //IP地址的结构体
if((h = gethostbyname(serverIP)) == 0){ //将传进来的域名转换为相应的IP地址
close(sock_clientfp);
sock_clientfp = 0;
return false;
}
//把服务器的地址和端口转换为数据结构体
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; //协议族
servaddr.sin_port = htons(port); //绑定通讯端口
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
//向服务端发起连接请求
if(connect(sock_clientfp,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0){
close(sock_clientfp);
sock_clientfp = 0; //这里手动置零比较严谨
return false;
}
return true;
}
ssize_t TCPclient::Send(const void *buf, size_t len){
return send(Toclient_sock,buf,len,0);
}
ssize_t TCPclient::Recv(void *buf, size_t len){
memset(buf,0,len);
return recv(Toclient_sock,buf,len,0);
}
int main(){
TCPclient tcptest01; //构建一个对象
if(tcptest01.connect_Server("xxx.xxx.xx.xxx",5000)==false){ //xxx这里写你要连接的服务器IP地址
printf("连接服务器(xxx.xxx.xx.xxx) 失败!\n");
return -1;
}
//与服务端通信,发送一个报文后等待回复,然后等待下一个报文
char strbuffer[1024];
for(int i = 0; i < 5; i++){
memset(strbuffer,0,sizeof(strbuffer));
sprintf(strbuffer,"这是第%d个报文.",i);
if((tcptest01.Send(strbuffer,strlen(strbuffer))) <= 0){ //发送失败
break;
}
printf("发送: %s\n",strbuffer);
if(tcptest01.Recv(strbuffer,strlen(strbuffer)) <= 0){ //接收失败
break;
}
printf("接收: %s\n",strbuffer);
printf("\n");
}
return 0;
}
可以看到,从左到右依次为服务端,客户端1,客户端2,客户端3,同一时段只能有一个客户端进去数据交互,另一些客户端都会阻塞,等待放锁。