一、多线程开发
线程和进程
程序写好存储在硬盘介质里,CPU读取程序到内存 ,这个在内存中的可执行程序实例就叫进程。一个程序如果多次读取到内存中,那他们就是各自独立的进程
内存中的任何位置都有相应的地址方便访问,而在内存中的进程,自己内部都有一个虚拟独立的地址空间。
每个进程首先有加载的程序,通常只有一个程序计数器,用来记录当前程序执行的位置,会按照程序顺序计算,这里的一个执行流就是一个线程。如果有多个线程,就需要多个程序计数器,每个线程会独立运行。除此之外,每个进程还有寄存器、
堆栈等程序运行时的状态信息,同时线程间共享的则有地址空间、全局变量、打开的文件等等信息。
那么为什么进程中还要有更小的"进程" - 线程
假如进程是一个文档编辑器,存放着相应的程序和文档,现在用户用键盘敲下回车,交互的程序接收键盘的按下事件,布局的程序将文字重新排布渲染出来,另外每隔一段时间,写入的程序保存文档到硬盘中,所以这三个程序最好能并行执行,但他们又需要访问修改同一个文档,所以肯定是在同一个进程中,所以现在需要更轻量级的3个线程,交互线程, 渲染线程,保存线程。
因此,线程是并行的最小单位,假如计算机只有一个单核CPU,即同一时刻只能执行一个线程,对每个线程快速切换轮流执行。为了简化,CPU在内核中为每个线程提供各自虚拟的CPU,每个线程会认为自己独占着CPU,他们就不需要考虑时间片轮转的问题了。
一句人尽皆知的终结:进程是资源分配的最小单位,线程是CPU调度/程序执行的最小单位
并行和并发
假设有多个线程(CPU需要执行的任务),CPU的一个核心挨个挨个处理叫批处理。通过时间片轮转的方式快速切换,就是并发(多线程处理),虽然CPU切换速度非常快宏观上看起来多个线程是并行运行的,但实际不是。批处理和并发的实际处理时间是一样的
而现代的多核系统下,假设系统中有两个核心有两个线程,那么此时操作系统可以把两个线程分配给两个CPU核心,那么此时,这两个线程就可以同时进行了,这就是并行
并行和并发的区别就是在于是否一个核心还是多个核心吗?
实际上更加复杂,上述内容有一个前提就是假设线程之间是独立的,现在假设系统中有两个核心有两个线程,这两个线程需要持有同一把锁,拿到锁的线程才能向前运行,所以此时即使有两个核心也无法做到并行。这也是为什么高性能程序要避免使用锁。
所以说并行和并发不是说单核就是并发,多核就是并行,还要看线程之间的依赖关系。只有在多核,且任务之间没有关系的情况下,才叫真正的并行。当然,多核系统下,如果线程数多余核心数,既有并行也有并发。假设两个核心1,2,4个任务a,b,c,d,ab分配给了核心1,cd分配给了核心2,此时ab就是并发,cd也是并发,a与c就可以并行,b与d也可以并行
网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的"ip地址"可以唯一标识网络中的主机,而传输层的"协议+端口"可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
多线程开发
top ps-ef 都可以查看进程
进程
进程有独立的地址空间
Linux为每个进程创建task struct
每个进程都参与内核调度,互不影响
线程
线程没有独立的内存空间,一个进程内所有线程公用一个
线程仅仅有自己的独立栈空间
使用线程的好处
大大提高了任务切换的效率避免了额外的TLB & cache的刷新
1、创建线程 pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数
pthread_t *thread: 线程id指针,存放tid的
onst pthread_attr_t *attr: 线程属性
void *(*start_routine) (void *): 线程回调函数(函数指针)
void *arg: 函数参数
c
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>
int funct(int a){
printf("hello\n");
pause();
}
int main()
{
pthread_t tid;
int ret=pthread_create(&tid,NULL,funct,NULL);
if(ret<0){
printf("create thread error\n");
}
sleep(2);
pause(); //进程结束,线程也就结束了
return 0;
}
查看线程 ps -eLf | grep progressNAme
查看自己的tid pthread_t pthread_self(void)
2、线程的退出
- 主动退出
return 退出,
void pthread_exit(void* retval) //类似于exit(),直接可以NULL - 被动退出
pthread_cancel(tid); - 资源回收
int pthread_join(pthread_t thread,void **retval); 需要父进程回收,是一个阻塞函数,即会等待线程结束
int pthread_detach(pthread_t thread);
3、线程同步
锁
访问共享资源之前上锁,结束之后上锁。但频繁的加锁解锁是低效的,会完全打破多线程的并发运行;另外多个锁的嵌套使用很有可能导致锁现象
二、Linux多线程TCP服务器
socket函数
int socket(int domain, int type, int protocol); 创建一个通信端点,并返回一个指向该端点的文件描述符
domain : 选择用于通信的协议族。如AF_INET : IPv4 Internet protocols
type: 套接字具有指定的类型,该类型指定了通信语义。 如 SOCK_STREAM 提供有序、可靠、双向、基于连接的字节流。 可支持带外数据传输机制
protocl: 指定与套接字一起使用的特定协议。 通常情况下,在给定的协议族中,只有一种协议支持特定的套接字类型,
在这种情况下,协议可以指定为 0。然而也可能存在多种协议,在这种情况下,必须在本手册中指定特定的协议
memset函数
void *memset(void *s, int c, size_t n); 初始化内存
The memset() function fills the first n bytes of the memory area pointed to by s with the constant byte c
bind函数
给socket帮 ip和端口号
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
当使用 socket() 创建套接字时,它存在于name space中,但没有分配地址。bind() 将 addr 指定的地址分配给文件描述符 sockfd 所指向的套接字。 addrlen 指定 addr 指向的地址结构体的大小。传统上,这一操作被称为 "为套接字分配name"。
On success, zero is returned. On error, -1 is returned
listen函数
socket变为监听模式
int listen(int sockfd, int backlog); listen for connections on a socket
listen() 会将 sockfd 所指向的套接字标记为可连接套接字,即使用 accept() 接受传入连接请求的套接字。
sockfd :文件描述符,指向 SOCK_STREAM 或 SOCK_SEQPACKET 类型的套接字。
backlog :定义了 sockfd 的待处理连接队列可能增长的最大长度。 如果连接请求在队列已满时到达,客户端可能会收到
带有 ECONNREFUSED 指示的错误信息,或者,如果底层协议支持重传,该请求可能会被忽略,以便以后重新尝试连接时获得成功。
accept函数
等待客户端连接,阻塞状态
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); accept a connection on a socket
accept() 用于基于连接的套接字类型(SOCK_STREAM、SOCK_SEQPACKET)。 它从监听套接字、sockfd 的待处理连接队列中
提取第一个连接请求,创建一个新的连接套接字,并返回一个指向该套接字的新文件描述符。 新创建的套接字不处于监听状态。 原始
套接字 sockfd 不受此调用影响。
参数
sockfd :是用 socket() 创建的套接字,用 bind() 绑定到本地地址,并在 listen() 之后监听连接。
addr :指向 sockaddr 结构的指针。 该结构由通信层已知的对等套接字地址填充。 addr 返回地址的具体格式由套接字的地址系列决定。 当 addr
为 NULL 时,将不填写任何内容;在这种情况下,addrlen 将不被使用,也应为 NULL。
addrlen :一个值-结果参数:调用者必须初始化它,使其包含 addr 指向的结构体的大小(字节);
返回:返回一个指向该套接字的新文件描述符。它将包含对等地址的实际大小。如果提供的缓冲区太小,返回的地址将被截断;在这种情况下,addrlen 返回的值将大于调用时提供的值。
recv函数
size_t recv(int sockfd, void *buf, size_t len, int flags); recv, recvfrom, recvmsg - receive a message from a socket
用于接收来自套接字的信息。 它们可用于接收无连接和面向连接套接字上的数据。
recv() 与 read() 的唯一区别在于是否有标志。 如果 flags 参数为零,recv() 通常等同于 read()
如果套接字上没有可用的消息,接收调用将等待消息的到来,除非套接字是非阻塞的,在这种情况下将返回值-1,并将外部变量 errno 设为 EAGAIN 或 EWOULDBLOCK。
接收调用通常会返回任何可用的数据,最多不超过请求的数量,而不是等待收到请求的全部数据。
参数:
int sockfd:已连接的套接字confd,
void *buf: 接收缓冲区
size_t len:希望接收数据的最大字节长度
int flags:
返回值: return the number of bytes received, or -1 if an error occurred. 连接套接字关闭时,返回0
实现单线程
c
#include <stdio.h> // 引入标准输入输出库
#include <stdlib.h> // 引入标准库,用于exit等函数
#include <string.h> // 引入字符串处理库,用于strcmp等函数
#include <unistd.h> // 引入POSIX操作系统API,用于close等函数
#include <sys/socket.h> // 引入套接字编程库
#include <netinet/in.h> // 引入IPv4和IPv6地址定义
#include <arpa/inet.h> // 引入地址转换函数,如inet_addr
#define MAXLINE 4096 // 定义接收缓冲区的大小
int main(int argc, char** argv)
{
int listenfd, connfd; // 分别用于监听和连接的套接字文件描述符
struct sockaddr_in serveraddr; // 存储服务器地址信息的结构体
char buff[MAXLINE]; // 接收消息的缓冲区
int n, ret; // 分别用于存储接收到的字节数和比较结果
// 创建TCP套接字
//netstat a 不仅显示套接字内容的命令,还显示尚未开始通信等状态的所有套接字
// n 显示ip地址和端口号
// o 显示使用该套接字的程序PID
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0); // 创建失败则退出
}
// 初始化服务器地址信息
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET; // 使用IPv4
serveraddr.sin_addr.s_addr = inet_addr("10.47.158.12"); // 设置服务器IP地址
serveraddr.sin_port = htons(6666); // 设置服务器端口号,htons用于主机字节序到网络字节序的转换
// 绑定套接字到服务器地址和端口
if (bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
printf("bind socket error: %s(errno:%d)\n", strerror(errno), errno);
exit(0); // 绑定失败则退出
}
// 监听连接请求
if (listen(listenfd, 10) < 0) {
printf("listen socket error: %s(errno:%d)\n", strerror(errno), errno);
exit(0); // 监听失败则退出
}
printf("=========waiting for client's request.....=====\n"); // 打印等待客户端请求的消息
// 无限循环,接受并处理客户端连接
while(1) {
//如果已建立连接,则不再调用accept,否则调用accept会生成新的connfd ,结果只会打印一次数据
if(connfd==0){
if ((connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1) {
printf("accept socket error:%s(errno:%d)\n", strerror(errno), errno);
continue;
}
}
// 从连接套接字接收消息
n = recv(connfd, buff, MAXLINE, 0);
if (n <= 0) {
// 如果n <= 0,表示连接已关闭或发生错误
close(connfd);
continue;
}
buff[n] = '\0'; // 在接收到的字符串末尾添加空字符
// 比较接收到的消息是否为"exit\n"
ret = strcmp(buff, "exit\n");
if (ret == 0) {
// 如果是,则关闭监听套接字并退出循环
close(listenfd);
close(connfd); // 注意:虽然此时connfd可能不再需要,但关闭是个好习惯
break;
}
printf("recv msg from client: %s\n", buff); // 打印接收到的消息
// 关闭连接套接字(在实际应用中,可能会在此处处理更多数据或保持连接)
close(connfd);
}
// 退出循环后,关闭监听套接字(注意:在上面的"exit"情况下,这一步会被跳过)
close(listenfd);
return 0; // 程序正常退出
}
c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include <arpa/inet.h>
#define MAXLINE 4096
int main(int argc, char** argv)
{
int listenfd, connfd;
struct sockaddr_in serveraddr;
char buff[MAXLINE];
int n, ret;
if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // third param is 0,means select default protocol
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET; //ipv4网络类型
serveraddr.sin_addr.s_addr = (inet_addr("192.168.72.128")); // INADDR_ANY: set addr 0.0.0.0, means all addr
serveraddr.sin_port = htons(1234); // 绑定端口号
if ( bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
printf("bind socket error: %s(errno:%d)\n", strerror(errno), errno);
exit(0);
}
if (listen(listenfd, 10) < 0) {
printf("listen socket error: %s(errno:%d)\n", strerror(errno), errno);
exit(0);
}
printf("=========waiting for client's request.....=====\n");
// if ( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1) {
// printf("accept socket error:%s(errno:%d)\n", strerror(errno), errno);
//continue;
// }
while(1) {
// ccept(listenfd, (struct sockaddr*)&clientaddr, &addrlen); //clientaddr ip地址, &addrlen 端口号
connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
if(connfd<0){
printf("accept socket error:%s(errno:%d)\n", strerror(errno), errno);
return -1;
}
if(connfd>0){
while(1){
n = recv(connfd, buff, MAXLINE, 0); //断开连接后则不再阻塞
if(n==0){
printf("connect %d close\n",connfd);
printf("=========waiting for client's request.....=====\n");
close(connfd);
break;
}
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
ret = strcmp(buff, "exit\n"); //相等返回0,客户端发送exit\n则断开连接
if (ret == 0){
close(connfd);
break;
}
}
}
}
close(listenfd);
return 0;
}
实现多线程
TCP服务器:该代码实现了一个基本的TCP服务器,能够监听指定端口上的连接请求,并接受来自客户端的数据。
多线程处理:服务器使用多线程来同时处理多个客户端连接。每个新连接都会触发一个新线程的创建,该线程负责与该客户端的通信。主线程(main函数中的循环)继续监听新的连接请求,并为每个新请求创建新的线程。这个过程会一直持续下去,直到程序被外部方式(如用户中断)终止。
数据接收与响应:服务器能够接收来自客户端的数据,并打印到标准输出。如果接收到特定的字符串(exit\n),则关闭与该客户端的连接。
错误处理:代码中包含了对套接字操作(如创建、绑定、监听、接受连接)的错误处理,如果操作失败,则会打印错误信息并退出程序(或继续监听其他连接)
c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include <arpa/inet.h>
#include <pthread.h>
#define MAXLINE 4096
void* handle_client(void* arg) {
int connfd = *(int*)arg;
free(arg); // 释放传递的指针
char buff[MAXLINE];
int n;
while (1) {
n = recv(connfd, buff, MAXLINE, 0);
if (n == 0) {
printf("Client %d disconnected\n", connfd);
close(connfd);
break;
}
buff[n] = '\0';
printf("Received from client: %s\n", buff);
if (strcmp(buff, "exit\n") == 0) {
close(connfd);
break;
}
// 在这里可以添加对客户端消息的处理逻辑
}
return NULL;
}
int main(int argc, char** argv)
{
int listenfd, connfd;
struct sockaddr_in serveraddr;
pthread_t tid;
int *new_sock;
char buff[MAXLINE];
int n, ret;
if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // third param is 0,means select default protocol
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
exit(0);
}
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET; //ipv4网络类型
serveraddr.sin_addr.s_addr = (inet_addr("192.168.72.128")); // INADDR_ANY: set addr 0.0.0.0, means all addr
serveraddr.sin_port = htons(1234); // 绑定端口号
if ( bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
printf("bind socket error: %s(errno:%d)\n", strerror(errno), errno);
exit(0);
}
if (listen(listenfd, 10) < 0) {
printf("listen socket error: %s(errno:%d)\n", strerror(errno), errno);
exit(0);
}
printf("=========Waiting for client's request.....=====\n");
while (1) {
connfd = accept(listenfd, NULL, NULL);
if (connfd < 0) {
printf("Accept error: %s (errno: %d)\n", strerror(errno), errno);
continue;
}
// 为新连接创建新线程
new_sock = malloc(sizeof(int));
*new_sock = connfd;
if (pthread_create(&tid, NULL, handle_client, (void*)new_sock) != 0) {
perror("Failed to create thread");
close(connfd);
free(new_sock);
continue;
}
// 可以在这里继续处理其他连接,或者什么都不做(由主线程循环负责)
}
close(listenfd);
return 0;
}
mysql部分
mysql_real_connect
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user,
const char *passwd, const char *db, unsigned int port,
const char *unix_socket, unsigned long clientflag);
mysql
: 指向 MYSQL 结构的指针,这个结构由mysql_init
函数初始化。这个结构包含了连接所需的所有信息。host
: MySQL 服务器的地址,可以是 IP 地址或者域名。如果 host 是 NULL 或 "localhost",客户端将尝试通过 UNIX socket 连接。user
: 连接 MySQL 服务器所使用的用户名。passwd
: 用户的密码。如果密码是 NULL,客户端将尝试使用空密码进行连接。db
: 尝试连接后立即使用的默认数据库名。如果不需要默认数据库,可以将其设置为 NULL。port
: MySQL 服务器的端口号。如果设置为 0,客户端将使用默认的 MySQL 端口(通常是 3306)。unix_socket
: UNIX socket 文件的路径(如果服务器运行在 UNIX 上)。如果 host 参数是 NULL 或 "localhost",并且 unix_socket 不是 - NULL,客户端将尝试通过 UNIX socket 而不是 TCP/IP 连接到服务器。clientflag
: 一个位掩码,用于修改客户端的默认行为。这些标志可以组合使用,以提供对连接行为的更细粒度控制。例如MYSQL_CLIENT_COMPRESS 可以用来请求使用压缩协议。返回值
: 成功,mysql_real_connect 返回一个指向 MYSQL 结构的指针,该结构与传递给函数的指针相同;失败,返回 NULL。你可以通过调用 mysql_error 函数来获取关于连接失败的错误消息。
在使用完 MYSQL 结构后,应使用 mysql_close 函数来关闭连接并释放相关资源。
mysql_real_connect 是线程安全的,但 MYSQL 结构本身不是线程安全的。如果你在多线程环境中工作,每个线程都应该有自己的 MYSQL 结构实例。
mysql_error (指定要使用的 MySQL 连接标识符)
返回值
成功时,如果上一个操作没有错误,则返回空字符串("")。
失败时,返回描述上一个 MySQL 操作的错误的字符串。
c
#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
MYSQL *conn; //MYSQL 是一个结构体类型,它包含了与 MySQL 数据库服务器建立连接所需的所有信息
MYSQL_RES *res; //指向MYSQL_RES的指针,存储结果
MYSQL_ROW row; //一个字符指针数组(char **)的别名,它用于表示结果集中的一行数据
// 初始化 MySQL 连接
conn = mysql_init(NULL);
// 连接到 MySQL 数据库
if (!mysql_real_connect(conn, "localhost", "your_username", "your_password", "testdb", 0, NULL, 0)) {
//fprintf 函数是 C 语言标准库中的一个非常有用的函数,它用于向文件或标准输
fprintf(stderr, "%s\n", mysql_error(conn)); 出(如屏幕)写入格式化的数据。
exit(1);
}
// 插入数据
if (mysql_query(conn, "INSERT INTO users (name) VALUES ('John Doe')")) {
fprintf(stderr, "%s\n", mysql_error(conn));
mysql_close(conn);
exit(1);
}
// 查询数据
if (mysql_query(conn, "SELECT * FROM users")) {
fprintf(stderr, "%s\n", mysql_error(conn));
mysql_close(conn);
exit(1);
}
res = mysql_use_result(conn);
while ((row = mysql_fetch_row(res)) != NULL) {
printf("%s\n", row[1]); // 假设 name 是第二列(索引从 0 开始)
}
mysql_free_result(res);
// 更新数据
if (mysql_query(conn, "UPDATE users SET name = 'Jane Doe' WHERE id = LAST_INSERT_ID()")) {
fprintf(stderr, "%s\n", mysql_error(conn));
mysql_close(conn);
exit(1);
}
// 再次查询以验证更新
if (mysql_query(conn, "SELECT * FROM users")) {
fprintf(stderr, "%s\n", mysql_error(conn));
mysql_close(conn);
exit(1);
}
res = mysql_use_result(conn);
while ((row = mysql_fetch_row(res)) != NULL) {
printf("%s\n", row[1]);
}
mysql_free_result(res);
// 删除数据
if (mysql_query(conn, "DELETE FROM users WHERE id = LAST_INSERT_ID()")) {
fprintf(stderr, "%s\n", mysql_error(conn));
mysql_close(conn);
exit(1);
}
// 关闭数据库连接
mysql_close(conn);
return 0;
}