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

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;
}

在这个示例中,我们创建了一个新线程,线程执行thread_function函数,在函数中打印传入的参数,然后休眠 2 秒模拟执行任务,最后返回一个值。主线程通过pthread_join等待子线程结束,并获取其返回值。

1.3线程的生命周期

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

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

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

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

二、Posix网络API

2.1客户端和服务端代码示例

(1)服务端server.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 != 2){printf("Using:./server port\nExample:./server 5005\n\n"); return -1;}// 第1步:创建服务端的socket。int listenfd;if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); return -1; }// 第2步:把服务端用于通信的地址和端口绑定到socket上。struct sockaddr_in servaddr;    // 服务端地址信息的数据结构。memset(&servaddr,0,sizeof(servaddr));servaddr.sin_family = AF_INET;  // 协议族,在socket编程中只能是AF_INET。servaddr.sin_addr.s_addr = htonl(INADDR_ANY);          // 任意ip地址。//servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); // 指定ip地址。servaddr.sin_port = htons(atoi(argv[1]));  // 指定通信端口。if (bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) != 0 ){ perror("bind"); close(listenfd); return -1; }// 第3步:把socket设置为监听模式。if (listen(listenfd,5) != 0 ) { perror("listen"); close(listenfd); return -1; }// 第4步:接受客户端的连接。int  clientfd;                  // 连上来的客户端socket。int  socklen = sizeof(struct sockaddr_in); // 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"); break; }printf("response client: %s success...\n", buffer);}// 第6步:关闭socket,释放资源。close(listenfd); close(clientfd); return 0;
}

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

着重分析以下几个函数

(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并返回。

(5) 三次握手过程分析

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

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

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

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

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

(6)send/recv 函数

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

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

过程分析

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

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

(7)close 函数

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

四次挥手流程:

  • 客户端调用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.2常见面试问题

为什么要三次握手?

答:因为一个完整的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 资源,因此可以适当增加线程数量 。此外,优化同步机制也能提升性能,比如使用更细粒度的锁,减少锁的竞争范围,或者在合适的场景下使用无锁数据结构,避免锁带来的开销 。通过不断地调试和优化,我们能够让多线程程序更加稳健高效地运行 。

相关文章:

  • 【iOS】Blocks学习
  • LangChain实现PDF中图表文本多模态数据向量化及RAG应用实战指南
  • 【身份证扫描件识别表格】如何识别大量身份证扫描件将内容导出保存到Excel表格,一次性处理多张身份证图片导出Excel表格,基于WPF和腾讯云的实现方案
  • Elasticsearch插件:IDEA中的Elasticsearch开发利器
  • LabVIEW 开发中数据滤波方式的选择
  • Ansys electronics安装多版本simulink打开s-function冲突解决方法
  • LlamaIndex 生成的本地索引文件和文件夹详解
  • [BJDCTF2020]EzPHP
  • 在麒麟KylinOS上通过命令行配置KYSEC的防火墙
  • android 多个viewmodel之间通信
  • Math.round(),Math.ceil(),Math.floor(),Math.sqrt(),Math.pow(),Math.abs()等!
  • Redis专题
  • 深度学习框架PyTorch——从入门到精通(3.3)YouTube系列——自动求导基础
  • 在Cursor编辑器上部署MCP(Minecraft Coder Pack)完整指南
  • PyTorch与TensorFlow模型全方位解析:保存、加载与结构可视化
  • 使用go-git同步文件到gitee
  • 【OSG学习笔记】Day 5: 坐标系与变换节点(Transform)
  • Spark-SQL(四)
  • C++之unordered封装
  • MyBatis框架—xml映射
  • 央行等:在上海试点通过再贴现窗口支持人民币跨境贸易融资
  • 上海交大发布“AI十条”,鄂维南院士已任该校人工智能学院讲席教授
  • 云南省交通发展投资有限责任公司原党委书记、董事长陈以东接受审查调查
  • 卡洛·金茨堡:女巫与萨满——我的学术之路
  • 2025年上海版权宣传周在杨浦启动
  • 世贸组织:美关税政策致全球贸易前景严重恶化