C语言系列12——多线程与并发编程

目录

  • 写在开头
  • 1 多线程的基本概念与应用场景
    • 1.1 什么是多线程?
    • 1.2 多线程的优势
    • 1.3 应用场景详解
      • 1.3.1 并行计算
      • 1.3.2 高响应性应用程序
      • 1.3.3 网络编程
      • 1.3.4 实时处理
    • 1.4 多线程编程的挑战
  • 2 POSIX线程库的使用与实际案例
    • 2.1 基础概念
    • 2.2 创建和管理线程
    • 2.3 线程同步
    • 2.4 实际案例:简单的Web服务器
      • 2.4.1 设计思路
      • 2.4.2 示例代码
      • 2.4.3 说明
  • 3 线程同步与互斥锁的原理与应用
    • 3.1 线程同步的必要性
    • 3.2 互斥锁(Mutex)
    • 3.3 条件变量(Condition Variables)
    • 3.4 死锁(Deadlocks)
    • 3.5 实践建议
  • 写在最后

写在开头

在计算机编程领域,多线程与并发编程是一项重要的技术,它允许程序同时执行多个任务,提高了系统资源的利用率和程序执行效率。本文将介绍多线程的基本概念、应用场景,以及如何使用POSIX线程库进行编程,同时探讨线程同步与互斥锁的原理与应用,以避免竞态条件的发生。

1 多线程的基本概念与应用场景

在现代编程中,多线程技术是一种允许同时执行多个任务的方法,它通过使得每个核心可以独立处理任务,从而在多核心处理器上提高应用程序的性能和响应速度。了解多线程的基本概念及其在不同场景下的应用,对于开发高效、可靠的软件系统至关重要。

1.1 什么是多线程?

多线程是一种使得一个程序可以同时运行多个任务的技术。在操作系统中,线程被定义为进程中的一个执行路径。与进程不同,线程共享同一进程空间的内存和资源,这使得线程间的通信和资源共享更为高效。每个线程都有自己的执行序列、程序计数器、寄存器集和堆栈,但它们可以访问同一进程中的共享内存和资源。

1.2 多线程的优势

  • 提高性能:在多核处理器上,多线程可以显著提高应用程序的执行速度,因为它们允许多个任务并行执行。
  • 改善响应时间:通过将长时间运行的任务放在后台线程中执行,可以保持用户界面的流畅和响应性。
  • 资源共享:线程比进程更轻量级,它们共享相同的进程资源,减少了资源消耗和切换的开销。

1.3 应用场景详解

1.3.1 并行计算

在科学计算、图像处理、数据分析等领域,多线程可用于并行执行复杂的计算任务,通过分解任务并在多个线程中并行处理,可以大幅度减少处理时间。

1.3.2 高响应性应用程序

在开发图形用户界面(GUI)应用时,多线程用于分离用户界面处理和后台任务。这样,即使后台任务需要较长时间才能完成,应用程序的界面仍然可以快速响应用户的操作。

1.3.3 网络编程

服务器端软件,如Web服务器和数据库服务器,通常需要同时处理大量客户端请求。通过为每个请求分配一个独立的线程,多线程服务器可以提高并发处理能力,提供更好的服务质量。

1.3.4 实时处理

在需要快速响应外部事件的系统中,例如在游戏开发、实时交易系统中,多线程被用于同时处理输入、输出、计算和渲染任务,确保系统能够及时响应外部变化。

1.4 多线程编程的挑战

虽然多线程带来了许多优势,但它也引入了额外的复杂性和潜在的问题,如线程安全、死锁和竞态条件等。正确地管理线程间的交互和资源共享是多线程编程的关键挑战之一。开发者需要仔细设计同步机制,以确保程序的正确性和高性能。

通过深入理解多线程的基本概念和应用场景,开发者可以更好地利用这一强大的编程模型,设计和实现高效、可靠的多线程应用程序。

2 POSIX线程库的使用与实际案例

POSIX线程库(Pthreads)是一种在UNIX-like操作系统中实现线程的标准集合。它为多线程编程提供了一套丰富的接口,包括线程的创建、终止、同步(如互斥锁、条件变量)等功能。深入了解和掌握POSIX线程库对于开发跨平台的多线程应用程序至关重要。

2.1 基础概念

  • 线程创建pthread_create函数用于创建一个新的线程,它接受四个参数,包括指向线程标识符的指针、线程属性、启动例程的地址和传递给启动例程的参数。
  • 线程终止:线程可以通过pthread_exit函数退出,也可以被其他线程用pthread_cancel函数取消。
  • 线程同步:Pthreads提供了多种同步机制,包括互斥锁(pthread_mutex_t)、条件变量(pthread_cond_t)和屏障(pthread_barrier_t)等。

2.2 创建和管理线程

在使用POSIX线程库时,首先需要包含头文件<pthread.h>,并链接到POSIX线程库。

创建线程示例

#include <pthread.h>
#include <stdio.h>// 线程的运行函数
void* do_work(void* arg) {// 执行某些任务printf("Thread is working.\n");return NULL;
}int main() {pthread_t tid; // 线程的ID// 创建线程pthread_create(&tid, NULL, do_work, NULL);// 等待线程结束pthread_join(tid, NULL);return 0;
}

在上面的示例中,pthread_create函数用于创建一个新线程,do_work是这个新线程将要执行的函数。pthread_join函数则用于等待指定的线程结束。

2.3 线程同步

多线程程序中,正确的数据同步和线程间的协调是非常重要的,以避免竞态条件和数据不一致等问题。

互斥锁使用示例

#include <pthread.h>
#include <stdio.h>pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;void* do_work(void* arg) {pthread_mutex_lock(&lock);// 访问或修改共享资源printf("Thread is working with mutual exclusion.\n");pthread_mutex_unlock(&lock);return NULL;
}int main() {pthread_t tid1, tid2;pthread_create(&tid1, NULL, do_work, NULL);pthread_create(&tid2, NULL, do_work, NULL);pthread_join(tid1, NULL);pthread_join(tid2, NULL);pthread_mutex_destroy(&lock);return 0;
}

在此示例中,两个线程使用pthread_mutex_lockpthread_mutex_unlock来确保同时只有一个线程可以访问共享资源。这是避免竞态条件的一种常见方法。

2.4 实际案例:简单的Web服务器

在本实际案例中,我们将介绍如何使用POSIX线程库来实现一个简单的多线程Web服务器。这个服务器的基本功能是:监听来自客户端的HTTP请求,并为每个请求创建一个新的线程来处理,从而能够并发地服务多个客户端。

2.4.1 设计思路

  • 主线程:负责监听指定端口上的客户端连接请求。一旦接收到连接请求,主线程就会创建一个新的工作线程来处理该请求。
  • 工作线程:负责处理具体的客户端请求,如解析HTTP请求、读取请求的资源(例如HTML文件)并将其发送回客户端。

2.4.2 示例代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>#define PORT 8080void* handle_request(void* arg) {int client_socket = *((int*)arg);free(arg);// 处理HTTP请求(这里简化处理,直接返回一个响应)char *response = "HTTP/1.1 200 OK\nContent-Type: text/plain\n\nHello, World!";write(client_socket, response, strlen(response));// 关闭客户端连接close(client_socket);return NULL;
}int main() {int server_fd, new_socket;struct sockaddr_in address;int addrlen = sizeof(address);// 创建套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 绑定套接字到端口8080address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr *)&address, sizeof(address))<0) {perror("bind failed");exit(EXIT_FAILURE);}// 监听端口if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}while(1) {// 接受客户端请求if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen))<0) {perror("accept");continue;}// 为每个请求创建新线程pthread_t thread_id;int* new_sock = malloc(sizeof(int));*new_sock = new_socket;if(pthread_create(&thread_id, NULL, handle_request, (void*)new_sock) != 0) {perror("pthread_create failed");}// 不等待工作线程结束pthread_detach(thread_id);}return 0;
}

2.4.3 说明

  • 这个例子中的Web服务器极其简化,只能处理最基本的HTTP请求,并返回一个固定的响应。
  • 在实际应用中,服务器需要能够解析HTTP请求的具体内容,如请求方法(GET、POST等)、请求的资源路径,并根据请求返回相应的内容。
  • 使用pthread_create创建新线程来处理每个客户端请求,通过这种方式,服务器可以同时处理多个请求,提高了并发处理的能力。
  • 使用pthread_detach让工作线程在完成任务后能够自行清理资源,避免内存泄漏。

通过这个简单的例子,我们可以看到,利用POSIX线程库开发多线程应用程序可以有效地提升应用的并发处理能力,对于Web服务器这类需要高并发处理的应用尤其重要。

3 线程同步与互斥锁的原理与应用

在多线程编程中,线程同步是确保数据的一致性和防止资源冲突的关键技术。互斥锁(Mutex)作为线程同步的基本机制之一,广泛应用于多线程环境下保护共享资源不被多个线程同时访问,从而避免竞态条件的发生。

3.1 线程同步的必要性

当多个线程尝试同时访问和修改同一份资源时,如果没有适当的同步机制,就会发生竞态条件(Race Condition),导致数据不一致甚至系统崩溃。因此,线程同步对于维护多线程程序的稳定性和可靠性至关重要。

3.2 互斥锁(Mutex)

互斥锁是一种最基本的线程同步机制,用于保证同一时间只有一个线程可以访问特定的资源或代码段。

原理

  • 加锁(Lock):当线程需要访问共享资源时,它会先尝试加锁。如果互斥锁已经被另一个线程锁定,该线程会被阻塞,直到互斥锁被释放。
  • 解锁(Unlock):访问完共享资源后,持有互斥锁的线程应立即释放锁,以便其他线程可以访问资源。

互斥锁广泛应用于多线程程序中,用于保护全局变量、数据结构、文件等共享资源。

以下是一个使用互斥锁来保护共享资源的简单示例。在这个示例中,假设有一个共享的计数器,多个线程将尝试更新这个计数器:

#include <pthread.h>
#include <stdio.h>pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;void* update_counter(void* arg) {pthread_mutex_lock(&lock); // 加锁counter++; // 更新共享资源printf("Counter value: %d\n", counter);pthread_mutex_unlock(&lock); // 解锁return NULL;
}int main() {pthread_t tid1, tid2;pthread_create(&tid1, NULL, update_counter, NULL);pthread_create(&tid2, NULL, update_counter, NULL);pthread_join(tid1, NULL);pthread_join(tid2, NULL);pthread_mutex_destroy(&lock); // 销毁互斥锁return 0;
}

在这个示例中,两个线程都尝试更新全局变量counter。通过使用互斥锁lock,我们确保了每次只有一个线程可以修改counter,防止了竞态条件。

3.3 条件变量(Condition Variables)

条件变量是另一种重要的线程同步机制,它允许线程在某些条件尚未达成时挂起(等待),直到其他线程改变了条件并通知它继续执行。

原理

  • 等待(Wait):线程在条件变量上等待,直到条件满足。在等待过程中,线程会释放已持有的互斥锁,以允许其他线程修改条件。
  • 通知(Signal/Broadcast):当条件发生变化时,一个或多个等待的线程会被通知(唤醒)继续执行。

条件变量常用于实现生产者-消费者模型等需要线程间协调的场景。

以下示例展示了如何使用条件变量来实现生产者-消费者模型,其中生产者线程生成数据,而消费者线程消费这些数据:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;// 假设有一个缓冲区
int buffer = 0;
int data_available = 0;void* producer(void* arg) {for (int i = 0; i < 5; i++) {pthread_mutex_lock(&mutex);buffer = i; // 生产数据data_available = 1;printf("Produced: %d\n", buffer);pthread_cond_signal(&cond); // 通知消费者数据已经生产pthread_mutex_unlock(&mutex);}return NULL;
}void* consumer(void* arg) {while (1) {pthread_mutex_lock(&mutex);while (!data_available) // 等待数据可用pthread_cond_wait(&cond, &mutex);printf("Consumed: %d\n", buffer);data_available = 0;pthread_mutex_unlock(&mutex);}return NULL;
}int main() {pthread_t tid1, tid2;pthread_create(&tid1, NULL, producer, NULL);pthread_create(&tid2, NULL, consumer, NULL);pthread_join(tid1, NULL);pthread_join(tid2, NULL);pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);return 0;
}

3.4 死锁(Deadlocks)

在使用互斥锁时,如果不恰当地设计锁的获取和释放策略,可能会导致死锁,即多个线程相互等待对方释放锁,从而无法继续执行。

避免死锁的策略

  • 锁的顺序:确保所有线程以相同的顺序获取互斥锁。
  • 避免持有多个锁:尽可能设计不需要同时持有多个锁的代码。
  • 使用锁超时:使用尝试加锁(try-lock)或设置锁的超时时间,避免无限等待。

3.5 实践建议

  • 最小化锁的范围:只在必要时加锁,并尽快释放锁,以减少锁的竞争。
  • 避免在持有锁时执行长时间操作:如I/O操作、计算密集型任务等。
  • 考虑使用更高级的同步机制:如读写锁(Read-Write Locks)、原子操作等,以提高程序的性能和可读性。

写在最后

在设计多线程项目时,应从架构层面考虑并发,明确各线程的职责和交互方式。挑战性任务可能包括实现无锁数据结构、优化线程池性能、设计高并发服务器等。通过面对这些挑战,开发者可以深入理解并发编程的原理和实践,成为更加熟练的多线程程序设计师。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/689870.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTML 入门指南

简述 参考&#xff1a;HTML 教程- (HTML5 标准) HTML 语言的介绍、特点 HTML&#xff1a;超级文本标记语言&#xff08;HyperText Markup Language&#xff09; “超文本” 就是指页面内可以包含图片、链接等非文字内容。“标记” 就是使用标签的方法将需要的内容包括起来。…

116 C++ 可变参数函数,initializer_list (初始化列表), 省略号形参

一 可变参数函数 有时候我们传递的参数是不固定的。 这种能接受非固定个数参数的函数就是可变参数函数 怎么实现呢&#xff1f;就要用到 initializer_list 标准库类型 该类型能够使用的前提条件是&#xff1a;所有的实参类型相同。 二&#xff0c;initializer_list(初始化列…

电阻(二):希尔伯特(Hilbert)曲线

1、Hilbert简介 希尔伯特曲线是一种能在 2D平面完美填充正方形的曲线&#xff0c;连续且稳定&#xff08;当细分足够小时&#xff0c;线构成面&#xff09;而又不可导的曲线。只要恰当选择函数&#xff0c;画出一条连续的参数曲线&#xff0c;当参数 t 在 [0、1 ] 区间取值时&a…

ESP32-Cam学习(2)——PC实时显示摄像头画面

具体代码和操作过程见&#xff1a; 3. 实时显示摄像头画面 (itprojects.cn)https://doc.itprojects.cn/0006.zhishi.esp32/02.doc/index.html#/e03.showvideo我主要记录一下我在复现的过程中&#xff0c;遇到的问题以及解决方法。 1.安装第三方库 首先电脑端的代码需要用pych…

备战蓝桥杯---动态规划(入门3之子串问题)

本专题再介绍几种经典的字串问题。 这是一个两个不重叠字串和的问题&#xff0c;我们只要去枚举分界点c即可&#xff0c;我们不妨让c作为右区间的左边界&#xff0c;然后求[1,c)上的单个字串和并用max数组维护。对于右边&#xff0c;我们只要反向求单个字串和然后选左边界为c的…

到底什么是哈希值,哈希值到底是怎么生成的,有什么用?

哈希值&#xff0c;即HASH值&#xff0c;通常用一个短的随机字母和数位组成的字串来代表&#xff0c;是一组任意长度的输入信息通过哈希算法得到的“数据指纹”&#xff0c;即进行加密运算得到的一组二进制值。因为电脑在底层机器码是采用二进位的模式&#xff0c;因此通过哈希…

java中x++和++x的区别,执行后x的值是多少

在Java和C等编程语言中&#xff0c;x 和 x 都是用来对变量 x 进行自增操作的表达式&#xff0c;它们的主要区别在于自增操作发生的时机以及返回值&#xff1a; 后置递增运算符 x&#xff1a; 先使用当前 x 的值进行表达式计算&#xff0c;然后将 x 的值加 1。 执行后的 x 值为…

django连接本地数据库并执行增删改查

1&#xff0c;首先需要将本地数据库的表同步到django的models.py文件 py manage.py inspectdb tb_books tb_heros > demo001/models.py 2&#xff0c;同步成功后models.py会根据每张表映射出不同的类 models.py文件根据数据库表映射出对应的类 3&#xff0c;然后根据不同…

初识 Rust 语言

目录 前言一、Rust 的背景二、Rust的特性三、部署开发环境&#xff0c;编写一个简单demo1、在ubuntu 20.04部署环境2、编写demo测试 四、如何看待Linux内核引入Rust 前言 自Linux 6.1起&#xff0c;初始的Rust基础设施被添加到Linux内核中。此后为了使内核驱动程序能够用Rust编…

应如何看待用AI写论文一事? AI写论文有助科研还是助长作弊?

自大语言模型问世后&#xff0c;许多高校学生都在悄悄利用ChatGPT等AI&#xff08;人工智能&#xff09;写作软件代写论文&#xff0c;或者用AI辅助论文写作&#xff0c;如罗列提纲、润色语言、降低重复率等。 国内类似ChatGPT的AI写作软件并不少见。在各大等网站上&#xff0…

SpringBoot 打成jar包后如何获取jar包Resouces下的文件

获取resouces下的文件使用以下代码即可读取&#xff0c;如果需要变成file传入其他的方法中&#xff0c;需要创建临时文件将输入流文件 复制到 临时文件中&#xff0c;并传入相关方法&#xff0c;最后删除临时文件即可。不能通过ClassPathResouce对象直接获取 文件File ClassPa…

管理员分级管控三大模式,提高企业内部管理效率

随着公司规模的不断扩大和部门的持续增加&#xff0c;权限管理问题日益凸显。每当新员工入职&#xff0c;都需要经过一系列繁琐的步骤来为其匹配相应的权限。然而&#xff0c;这种传统的、基于手动更新的管理方式不仅效率低下、安全风险大&#xff0c;给企业带来了巨大的数据安…

echats 时间直方图示例

需求背景 某订单有N个定时任务&#xff0c;每个任务的执行时间已经确定&#xff0c;希望直观的查看该订单的任务执行趋势 查询SQL&#xff1a; select UNIX_TIMESTAMP(DATE_FORMAT(exec_time,%Y-%m-%d %H:%i)) execTime, count(*) from order_detail_task where order_no 2…

LeetCode--2388. 将表中的空值更改为前一个值

文章目录 1 题目描述2 测试用例3 解题思路 1 题目描述 表: CoffeeShop ---------------------- | Column Name | Type | ---------------------- | id | int | | drink | varchar | ----------------------id 是该表的主键&#xff08;具有唯一值的列&…

Jmeter教程-JMeter 环境安装及配置

Jmeter教程 JMeter 环境安装及配置 在使用 JMeter 之前&#xff0c;需要配置相应的环境&#xff0c;包括安装 JDK 和获取 JMeter ZIP 包。 安装JDK 1.JDK下载 示例环境为Windows11环境&#xff0c;读者应根据实际环境下载JDK的安装包。 JDK下载地址&#xff1a; Java21 下载 …

【Linux】软件包管理器 yum | vim编辑器

前言: 软件包管理器 yum和vim编辑器讲解 文章目录 软件包管理器 yum编辑器-vim四种模式普通模式批量化注释和批量化去注释末行模式临时文件 软件包管理器 yum yum&#xff08;Yellowdog Updater, Modified&#xff09;是一个在基于 RPM&#xff08;管理软件包的格式和工具集合&…

如何将多张图片变成一张?一个工具在线分享

如何将多张图片变成一张gif动图&#xff1f;现在gif动图非常受大家的欢迎我们想要将自己手中的多张图片变成一张gif动图时应该怎么制作呢&#xff1f;通过使用在线图片合成&#xff08;https://www.gif.cn/&#xff09;工具&#xff0c;不需要下载软件&#xff0c;手机、pc均可…

(01)Hive的相关概念——架构、数据存储、读写文件机制

目录 一、架构及组件介绍 1.1 Hive整体架构 1.2 Hive组件 1.3 Hive数据模型&#xff08;Data Model&#xff09; 1.3.1 Databases 1.3.2 Tables 1.3.3 Partitions 1.3.4 Buckets 二、Hive读写文件机制 2.1 SerDe 作用 2.2 Hive读写文件流程 2.2.1 读取文件的过程 …

Pinia 官网速通

前言&#xff1a;参考 Pinia 中文文档&#xff0c;在 Vue3 配合 ts 中的使用。 一&#xff1a;介绍 1. 什么是 Pinia Pinia 是 Vue 的存储库&#xff0c;允许跨组件/页面共享状态。 1.1. 为什么要使用 Pinia&#xff1f; 热模块更换、保持任何现有状态、使用插件扩展 Pinia …

Opencv实战(1)读取与图像操作

Opencv 文章目录 Opencv一、读取图片1.imshow2.namedWindow3.imshow4.效果图 二、像素操作(1).访问像素1. at()2.Mat_ (2).遍历像素1.指针遍历2.迭代器遍历 (3).threshold(4).通道分离1.split2.merge (5)Gamma矫正 三、深浅拷贝 一、读取图片 1.imshow Mat imread(const stri…