当前位置: 首页 > article >正文

POSIX多线程

在计算机编程的广阔领域中,POSIX 标准就像是一把通用的钥匙,开启了跨平台编程的大门。POSIX,即 Portable Operating System Interface(可移植操作系统接口) ,是 IEEE 为了规范各种 UNIX 操作系统提供的 API 接口而定义的一系列互相关联标准的总称。它的出现,旨在解决不同 UNIX 系统之间接口不一致的问题,让开发者能够编写一次代码,在多个符合 POSIX 标准的系统上运行,实现源代码级别的软件可移植性。

对于多线程编程而言,POSIX 标准同样意义非凡。在多核处理器盛行的今天,多线程编程成为充分利用硬件资源、提高程序性能的关键技术。POSIX 标准定义了一套清晰、规范的多线程编程接口,让开发者可以在不同的操作系统环境中,以统一的方式创建、管理线程,以及处理线程之间的同步和通信问题 。无论是开发高性能的服务器程序,还是优化计算密集型的应用,POSIX 标准下的多线程编程都能提供强大的支持。

接下来,让我们深入探索 POSIX 标准下的多线程编程世界,揭开线程创建、同步机制等核心概念的神秘面纱。

一、多线程编程简介

1.1线程初印象

线程,作为进程内的执行单元,可以理解为进程这个大舞台上的一个个小舞者,各自有着独立的舞步(执行路径),却又共享着舞台的资源(进程资源)。与进程相比,线程更加轻量级。进程是系统进行资源分配和调度的基本单位,拥有独立的地址空间、内存、文件描述符等资源 ,进程间的切换开销较大。而线程则是共享所属进程的资源,它们之间的切换开销相对较小,就像在同一个舞台上不同舞者之间的快速换位,无需重新搭建整个舞台。

线程的这些特点,使得多线程编程在提升程序执行效率上有着独特的优势。多个线程可以并发执行,充分利用多核处理器的并行计算能力,将复杂的任务分解为多个子任务,每个子任务由一个线程负责处理,从而大大提高了程序的整体运行速度。例如,在一个网络服务器程序中,一个线程可以负责监听客户端的连接请求,另一个线程负责处理已经建立连接的客户端的数据传输,这样可以同时处理多个客户端的请求,提升服务器的响应性能 。

1.2POSIX 线程库

在 POSIX 标准下,进行多线程编程离不开 POSIX 线程库(pthread 库)。它就像是一根神奇的魔法棒,为开发者提供了一系列强大的接口函数,让我们能够轻松地操控线程。

其中,pthread_create函数用于创建一个新的线程 ,它的原型如下:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

thread参数用于返回新创建线程的 ID;attr参数用于设置线程的属性,如果为NULL则使用默认属性;start_routine是一个函数指针,指向线程开始执行时调用的函数;arg是传递给start_routine函数的参数。

而pthread_join函数则用于等待一个线程结束,其原型为:

int pthread_join(pthread_t thread, void **retval); 

 thread参数是要等待结束的线程 ID,retval用于获取线程结束时的返回值。

下面是一个简单的使用pthread_create和pthread_join函数的代码示例:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>// 线程执行的函数
void* thread_function(void* arg) {printf("线程开始执行,参数为: %s\n", (char*)arg);sleep(2);  // 模拟线程执行任务printf("线程执行结束\n");return (void*)1;  // 返回线程执行结果
}int main() {pthread_t thread;int res;void* thread_result;// 创建线程res = pthread_create(&thread, NULL, thread_function, (void*)"Hello, Thread!");if (res != 0) {perror("线程创建失败");return 1;}printf("等待线程结束...\n");// 等待线程结束,并获取线程返回值res = pthread_join(thread, &thread_result);if (res != 0) {perror("线程等待失败");return 1;}printf("线程已结束,返回值为: %ld\n", (long)thread_result);return 0;
}

1.3线程的生命周期

线程如同一个有生命的个体,有着自己完整的生命周期,从创建的那一刻开始,经历运行、阻塞、唤醒等阶段,最终走向结束。

当我们调用pthread_create函数时,线程就诞生了,此时它处于就绪状态,等待着 CPU 的调度。一旦获得 CPU 时间片,线程就进入运行状态,开始执行它的任务,也就是调用我们指定的函数 。

在运行过程中,线程可能会因为某些原因进入阻塞状态。比如,当线程调用sleep函数时,它会主动放弃 CPU 使用权,进入睡眠状态,直到睡眠时间结束才会重新回到就绪状态,等待再次被调度执行 。又或者,当线程访问共享资源时,如果资源被其他线程占用,它就需要等待,从而进入阻塞状态,直到获取到资源才会被唤醒,重新进入运行状态。

当线程执行完它的任务,也就是指定的函数返回时,线程就进入了结束状态。此时,我们可以通过pthread_join函数等待线程结束,并获取它的返回值 ,也可以在创建线程时将其设置为分离状态,这样线程结束后资源会自动被回收,无需等待。了解线程的生命周期,有助于我们更好地管理线程,优化程序的性能 。

二、Posix网络API

server.cpp

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<sys/types.h>
#include<arpa/inet.h>int main(int argc,char* argv[]){if (argc != 2){printf("Using:./server port\nExample:./server 5005\n\n"); return -1;}//第一步:创建服务端的socketint listenfd;if((listenfd=socket(AF_INET,SOCK_STREAM,0))==-1){perror("socket error!");return -1;}//第二步:把服务端用于通信的地址和端口绑定到socket上。struct sockaddr_in serveraddr;  //服务端信息结构体memset(&serveraddr,0,sizeof(serveraddr));serveraddr.sin_family=AF_INET;serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);  //任意ip地址//servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); // 指定ip地址。serveraddr.sin_port=htons(atoi(argv[1]));  //指定通信端口if(bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))!=0){perror("bind error");close(listenfd);return -1;}//第三步:把socket设置为监听模式if(listen(listenfd,5)==-1){perror("listen error");close(listenfd);return -1;}//第四步:接受客户端的连接int clientfd;  //连接上来的客户端socketint socklen = sizeof(struct sockaddr_in);struct sockaddr_in clientaddr;  //客户端地址信息clientfd = accept(listenfd,(struct sockaddr*)&clientaddr,(socklen_t*)&socklen);printf("client (%s) connect server success。。。\n",inet_ntoa(clientaddr.sin_addr));// 第5步:与客户端通信,接收客户端发过来的报文后,将该报文原封不动返回给客户端。char buffer[1024];// memset(buffer, 0, 1024);while(1){int ret;memset(buffer,0,sizeof(buffer));if((ret=recv(clientfd,buffer,sizeof(buffer),0))<=0){printf("ret = %d , client disconected!!!\n", ret);break;}printf("recv msg: %s\n", buffer);// 向客户端发送响应结果。if((ret=send(clientfd,buffer,strlen(buffer),0))<=0){perror("send error");break;}printf("response client: %s success...\n", buffer);}//第六步:关闭socket,释放资源close(listenfd);close(clientfd);return 0;}

 client.cpp

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>int main(int argc,char* argv[]){if(argc!=3){printf("Using:./client ip port\nExample:./client 127.0.0.1 5005\n\n");return -1;}//第一步:创建客户端的socketint sockfd;if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){perror("socket error");return -1;}//第二步:向服务器发起连接请求struct hostent* h;if((h=gethostbyname(argv[1]))==0){  //指定服务器的ip地址printf("gethostbyname failed.\n");close(sockfd);return -1;}struct sockaddr_in serveraddr;memset(&serveraddr,0,sizeof(serveraddr));serveraddr.sin_family=AF_INET;serveraddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。memcpy(&serveraddr.sin_addr,h->h_addr,h->h_length);//向服务端发起连接请求if(connect(sockfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))!=0){perror("connect error");close(sockfd);return -1;}char buffer[1024];// 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文for(int i=1;i<=3;i++){int ret;memset(buffer,0,sizeof(buffer));sprintf(buffer,"这是第%d条消息!",i);if((ret=send(sockfd,buffer,strlen(buffer),0))<=0){perror("send error");close(sockfd);return -1;}printf("发送:%s\n", buffer);if((ret=recv(sockfd,buffer,sizeof(buffer),0))<=0){//接受服务端的回应报文printf("ret = %d error\n", ret);break;}printf("从服务端接收:%s\n",buffer);sleep(1);}//关闭socket,释放资源close(sockfd);return 0;}

服务端执行:

g++ server.cpp -o server

./server 5555

客户端执行:

g++ client.cpp -o client

./client 127.0.0.1 5555 

 函数

(1)socket函数

int socket(int domain, int type, int protocol);

调用socket()函数会创建一个套接字(socket)对象。套接字由两部分组成,文件描述符(fd)和 TCP控制块(Tcp Control Block,tcb) 。Tcb主要包括关系信息有网络的五元组(remote IP,remote Port, local IP, local Port, protocol),一个五元组就可以确定一个具体的网络连接。

(2)listen函数

listen(int listenfd, backlog); 

服务端在调用listen()后,就开始监听网络上连接请求。第二个参数 backlog, 在Linux是指全连接队列的长度,即一次最多能保存 backlog 个连接请求。 

(3)connect 函数 

客户端调用connect()函数,向指定服务端发起连接请求。

(4)accept 函数

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept()函数只做两件事,将连接请求从全连接队列中取出,给该连接分配一个fd并返回。

(6)send/recv 函数

至此,客户端与服务端已经成功建立连接,就可以相互通信了。

send/recv函数主要负责数据的收发。

过程分析

send函数:负责将数据从用户空间拷贝到内核(具体是拷贝到该连接对应的Tcb控制块中的发送缓冲区)。注意:send函数返回并不意味着数据已成功发送,因为数据在到达内核缓冲区后,内核会根据自己的策略决定什么时候将数据发出。

recv函数:负责将数据从内核缓冲区拷贝到用户空间。同理,数据也显示到达该连接对应的Tcb控制块的接受缓冲区。

(7)close 函数

 在服务器与客户端建立连接之后,会进行一些读写操作,完成读写操作后我们需要关闭相应的socket,好比操作完打开的文件要调用fclose关闭打开的文件一样。close过程涉及到四次挥手的全过程

三次握手过程分析

三次握手与listen/connect/accept三个函数有关,这里放到一起进行描述。

客户端调用 connect 函数,开始进入三次握手。客户端发送syn包,以及带着随机的seq;

服务端listen函数监听到有客户端连接,listen函数会在内核协议栈为该客户端创建一个Tcb控制块,并将其加入到半连接队列。服务端在收到syn包后,会给客户端恢复ack和syn包;

客户端收到服务端的ack和syn后再次恢复ack,连接建立成功。

服务端在收到客户端的ack后,会将该客户端对应的Tcb数据从半连接队列移动到全连接队列。只要全连接队列中有数据就会触发accept,返回连接成功的客户端fd、IP以及端口。此时,Tcb完整的五元组构建成功。

四次挥手流程

  • 客户端调用close函数,内核会发送fin包,客户端进入fin_wait1状态;

  • 服务端收到fin包回复ack,客户端进入close_wait状态。此时,客户客户端往服务端发送的通道就关闭了,因为Tcp是全双工的,服务端还可以向客户端发数据。

  • 客户端收到ack,进入到fin_wait2状态;

  • 服务端发送完数据,发送fin包,服务端进入last_ack状态;

  • 客户端收到fin包后,回复ack,进入到time_wait状态;

  • 服务端收到ack,双方连接正常关闭。

注意:close操作只是让相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求

2.3常见面试问题

为什么要三次握手?

答:因为一个完整的TCP连接需要双方都得到确认,客户端发送请求和收到确认需要两次;服务端发送请求和收到确认需要两次,当中服务回复确认和发送请求合并为一次总共需要3次;才能保证双向通道是通的。

一个服务器的端口数是65535,为何能做到一百万的连接?

答:主要是因为一条连接是由五元组所组成,所以一个服务器的连接数是五个成员数的乘积。

如何应对Dos(Deny of Service,拒绝服务)攻击?

答:Dos攻击就是利用三次握手的原理,模拟客户端只向服务器发送syn包,然后耗尽被攻击对象的资源。比较多的做法是利用防火墙,做一些过滤规则

如何解决Tcp的粘包问题?

答:(1) 在包头上添加一个数据包长度的字段,用于数据的划分,实际项目中这个也用的最多;(2)包尾部加固定分隔符;

Tcp如何保证顺序到达?

答:顺序到达是由于TCP的延迟ACK的机制来保证的,TCP接收到数据并不是立即回复而是经过一个延迟时间,回复接收到连续包的最大序列号加1。如果丢包之后的包都需要重传。在弱网情况下这里就会有实时性问题和带宽占用的问题;

time_wait 作用?

答:防止最后一个ACK没有顺利到达对方,超时重新发送ack。time_wait时常一般是120s可以修改。

服务器掉线重启出现端口被占用怎么办?

答:其实主要是由于还处于time_wait状态,端口并没有真正释放。这时候可以设置SO_REUSEADDR属性,保证掉线能马上重连。

三、同步机制:多线程协作的 “指挥家”

在多线程编程的舞台上,同步机制就像是一位经验丰富的指挥家,协调着各个线程的行动,确保它们能够和谐共处,高效地完成任务。多线程编程中,由于多个线程共享进程资源,资源竞争和线程协作问题不可避免,而同步机制正是解决这些问题的关键。接下来,我们将深入探讨互斥锁信号量条件变量这几种常见的同步机制 。

3.1资源竞争:多线程中的 “暗礁”

当多个线程同时访问和修改共享资源时,资源竞争问题就如同隐藏在暗处的暗礁,随时可能让程序的运行陷入混乱。假设我们有一个简单的程序,包含两个线程,它们都试图对一个全局变量进行加 1 操作:

#include <stdio.h>
#include <pthread.h>// 全局变量
int global_variable = 0;// 线程执行函数
void* thread_function(void* arg) {for (int i = 0; i < 1000000; i++) {global_variable++;}return NULL;
}int main() {pthread_t thread1, thread2;// 创建线程pthread_create(&thread1, NULL, thread_function, NULL);pthread_create(&thread2, NULL, thread_function, NULL);// 等待线程结束pthread_join(thread1, NULL);pthread_join(thread2, NULL);printf("最终的全局变量值: %d\n", global_variable);return 0;
}

按照我们的预期,两个线程各对全局变量加 1000000 次,最终的结果应该是 2000000。然而,实际运行这个程序,你会发现结果往往小于 2000000。这是因为在多线程环境下,global_variable++ 这一操作并非原子操作,它实际上包含了读取变量值、加 1、写回变量值这三个步骤 。当两个线程同时执行这一操作时,可能会出现一个线程读取了变量值,还未完成加 1 和写回操作,另一个线程也读取了相同的值,导致最终结果出现偏差,数据不一致 。

3.2互斥锁:守护资源的 “卫士”

互斥锁(Mutex)是解决资源竞争问题的常用工具,它就像一位忠诚的卫士,守护着共享资源,确保同一时间只有一个线程能够访问资源。互斥锁的工作原理基于一个简单的概念:当一个线程获取到互斥锁时,其他线程就必须等待,直到该线程释放互斥锁。

在 POSIX 线程库中,使用互斥锁非常简单。首先,我们需要定义一个互斥锁变量:

pthread_mutex_t mutex;

然后,在访问共享资源之前,通过 pthread_mutex_lock 函数获取互斥锁:

pthread_mutex_lock(&mutex); 

如果互斥锁已经被其他线程持有,调用 pthread_mutex_lock 的线程将被阻塞,直到互斥锁被释放。

当访问完共享资源后,使用 pthread_mutex_unlock 函数释放互斥锁:

pthread_mutex_unlock(&mutex); 

 下面是使用互斥锁改进后的代码:

#include <stdio.h>
#include <pthread.h>// 全局变量
int global_variable = 0;
// 互斥锁
pthread_mutex_t mutex;// 线程执行函数
void* thread_function(void* arg) {for (int i = 0; i < 1000000; i++) {// 获取互斥锁pthread_mutex_lock(&mutex);global_variable++;// 释放互斥锁pthread_mutex_unlock(&mutex);}return NULL;
}int main() {pthread_t thread1, thread2;// 初始化互斥锁pthread_mutex_init(&mutex, NULL);// 创建线程pthread_create(&thread1, NULL, thread_function, NULL);pthread_create(&thread2, NULL, thread_function, NULL);// 等待线程结束pthread_join(thread1, NULL);pthread_join(thread2, NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);printf("最终的全局变量值: %d\n", global_variable);return 0;
}

通过这种方式,互斥锁有效地保护了共享资源,确保了数据的一致性 。

3.3信号量:资源分配的 “调度员”

信号量(Semaphore)是另一种强大的同步工具,它不仅可以用于实现互斥,还能用于管理资源的分配。信号量可以看作是一个计数器,它的值表示可用资源的数量 。当一个线程想要访问资源时,它需要先获取信号量,如果信号量的值大于 0,则表示有可用资源,线程可以获取信号量并继续执行,同时信号量的值减 1;如果信号量的值为 0,则表示没有可用资源,线程将被阻塞,直到有其他线程释放信号量 。

在 POSIX 标准中,信号量相关的函数主要有 sem_init(初始化信号量)、sem_wait(等待信号量)、sem_post(释放信号量)和 sem_destroy(销毁信号量)。假设我们有一个场景,有多个线程需要访问有限数量的资源,比如数据库连接池中的连接。我们可以使用信号量来控制对这些资源的访问:

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>// 定义信号量,假设有5个可用资源
sem_t semaphore;// 线程执行函数
void* thread_function(void* arg) {// 等待信号量sem_wait(&semaphore);printf("线程获取到资源,开始执行任务...\n");// 模拟任务执行sleep(1);printf("线程任务执行完毕,释放资源\n");// 释放信号量sem_post(&semaphore);return NULL;
}int main() {pthread_t threads[10];// 初始化信号量,设置初始值为5sem_init(&semaphore, 0, 5);// 创建10个线程for (int i = 0; i < 10; i++) {pthread_create(&threads[i], NULL, thread_function, NULL);}// 等待所有线程结束for (int i = 0; i < 10; i++) {pthread_join(threads[i], NULL);}// 销毁信号量sem_destroy(&semaphore);return 0;
}

在这个例子中,我们初始化信号量的值为 5,表示有 5 个可用资源。每个线程在执行任务前先通过 sem_wait 等待信号量,获取到信号量后才能访问资源,执行完任务后通过 sem_post 释放信号量,这样就保证了同时最多只有 5 个线程可以访问资源 。

3.4条件变量:线程间的 “传声筒”

条件变量(Condition Variable)用于线程间基于条件的通信,它为线程提供了一种等待特定条件发生的机制,就像一个传声筒,让线程之间能够相互传达信息。条件变量通常与互斥锁配合使用,以实现线程之间的同步和协作。

一个经典的例子是生产者 - 消费者模型。在这个模型中,生产者线程负责生成数据并将其放入缓冲区,消费者线程则从缓冲区中取出数据进行处理。当缓冲区为空时,消费者线程需要等待,直到生产者线程向缓冲区中放入数据;当缓冲区满时,生产者线程需要等待,直到消费者线程从缓冲区中取出数据 。

下面是使用条件变量和互斥锁实现生产者 - 消费者模型的代码示例:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0, out = 0;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;// 生产者线程函数
void* producer(void* arg) {while (1) {int item = rand() % 100; // 生成一个随机数pthread_mutex_lock(&mutex);while ((in + 1) % BUFFER_SIZE == out) { // 缓冲区满pthread_cond_wait(&not_full, &mutex);}buffer[in] = item;printf("生产者放入数据: %d\n", item);in = (in + 1) % BUFFER_SIZE;pthread_cond_signal(&not_empty);pthread_mutex_unlock(&mutex);sleep(rand() % 2); // 模拟生产时间}return NULL;
}// 消费者线程函数
void* consumer(void* arg) {while (1) {pthread_mutex_lock(&mutex);while (in == out) { // 缓冲区空pthread_cond_wait(&not_empty, &mutex);}int item = buffer[out];printf("消费者取出数据: %d\n", item);out = (out + 1) % BUFFER_SIZE;pthread_cond_signal(&not_full);pthread_mutex_unlock(&mutex);sleep(rand() % 3); // 模拟消费时间}return NULL;
}int main() {pthread_t producer_thread, consumer_thread;// 创建生产者和消费者线程pthread_create(&producer_thread, NULL, producer, NULL);pthread_create(&consumer_thread, NULL, consumer, NULL);// 等待线程结束pthread_join(producer_thread, NULL);pthread_join(consumer_thread, NULL);// 销毁互斥锁和条件变量pthread_mutex_destroy(&mutex);pthread_cond_destroy(&not_empty);pthread_cond_destroy(&not_full);return 0;
}

在这个代码中,pthread_cond_wait 函数会使线程进入等待状态,并自动释放互斥锁,当条件满足被唤醒时,会重新获取互斥锁。pthread_cond_signal 函数则用于唤醒等待在条件变量上的一个线程。通过条件变量和互斥锁的紧密配合,生产者和消费者线程能够有条不紊地工作,实现高效的数据处理 。

四、多线程编程实战演练

4.1多线程案例分析

在日常的编程工作中,文件处理是一项常见的任务。当面对大量文件需要处理时,单线程的处理方式往往效率低下,而多线程编程则能成为提升效率的利器。假设我们有一个需求:处理一批日志文件,需要统计每个文件中特定关键词出现的次数,并将结果汇总。

为了实现这个目标,我们可以设计一个多线程的文件处理方案。首先,将文件列表进行分割,把不同的文件分配给不同的线程处理,这就像是将一堆任务分配给不同的工人,每个工人专注于自己手头的任务 。每个线程负责读取分配给自己的文件内容,逐行扫描,统计关键词出现的次数。

这个过程中,线程之间的同步机制至关重要。我们可以使用互斥锁来保护共享的统计结果变量,确保不同线程在更新统计结果时不会出现数据竞争问题 。比如,当一个线程统计完自己负责文件后,需要将统计结果累加到全局的统计变量中,此时通过获取互斥锁,保证同一时间只有一个线程能够进行累加操作,避免了数据不一致的情况 。

 4.2代码实现示例

下面是使用 POSIX 线程库实现多线程文件处理的具体代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>#define MAX_FILES 10
#define KEYWORD "error"  // 要统计的关键词// 线程参数结构体
typedef struct {char *file_name;
} ThreadArgs;// 全局统计变量
int global_count = 0;
// 互斥锁
pthread_mutex_t mutex;// 线程执行函数
void* count_keyword(void* arg) {ThreadArgs *args = (ThreadArgs*)arg;FILE *file = fopen(args->file_name, "r");if (file == NULL) {perror("文件打开失败");pthread_exit(NULL);}char line[1024];int local_count = 0;while (fgets(line, sizeof(line), file) != NULL) {if (strstr(line, KEYWORD) != NULL) {local_count++;}}fclose(file);// 获取互斥锁,更新全局统计变量pthread_mutex_lock(&mutex);global_count += local_count;pthread_mutex_unlock(&mutex);pthread_exit(NULL);
}int main() {pthread_t threads[MAX_FILES];ThreadArgs args[MAX_FILES];char file_names[MAX_FILES][50] = {"file1.log", "file2.log", "file3.log", "file4.log", "file5.log", "file6.log", "file7.log", "file8.log", "file9.log", "file10.log"};// 初始化互斥锁pthread_mutex_init(&mutex, NULL);// 创建线程并分配文件for (int i = 0; i < MAX_FILES; i++) {args[i].file_name = file_names[i];if (pthread_create(&threads[i], NULL, count_keyword, &args[i]) != 0) {perror("线程创建失败");return 1;}}// 等待所有线程结束for (int i = 0; i < MAX_FILES; i++) {if (pthread_join(threads[i], NULL) != 0) {perror("线程等待失败");return 1;}}// 销毁互斥锁pthread_mutex_destroy(&mutex);printf("关键词 '%s' 出现的总次数: %d\n", KEYWORD, global_count);return 0;
}

在这段代码中,count_keyword 函数是线程执行的主体,它打开分配的文件,逐行读取并统计关键词出现的次数,最后通过互斥锁将本地统计结果累加到全局变量中 。main 函数负责创建线程,为每个线程分配文件,并等待所有线程执行完毕后输出最终的统计结果 。 

4.3多线程调试与优化

在多线程程序的调试过程中,我们可能会遇到各种各样的问题。死锁是一个常见的问题,比如两个线程分别持有不同的锁,却又试图获取对方持有的锁,就会陷入死锁状态,导致程序无法继续执行 。为了检测死锁,可以使用工具如Valgrind的Helgrind工具,它能够帮助我们发现潜在的死锁问题。一旦发现死锁,我们需要仔细检查代码中锁的获取和释放顺序,避免嵌套锁的不合理使用 。

线程异常也是需要关注的问题。当线程执行过程中出现未捕获的异常时,可能会导致整个程序崩溃。我们可以在线程函数中使用try - catch块(如果是 C++ 代码)或者进行适当的错误处理,确保线程在遇到异常时能够安全地退出,而不影响其他线程的正常运行 。

在优化方面,合理调整线程数量是一个重要的思路。线程数量并非越多越好,过多的线程会导致上下文切换开销增大,反而降低程序性能 。对于 CPU 密集型的任务,线程数量可以设置为接近 CPU 核心数;对于 I/O 密集型的任务,由于线程在等待 I/O 操作时会阻塞,不会占用 CPU 资源,因此可以适当增加线程数量 。此外,优化同步机制也能提升性能,比如使用更细粒度的锁,减少锁的竞争范围,或者在合适的场景下使用无锁数据结构,避免锁带来的开销 。通过不断地调试和优化,我们能够让多线程程序更加稳健高效地运行 。

相关文章:

POSIX多线程

在计算机编程的广阔领域中&#xff0c;POSIX 标准就像是一把通用的钥匙&#xff0c;开启了跨平台编程的大门。POSIX&#xff0c;即 Portable Operating System Interface&#xff08;可移植操作系统接口&#xff09; &#xff0c;是 IEEE 为了规范各种 UNIX 操作系统提供的 API…...

济南国网数字化培训班学习笔记-第二组-1节-输电线路工程

输电线路工程 输电 电网定义 将发电场采集的电能通过输电线路传输到用户终端。由输电线路、变电站和配电网络等组成。 六精四化 安全、质量、进度、造价、技术、队伍 标准化&#xff0c;模块化&#xff0c;机械化&#xff0c;智能化 发展历程 1908-22kv-石龙坝水电-昆明…...

相机雷达外参标定算法调研

0. 简介 相机与激光雷达的外参标定是自动驾驶、机器人等领域的基础工作。精准的标定不仅有助于提高数据融合的效果&#xff0c;还能提升算法的整体性能。随着技术的发展&#xff0c;许多研究者和公司致力于开发高效的标定工具和算法&#xff0c;本文将对无目标标定和有目标标定…...

网络原理 - 7(TCP - 4)

目录 6. 拥塞控制 7. 延时应答 8. 捎带应答 9. 面向字节流 10. 异常情况 总结&#xff1a; 6. 拥塞控制 虽然 TCP 有了滑动窗口这个大杀器&#xff0c;就能够高效可靠的发送大量的数据&#xff0c;但是如果在刚开始阶段就发送大量的数据&#xff0c;仍然可能引起大量的…...

JAVA---面向对象(上)

今天写重生之我开始补知识 第二集 面向对象编程&#xff1a;拿东西过来做对应的事。 设计对象并使用 1.类和对象 类&#xff08;设计图&#xff09;&#xff1a;是对象共同特征的描述&#xff1b; 对象&#xff1a;是具体存在的具体东西&#xff1b; 如何定义类&#xf…...

idea连接远程服务器kafka

一、idea插件安装 首先idea插件市场搜索“kafka”进行插件安装 二、kafka链接配置 1、检查服务器kafka配置 配置链接前需要保证远程服务器的kafka配置里边有配置好服务器IP&#xff0c;以及开放好kafka端口9092&#xff08;如果有修改 过端口的开放对应端口就好&#xff09; …...

Linux操作系统--基础I/O(上)

目录 1.回顾C文件接口 stdin、stdout、stderr 2.系统文件I/O 3.接口介绍 4.open函数返回值 5.文件描述符fd 5.1 0&1&2 1.回顾C文件接口 hello.c写文件 #include<stdio.h> #include<string.h>int main() {FILE *fp fopen("myfile","…...

IOMUXC_SetPinMux的0,1参数解释

IOMUXC_SetPinMux(IOMUXC_ENET1_RX_DATA0_FLEXCAN1_TX, 0); 这里的第二个参数 0 实际上传递给了 inputOnfield&#xff0c;它控制的是 SION&#xff08;Software Input On&#xff09;位。 当 inputOnfield 为 0 时&#xff0c;SION 关闭&#xff0c;此时引脚的输入/输出方向由…...

go 的 net 包

目录 一、net包的基本功能 1.1 IP地址处理 1.2 网络协议支持 1.3 连接管理 二、net包的主要功能模块 2.1 IP地址处理 2.2 TCP协议 2.3 UDP协议 2.4 Listener和Conn接口 三、高级功能 3.1 超时设置 3.2 KeepAlive控制 3.3 获取连接信息 四、实际应用场景 4.1 Web服…...

weibo_har鸿蒙微博分享,单例二次封装,鸿蒙微博,微博登录

weibo_har鸿蒙微博分享&#xff0c;单例二次封装&#xff0c;鸿蒙微博 HarmonyOS 5.0.3 Beta2 SDK&#xff0c;原样包含OpenHarmony SDK Ohos_sdk_public 5.0.3.131 (API Version 15 Beta2) &#x1f3c6;简介 zyl/weibo_har是微博封装使用&#xff0c;支持原生core使用 &a…...

【MySQL数据库入门到精通-06 DCL操作】

一、DCL DCL英文全称是Data Control Language(数据控制语言)&#xff0c;用来管理数据库用户、控制数据库的访 问权限。 二、管理用户 1.查询与创建用户 代码如下&#xff08;示例&#xff09;&#xff1a; -- DCL 管理用户 -- 1.查询用户 use mysql; select *from user;-…...

第55讲:农业人工智能的跨学科融合与社会影响——构建更加可持续、包容的农业社会

目录 一、农业人工智能的多维融合:科技与社会的桥梁 1. 技术与社会:解决现代农业中的不平等 2. AI与伦理:塑造道德规范与社会责任 3. AI与政策:推动农业政策的科学决策与智能执行 二、AI与农业未来社会的构建:更绿色、更智能、更包容 1. 推动农业可持续发展:绿色农…...

nodejs之Express-介绍、路由

五、Express 1、express 介绍 express 是一个基于 Node.js 平台的极简、灵活的 WEB 应用开发框架,官方网址: https://www.expressjs.com.cn/ 简单来说,express 是一个封装好的工具包,封装了很多功能,便于我们开发 WEB 应用(HTTP 服务) (1)基本使用 第一步:初始化项目并…...

无感字符编码原址转换术——系统内存(Mermaid文本图表版/DeepSeek)

安全便捷无依赖&#xff0c;不学就会无感觉。 笔记模板由python脚本于2025-04-24 20:00:05创建&#xff0c;本篇笔记适合正在研究字符串编码制式的coder翻阅。 学习的细节是欢悦的历程 博客的核心价值&#xff1a;在于输出思考与经验&#xff0c;而不仅仅是知识的简单复述。 P…...

ecovadis认证需要提供哪些文件?ecovadis认证优势是什么?

EcoVadis认证详解&#xff1a;所需文件与核心优势 一、EcoVadis认证需要提供哪些文件&#xff1f; EcoVadis评估基于企业提交的ESG&#xff08;环境、社会、治理&#xff09;相关文档&#xff0c;具体包括以下四类核心主题的文件&#xff1a; 1. 环境&#xff08;Environment…...

第七部分:向量数据库和索引策略

什么是矢量数据库&#xff1f; 简单来说&#xff0c;向量数据库是一种专门化的数据库&#xff0c;旨在优化存储和检索以高维向量形式表示的文本。 为什么这些数据库对RAG至关重要&#xff1f;因为向量表示能够在大规模文档库中进行高效的基于相似性的搜索&#xff0c;根据用户…...

Java 2025 技术全景与实战指南:从新特性到架构革新

作为一名Java开发者&#xff0c;2025年的技术浪潮将带给我们前所未有的机遇与挑战。本文将带你深入探索Java生态的最新发展&#xff0c;从语言特性到架构革新&#xff0c;助你在技术洪流中把握先机&#xff01; &#x1f31f; Java 2025 新特性全景 1. 模式匹配的全面进化 (J…...

查看MAC 地址以及简单了解

MAC地址 简介 MAC 地址&#xff08;Media Access Control Address&#xff09;&#xff0c;直译为媒体访问控制地址&#xff0c;又称局域网地址&#xff08;LAN Address&#xff09;、MAC 地址、以太网地址&#xff08;Ethernet Address&#xff09;、硬件地址&#xff08;Ha…...

c语言 write函数

write函数 #include <unistd.h>ssize_t write(int fd, const void *buf, size_t count); 参数说明 fd:这是文件描述符,用于指定要写入数据的目标对象。文件描述符是一个非负整数,它代表了一个打开的文件、设备、管道等。常见的文件描述符有: 0:标准输入(stdin)。…...

Halcon 的基础用法

基础语法 1. 下载链接2. 赋值3. 判断符4. 循环5. 加载图片6. 读取文件夹下所有图片 1. 下载链接 链接:https://pan.baidu.com/s/1ZhQ_tTcubUtUggbb-OxUGw?pwdw3rs 提取码:w3rs 2. 赋值 x : 1 s : hello list2 : [a, b, c]3. 判断符 * 等于比较符 if(x 1)h : 6 endif* 不等…...

《100天精通Python——基础篇 2025 第2天:Python解释器安装与基础语法入门》

目录 一、Windows安装Python1.1 下载并安装 Python1.2 测试安装是否成功 二、Linux系统安装Python(新手可以跳过)2.1 基于RockyLinux系统安装Python(编译安装)2.2 基于Ubuntu系统安装Python(编译安装)2.3 macOS 安装python解释器 三、如何运行Python程序&#xff1f;3.1 Python…...

MyBatis 和 MyBatis-Plus 在 Spring Boot 中的配置、功能对比及 SQL 日志输出的详细说明,重点对比日志输出的配置差异

以下是 MyBatis 和 MyBatis-Plus 在 Spring Boot 中的配置、功能对比及 SQL 日志输出的详细说明&#xff0c;重点对比日志输出的配置差异&#xff1a; 1. MyBatis 和 MyBatis-Plus 核心对比 特性MyBatisMyBatis-Plus定位基础持久层框架MyBatis 的增强版&#xff0c;提供代码生…...

【大模型有哪些训练阶段?】

大模型&#xff08;如 GPT、BERT 等&#xff09;训练一般可以分为以下 三个主要阶段&#xff0c;每个阶段都承担着不同的职责&#xff0c;共同推动模型从“语言新手”成长为“多任务专家”。 &#x1f9e0; 一、预训练阶段&#xff08;Pre-training&#xff09; &#x1f4cc;…...

动手试一试 Spring Boot默认缓存管理

1.准备数据 使用之前创建的springbootdata的数据库&#xff0c;该数据库有两个表t_article和t_comment&#xff0c;这两个表预先插入几条测试数据。 2.编写数据库表对应的实体类 Entity(name "t_comment") public class Comment {IdGeneratedValue(strategy Gener…...

A2A Agent 框架结构化分析报告

A2A Agent 框架结构化分析报告 第一章 绪论 1.1 引言 在全球数字化转型的浪潮中&#xff0c;人工智能&#xff08;Artificial Intelligence, AI&#xff09;技术正以前所未有的速度改变着我们的生活和工作方式。然而&#xff0c;随着AI系统的广泛应用&#xff0c;单一AI系统…...

Opencv图像处理:旋转、打包、多图像匹配

文章目录 一、图像的旋转1、使用numpy方法实现旋转1&#xff09;顺时针旋转90度2&#xff09;逆时针旋转90度 2、使用opencv的方法实现图像旋转1&#xff09;顺时针旋转90度2&#xff09;逆时针旋转90度3&#xff09;旋转180度 3、效果 二、多图像匹配1、模板2、匹配对象3、代码…...

BOM与DOM(解疑document window关系)

BOM&#xff08;浏览器对象模型&#xff09; 定义与作用 BOM&#xff08;Browser Object Model&#xff09;提供与浏览器窗口交互的接口&#xff0c;用于控制导航、窗口尺寸、历史记录等浏览器行为 window&#xff1a;浏览器窗口的顶层对象&#xff0c;包含全局属性和方法&am…...

数据仓库建设全解析!

目录 一、数据仓库建设的重要性 1. 整合企业数据资源 2. 支持企业决策制定 3. 提升企业竞争力 二、数据仓库建设的前期准备 1. 明确业务需求 2. 评估数据源 3. 制定项目计划 三、数据仓库建设的具体流程 1.需求分析​ 2.架构设计​ 3.数据建模​ 4.ETL 开发​ 5.…...

时序约束 记录

一、基础知识 1、fpga的约束文件为.fdc&#xff0c;synopsys的约束文件为.sdc。想通过fpga验证soc设计是否正确&#xff0c;可以通过syn工具(synplify)吃.fdc把soc code 转换成netlist。然后vivado P&R工具通过吃上述netlist、XDC 出pin脚约束、fdc时序约束三个约束来完成…...

Redis-cli常用参数及功能的详细说明

Redis-cli常用参数及功能的详细说明 相关参考知识书籍 <<Redis运维与开发>> 以下是Redis-cli常用参数及功能的详细说明 1. **-r​&#xff08;重复执行命令&#xff09;** 作用&#xff1a;重复执行指定命令多次。 示例&#xff1a;执行3次PING​命令&#xff1…...