【术语学习】零碎的知识学习记录-并发

写在前面 / 链接存档

并发

主要内容来自天工AI,辅助以DeepSeek的回答

  • 答前附录:DeepSeek的思考过程

用户输入:并发

DeepSeek回复的思考路径:嗯,用户提到“并发”,这可能涉及编程中的并发处理。首先,我得解释什么是并发,可能需要提到并发与并行的区别。然后,可以讨论一下并发编程的重要性,特别是在多核处理器普及的今天。接着,应该介绍一些常见的并发模型,比如多线程和多进程,以及它们的优缺点。还有,可能得讲讲同步机制,比如锁、信号量这些,以及为什么需要它们来避免竞争条件。另外,现代编程语言中的一些并发特性,比如Go的协程,Python的asyncio,也应该提一下。最后,可以谈谈并发编程中常见的问题,比如死锁、活锁,还有如何调试和测试并发程序。总之,要全面覆盖并发的基本概念和实际应用。

并发基础

并发的概念

  • 并发(Concurrency)是指在计算机系统中同时处理多个任务的能力。
  • 这些任务可能在同一时间间隔内交替执行,或者在多个处理器上真正同时执行
  • 并发是计算机科学中的一个重要概念,广泛应用于操作系统、编程语言、分布式系统等领域。

并发的特点

  • 时间上的重叠:多个任务或线程的执行时间有重叠部分。
  • 资源共享:并发执行的任务或线程需要共享处理器、内存等资源。
  • 调度和切换:操作系统需要负责任务的调度和线程的切换,以确保并发执行。

并发与并行的区别

  • 并发:指多个任务在同一个时间段内交替执行,可能是在同一个处理器上通过时间片轮转的方式实现。
  • 并行:指多个任务在同一时刻真正同时执行,通常需要多个处理器或核心。
特性并发(Concurrency)并行(Parallelism)
时间维度逻辑上的同时发生(simultaneous)物理上的同时发生
资源需求共享处理器资源拥有独立的处理器资源
执行效率可能受到资源竞争和调度开销的影响显著提高任务的执行效率
实现方式多线程、事件驱动编程或协作式多任务多核处理器、分布式计算、GPU计算
适用场景多任务处理,特别是任务需要较长时间完成时需要处理大量数据或进行密集型计算的任务

所以当时同时开好几个网页调用工作流生成问答对应该是:并发

从时间维度看:看似是几个网页同时工作(并行),但是看F12是浏览器接收和处理每一个资源包再把数据返回给网页(并发),这有一个顺序和排队处理的过程→并发

从资源需求看:几个网页用的是用一个CPU,开太多网页导致CPU内存不足,就全崩了→并发

从执行效率看:有受到资源竞争和调度开销的影响→并发

实现方式:单个CPU处理接收的信息,所以更像是→并发

适用场景:多任务(多个网页同时开着),任务需要时间长,数据量大,所以从这一点不好判断

并发模型

并发模型是设计和实现并发系统的基础框架,它定义了如何组织和协调多个并发任务的执行

1. 共享内存模型
  • 共享内存模型是最传统的并发模型之一。
  • 在这种模型中,多个并发执行的任务共享同一个内存空间,通过读写共享变量进行通信和协调。
  • 虽然这种方法直观易懂,但在处理复杂并发场景时可能会遇到死锁和数据竞争等问题。
2. 消息传递模型
  • 消息传递模型则采取了一种截然不同的方法。
  • 在这种模型中,任务之间不共享内存,而是通过发送和接收消息进行通信。
  • 这种方式提供了更好的隔离性和并发性,减少了共享状态可能带来的问题。
  • 消息传递模型特别适用于【分布式系统】【消息队列】等场景。
3. Actor 模型
  • Actor模型是一种高层次的抽象模型,近年来在并发领域备受关注。
  • 它将系统视为由独立的Actor组成,每个Actor都有自己的状态和行为。
  • Actor之间通过异步消息传递进行通信,这种机制提供了良好的隔离性和并发性
  • Actor模型的一个重要特点是能够【有效处理大规模并发】,使其成为构建高并发系统(如分布式系统和通信系统)的理想选择。
4. CSP 模型
  • CSP(Communicating Sequential Processes)模型是由Tony Hoare提出的另一种重要的并发模型。
  • CSP 描述了并发系统中独立组件之间的通信和协作
  • CSP 模型的一个关键特征是进程通过通道进行消息通信,通信同步的,即发送者和接收者需要达成一致才能继续执行。
  • 这种模型支持并发执行和非确定性选择,常用于【描述并发系统的行为】【设计并发算法】
5. 最新研究成果

在并发模型的研究方面,近年来取得了一些重要进展。

  • 例如,软件交易内存(Software Transactional Memory, STM)模型作为一种新兴的并发编程范式,正逐渐受到研究者的关注。
    • STM模型试图简化并发编程,通过提供原子性、隔离性和一致性的事务处理,减少了手动管理锁的复杂性。
    • 这种模型特别适用于【函数式编程语言】和【需要高并发性的系统设计】
  • 此外,事件循环和异步回调(EventLoop)模型I/O 密集型应用中表现突出。
    • 这种模型通过事件循环持续监听分发事件,使得系统能够高效处理大量并发连接和事件。
    • 在Web服务器和应用程序框架中,这种模型被广泛应用于【处理请求和响应】

并发实现机制

多线程

多线程-简介
  • 并发实现机制中的核心技术之一
  • 通过在单个进程中创建多个执行序列来提高程序的并发能力资源利用率
  • 不仅提高了系统的整体性能,还为开发者提供了更灵活的编程模型
多线程-原理和优势
  • 核心原理:基于操作系统的时间片轮转调度机制。

    • 通过将处理器时间划分为短小的时间片,操作系统可以在这些时间片之间快速切换不同的线程,创造出多个任务同时执行的假象。
    • 这种机制使得多线程程序能够更好地利用系统资源,特别是在处理I/O密集型或计算密集型任务时表现出色。
  • 主要优势:

    1. 资源共享便利 :同一进程内的线程共享相同的内存空间和资源,大大简化了数据交换和通信过程。
    2. 提高CPU利用率 :通过并发执行多个线程,可以更充分地利用CPU资源,尤其是在多核处理器系统中。
    3. 改善程序结构 :多线程允许将复杂的任务分解为多个独立的执行单元,提高了代码的模块化程度和可维护性。
    4. 提高响应速度 :对于需要频繁与用户交互的应用程序,多线程可以确保UI线程始终响应用户输入,同时后台线程处理耗时操作。
多线程-最新研究成果
  • 在多线程领域的最新研究中, 软件交易内存(Software Transactional Memory, STM) 模型引起了广泛关注。
  • STM旨在简化并发编程,通过提供原子性、隔离性和一致性的事务处理,减少了手动管理锁的复杂性。
  • 这种模型特别适合于函数式编程语言和需要高并发性的系统设计。

多线程-代码示例

1
2
3
4
5
6
7
8
9
10
11
12
class HelloThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}
}

public class Main {
public static void main(String[] args) {
HelloThread t = new HelloThread();
t.start();
}
}

HelloThread类继承自Thread类并重写了run()方法。

当调用t.start()时,新的线程开始执行run()方法中的代码。

多进程

多进程-简介

通过创建多个独立的进程来执行不同的任务,每个进程拥有自己的内存空间和资源

多进程-优点
  • 提高系统并发性:允许多个任务同时运行
  • 增强系统稳定性:进程间相对独立,故障影响范围有限
  • 简化程序设计:将复杂任务分解为多个独立部分
多进程-缺点
  • 资源开销较大:每个进程都需要单独的内存空间
  • 进程间通信复杂:需要使用 IPC 机制
多进程总结
  • 多进程在CPU密集型任务中表现优异,如科学计算、图像渲染等领域。
  • 在实际应用中,多进程常与其他并发机制(如多线程)结合使用,以平衡资源利用和并发性能。

协程

协程-简介
  • 协程是另一种实现并发的重要机制
    • 协程是一种比线程更轻量级的并发单元,其核心特性是 协作式调度
    • 与传统线程依赖操作系统的抢占式调度不同,协程通过显式地让出控制权(await)来实现并发。
协程-实现工具
  • Python的asyncio库是实现协程的强大工具,它允许开发者编写高效的异步代码。
    • 通过使用async def关键字定义协程函数和await关键字等待可等待对象的完成,开发者可以轻松实现复杂的并发逻辑。
    • 这种语法糖极大地简化了异步编程,使代码更加易于理解和维护。

以下是自分の一些实践:

  • 例子1:多问答对生成工作流-代码模块-多行内容分割整合存入数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 在这里,您可以通过 ‘args’  获取节点中的输入变量,并通过 'ret' 输出结果
# 'args' 和 'ret' 已经被正确地注入到环境中
# 下面是一个示例,首先获取节点的全部输入参数params,其次获取其中参数名为‘input’的值:
# params = args.params;
# input = params.input;
# 下面是一个示例,输出一个包含多种数据类型的 'ret' 对象:
# ret: Output = { "name": ‘小明’, "hobbies": [“看书”, “旅游”] };

async def main(args: Args) -> Output:
params = args.params

# 按照换行符分割输入内容,并过滤掉空行
input_lines = [line for line in params['input'].split('\n') if line.strip()] # 使用列表推导式

# 构建输出对象
ret: Output = {
"questions": input_lines, # 将过滤后的字符串列表作为输出
}
return ret
  • 例子2:多问答对生成工作流-代码模块-把输出处理成 json 格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async def main(args: Args) -> Output:
params = args.params

# 获取输入的三个数组
instructions = params.get('instruction', [])
inputs = params.get('input', [])
outputs = params.get('output', [])

# 确保三个数组长度一致,否则取最短数组的长度
length = min(len(instructions), len(inputs), len(outputs))

# 构建输出对象列表
ret: Output = []
for i in range(length):
item = {
"instruction": instructions[i].strip() if i < len(instructions) else "",
"input": inputs[i].strip() if i < len(inputs) else "",
"output": outputs[i].strip() if i < len(outputs) else ""
}
ret.append(item)

return ret
协程-实际应用
  • 在实际应用中,协程特别适合处理 I/O 密集型任务,如网络请求和文件操作。
  • 通过合理使用协程,可以显著提高系统的并发能力和资源利用率,同时保持代码的清晰性和可读性。

并发编程会遇到的挑战

资源竞争

资源竞争-常见类型
  1. 数据竞争:多个线程同时访问和修改共享数据
  2. 锁竞争:多个线程争夺同一锁资源
  3. 内存竞争:多个线程频繁访问同一内存区域
  4. I / O竞争:多个线程同时进行I / O 操作
资源竞争-产生原因
  • 根本原因:并发执行的本质。
    • 在多线程或多进程环境下,系统资源(如处理器时间、内存、磁盘等)通常是有限的。
    • 当多个任务同时尝试访问或修改同一资源时,就会产生竞争。
    • 这种竞争可能导致数据不一致、性能下降甚至死锁等问题。
资源竞争-最新研究成果
  • 软件交易内存(Software Transactional Memory, STM) 模型,一种新兴的并发编程范式。
  • STM模型试图简化并发编程,通过提供原子性、隔离性和一致性的事务处理,减少了手动管理锁的复杂性。
  • 特别适用于函数式编程语言和需要高并发性的系统设计。
资源竞争-代码示例
1
2
3
4
5
6
7
8
9
10
11
public class Counter {
private int count = 0;

public void increment() {
count ++;
}

public int getCount() {
return count;
}
}

如果有多个线程同时调用increment()方法,就可能发生资源竞争。

如果没有适当的同步机制,最终的计数值可能不正确。

资源竞争-解决方法
  1. 使用其他同步机制来保护共享资源
  2. 设计无锁算法来减少竞争
  3. 优化数据结构和算法以减少共享资源的使用频率
  4. 使用并发容器和集合类来自动处理竞争问题
资源竞争-总结
  • 通过深入理解资源竞争的本质和最新研究成果
  • 可以更好地设计和实现高性能的并发系统
  • 最大限度地发挥多核处理器的优势
  • 同时避免常见的并发陷阱

死锁问题

引起死锁的四个必要条件
  1. 互斥条件 :至少有一个资源必须处于非共享模式,即一次只能被一个线程使用。
  2. 请求与保持条件 :一个已经保持至少一个资源的线程能够申请新的资源,而该资源可能被另一个线程所持有。
  3. 不可抢占条件 :资源不能被抢占,只能由持有线程主动释放。
  4. 循环等待条件 :存在一个线程等待链,链中的每一个线程都在等待下一个线程所占有的资源
预防和应对死锁的解决策略
  1. 资源有序分配 :(广受欢迎)要求所有线程按照固定的顺序申请资源
    1. 可以有效打破循环等待条件,从而避免死锁的发生。
    2. 例如,可以为所有资源分配一个唯一的ID,并要求线程按照ID从小到大的顺序获取资源。
  2. 超时机制 :为资源获取设置超时时间。
    1. 如果线程在指定时间内无法获取所需资源,它将释放已持有的资源并稍后重试。
    2. 这种方法虽然可以有效避免长期死锁,但也可能导致资源利用率降低
  3. 死锁检测与恢复 :一些先进的并发系统采用了死锁检测算法,定期检查系统是否存在死锁状态。
    1. 一旦检测到死锁,系统可以采取相应的措施,
    2. 如终止某个线程或剥夺某些资源,以打破死锁状态。
应对死锁的实际应用-STM
  • 在实际应用中, 软件交易内存(Software Transactional Memory, STM) 模型为解决死锁问题提供了新的思路。
  • STM通过提供原子性、隔离性和一致性的事务处理,减少了手动管理锁的复杂性,从而降低了死锁发生的概率。
  • 这种模型特别适用于函数式编程语言和需要高并发性的系统设计。
死锁问题-总结
  • 死锁是并发编程中的一大挑战
  • 但通过深入理解其产生的原因和预防策略→可以有效避免这一问题,并确保系统稳定性和效率
    • 在设计并发系统时,需要综合考虑各种因素,最大化系统的性能和可靠性

线程安全

线程安全-最新技术手段-STM
  • 在最新的线程安全技术中, 软件交易内存(Software Transactional Memory, STM) 模型脱颖而出。
    • STM通过提供原子性、隔离性和一致性的事务处理,简化了并发编程的复杂性。这种方法特别适用于函数式编程语言和需要高并发性的系统设计。
  • STM的核心思想是将一组操作封装为一个事务,确保整个事务要么全部成功,要么全部失败。

其实思想有点像数据库的事务,不过 STM 是对于程序执行的

  • 这种方法可以有效减少手动管理锁的复杂性,从而降低死锁和竞态条件的风险。STM的一个典型应用场景是在数据库事务处理中,它可以确保数据的一致性和完整性。
线程安全-相关研究成果
  • 在STM领域的研究中, 乐观并发控制(Optimistic Concurrency Control, OCC) 技术值得关注。
  • OCC假设大多数事务都能顺利提交,只有在检测到冲突时才会回滚。
  • 这种方法相比传统的悲观锁机制,可以显著提高并发性能,尤其适用于读多写少的场景。
线程安全-涉及的工具 / 库-Java
  • 在Java生态系统中, java.util.concurrent.atomic包 提供了一系列原子类,如AtomicInteger和AtomicReference,用于执行常见的原子操作
  • 这些类利用了硬件级别的原子操作,如compare-and-swap (CAS),在保证线程安全的同时,避免了锁的开销。
线程安全-代码示例

以下是一个使用 AtomicInteger 的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
count.incrementAndGet();
}

public int getCount() {
return count.get();
}
}

AtomicInteger的incrementAndGet()方法保证了递增操作的原子性,无需使用显式的锁

并发控制

互斥锁

互斥锁-简介
  • 互斥锁是并发控制中最基本且广泛应用的机制之一。

  • 通过确保同一时间只有一个线程可以访问特定的共享资源,有效解决了资源竞争问题。

互斥锁-工作原理

互斥锁的工作原理基于加锁和解锁两个关键操作:

  1. 加锁:线程尝试获取互斥锁。

    1. 如果锁当前未被其他线程持有,该线程将成功获得锁并进入临界区(即访问共享资源的代码部分)
  2. 解锁:线程完成对共享资源的访问后,必须释放互斥锁,以便其他等待中的线程可以继续访问共享资源

互斥锁-实现示例代码-C++
  • 互斥锁的实现可以基于操作系统提供的原语,如互斥量(mutex)

  • 在C++中,标准库提供了std::mutex类,简化了互斥锁的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <thread>
#include <mutex>

using namespace std; // 导入std命名空间,以简化符号使用

// 定义一个全局互斥锁,用于保护对共享资源counter的访问
mutex mtx;

// 定义一个整型变量作为计数器,初始值为0,它将被两个线程同时递增100,000次
int counter = 0;

// increment函数定义了每个线程要执行的任务:循环100,000次,每次递增counter一次
void increment() {
for (int i = 0; i < 100000; ++i) {
// 在每次递增之前,线程尝试获取互斥锁mtx,确保同一时间只有一个线程可以修改counter
mtx.lock();
++counter;
// 操作完成后释放锁,让其他等待的线程有机会获取锁并执行自己的增量操作
mtx.unlock();
}
}

int main() {
// 创建两个线程th1和th2,它们都运行increment函数
thread th1(increment);
thread th2(increment);

// join调用保证主线程会等待直到th1和th2完成它们的工作
// 这很重要,因为它确保了counter的最终值是在所有线程完成之后才打印出来的
th1.join();
th2.join();

// 输出counter的最终值200000,并结束程序
cout << "Final counter values: " << counter << endl;

return 0;
}

mutex对象mtx用于保护对counter变量的访问。通过在每次修改counter前后调用lock()unlock()方法,确保了同一时间只有一个线程可以修改counter

互斥锁-避免死锁的策略

互斥锁的不当使用可能导致死锁问题

死锁发生在多个线程互相等待对方释放锁的情况下

以下是几种避免在使用互斥锁时产生死锁的策略:

  1. 设计合理的锁获取顺序
  2. 使用超时机制
  3. 实现死锁检测和恢复算法
互斥锁-最新研究情况-STM
  • 软件交易内存(Software Transactional Memory, STM) 模型作为一种新兴的并发编程范式,正逐渐受到研究者的关注。
  • STM模型试图简化并发编程,通过提供原子性、隔离性和一致性的事务处理,减少了手动管理锁的复杂性。这种模型特别适用于函数式编程语言和需要高并发性的系统设计

信号量

信号量-简介
  • 信号量是并发控制中的一个重要机制,用于协调多个进程或线程对共享资源的访问
  • 它本质上是一个非负整数变量,用于记录可用资源的数量。
信号量-两种基本操作类型
  • 信号量有两种基本操作:P操作(等待操作)和V操作(信号操作)。

  • P操作会减小信号量的值,如果值变为负数,则进程被阻塞。

  • V操作会增加信号量的值,并可能唤醒一个被阻塞的进程。

信号量-两种信号量分类
  1. 整型信号量:
    1. 简单实现,但可能出现“忙等”现象
  2. 记录型信号量:
    1. 额外维护一个等待队列,避免“忙等”
信号量-代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable> // 添加此头文件以使用条件变量

using namespace std;

// 定义一个全局条件变量,用于线程间的通信和同步
condition_variable cv;
// 定义一个全局互斥锁,用于保护对共享资源counter的访问
mutex mtx;
// 定义一个整型变量作为计数器,初始值为0,表示可用资源的数量
int counter = 0;


// 生产者函数:增加可用资源数量,并通知等待的消费者
void producer() {
unique_lock<mutex> lock(mtx); // 获取锁,确保在修改counter时不会被其他线程干扰
counter++; // 增加资源数量
cout << "Produced, counter is now: " << counter << endl;
cv.notify_one(); // 唤醒一个等待的消费者线程
}


// 消费者函数:减少可用资源数量,但在没有可用资源时会等待
void consumer() {
unique_lock<mutex> lock(mtx); // 获取锁,确保在修改counter时不会被其他线程干扰
// 等待直到有可用资源 (即counter > 0),条件变量允许线程在条件满足时继续执行
cv.wait(lock, []{ return counter > 0; });
counter--; // 减少资源数量
cout << "Consumed, counter is now: " << counter << endl;
}

int main() {
// 创建一个生产者线程和一个消费者线程
thread th_producer(producer);
thread th_consumer(consumer);

// 等待两个线程完成它们的工作
th_producer.join();
th_consumer.join();

// 输出最终的counter值
cout << "Final counter value: " << counter << endl;

return 0;
}

展示了如何使用条件变量实现简单的信号量机制,协调生产和消费操作

条件变量

条件变量-简介
  • 条件变量是并发控制中的重要机制,用于协调线程间的同步
  • 它与互斥锁配合使用,允许线程在特定条件下等待或唤醒。
  • 在C++中,std::condition_variable类提供了标准化的实现。
  • 典型应用场景包括生产者-消费者模型线程间通信
条件变量-使用步骤
  1. 创建条件变量对象
  2. 使用互斥锁保护共享对象
  3. 等待条件满足:cv.wait(lock, predicate)
  4. 通知等待线程:cv.notify_one()cv.notify_all()

以上方法有效解决了线程间的同步问题,提高了并发程序的效率和可靠性

并发设计模式

生产者消费者

生产者消费者模型-简介
  • 生产者消费者模型是并发设计模式中的一个经典范式,广泛应用于解决多线程或多进程之间的协同工作问题。
  • 这种模型巧妙地分离数据生产和数据消费的过程,通过引入共享缓冲区实现了生产者和消费者之间的解耦。

生产者消费者模型-三个关键角色

  1. 生产者:负责生成数据或资源,并将其放入共享缓冲区
  2. 消费者:从共享缓冲区中获取数据或资源,并进行处理
  3. 共享缓冲区:作为生产者和消费者之间的中介,用于暂时存储数据
生产者消费者模型-工作流程步骤
  1. 生产者生成数据并将其放入缓冲区
  2. 消费者从缓冲区中取出数据并进行处理
  3. 当缓冲区满时,生产者等待直至有空闲空间
  4. 当缓冲区空时,消费者等待直至有新数据到达
生产者消费者模型-在实际运用中面临着的几个关键挑战
  1. 缓冲区溢出:当生产速度快于消费速度时,缓冲区可能变得过满
  2. 缓冲区空:当消费速度快于生产速度时,缓冲区可能变为空

为了解决以上问题,生产者和消费者通常需要使用同步机制,如锁、信号量或条件变量,以确保双方在适当的时间进行操作

生产者消费者模型-最新研究成果
  • 软件交易内存(Software Transactional Memory, STM) 模型作为一种新兴的并发编程范式,正逐渐受到研究者的关注。
  • STM模型试图简化并发编程,通过提供原子性、隔离性和一致性的事务处理,减少了手动管理锁的复杂性。
  • 这种模型特别适用于生产者消费者模型的实现,可以有效减少死锁和竞态条件的风险
生产者消费者模型-实际应用

该模型广泛应用于各种并发场景,例如:

  • I / O密集型应用:在网络服务器中,生产者负责接收客户端请求,消费者负责处理请求并生成响应
  • 数据处理管道:在大数据处理系统中,生产者负责读取原始数据,消费者负责清洗、转换和分析数据
  • 图形渲染:在图形渲染引擎中,生产者负责生成图像帧,消费者负责将帧输出到显示器

通过合理运用生产者消费者模型,开发者可以设计出高效、可靠且可扩展的并发系统,有效提升系统的整体性能和资源利用率

自分即将要做的数据库课设疑似无意中使用到了这个模型!!感觉可以润色一下写进数据库设计报告呢嘿嘿,就是并发部分需要在 Django 框架里面用代码好好打磨体现一下

读写锁

读写锁-简介
  • 读写锁是一种专门设计用于优化并发访问共享资源锁机制
  • 它允许多个线程同时进行读操作,但写操作是独占的。
  • 这种机制特别适用于【读多写少】的场景,可以显著提高系统的并发性能。
读写锁-代码示例

在Java中,java.util.concurrent.locks.ReentrantReadWriteLock类提供了读写锁的实现。使用时,可通过分别调用readLock()writeLock()方法获取读锁和写锁。

1
2
3
4
5
6
7
8
9
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

lock.readLock().lock();
// 这里是执行读操作的代码
lock.readLock().unlock();

lock.writeLock().lock();
// 这里是执行写操作的代码
lock.writeLock().unlock();
读写锁-作用与应用总结
  • 有效减少了不必要的锁竞争,提高了系统的整体吞吐量。
  • 在实际应用中,读写锁广泛用于缓存系统、**数据库查询**等场景,特别适合处理大量并发读请求的情况

线程池

线程池-简介
  • 线程池是并发设计模式中的关键组件,用于管理和复用线程资源
  • 它通过预创建一组线程来处理任务,避免了频繁创建和销毁线程的开销,从而提高了系统的性能和资源利用率。
  • 在Java中,java.util.concurrent.ThreadPoolExecutor类提供了强大的线程池实现。
线程池-核心参数
  • 核心线程数:决定线程池的基本规模
  • 最大线程数:限制并发执行的上限
  • 空闲线程存活时间:控制非核心线程的生命周期
  • 工作队列:用于暂存待执行的任务

通过合理配置这些参数,可以有效控制系统的并发水平和资源消耗,同时提高系统的响应能力和吞吐量。

线程池的使用不仅简化了并发编程的复杂性,还能显著提升系统的整体性能和稳定性。

并发性能优化

减少上下文切换

减少上下文切换-简介
  • 在并发编程中,上下文切换是影响系统性能的关键因素。
减少上下文切换-无锁编程
  • 无锁编程是一种减少上下文切换的有效方法。
  • 通过使用原子操作非阻塞数据结构,可以避免使用传统的锁机制,从而减少线程阻塞和上下文切换的发生。
  • Java的java.util.concurrent.atomic包提供了实现无锁编程所需的原子类,如AtomicIntegerAtomicReference
减少上下文切换-代码示例

以下是一个使用AtomicInteger实现无锁计数器的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.concurrent.atomic.AtomicInteger;

public class LockFreeCounter {
// 定义一个原子整型变量count,初始值为0。
// AtomicInteger提供了原子操作,可以保证在多线程环境下对这个变量的操作是线程安全的。
private AtomicInteger count = new AtomicInteger(0);

// increment方法用于增加计数器的值。
// 使用incrementAndGet()方法,它会原子地将当前值加1,并返回新的值。
public void increment() {
count.incrementAndGet();
}

// getCount方法用于获取当前计数器的值。
// 使用get()方法,它返回当前的值,而不需要任何额外的同步或锁定。
public int getCount() {
return count.get();
}
}

incrementAndGet()方法保证了递增操作的原子性,无需使用显式的锁。

这种方法不仅可以减少上下文切换,还可以提高系统的并发性能

减少上下文切换-软件交易内存(STM)
STM-简介
  • 软件交易内存(Software Transactional Memory, STM)是另一种值得关注的优化技术。
  • STM通过提供原子性、隔离性和一致性的事务处理,简化了并发编程的复杂性。
  • 这种方法特别适用于需要高并发性的系统设计。
  • STM的一个典型应用场景是在数据库事务处理中,它可以确保数据的一致性和完整性
STM-示例
1
2
3
4
5
6
Transaction.run(() -> {
Account aliceAccount = accounts.get("Alice");
Account bobAccount = accounts.get("Bob");

aliceAccount.transfer(bobAccount, 100);
})

在上例中,转账操作被封装在一个事务中,STM确保了整个操作的原子性和一致性,无需手动管理锁

减少上下文切换-协程
协程-简介
  • 协程是另一种减少上下文切换的有效方法。
  • 通过在单线程内实现多任务调度,协程可以在单线程中维持多个任务的切换,避免了内核层面的上下文切换开销。
  • Python的asyncio库是实现协程的强大工具,它允许开发者编写高效的异步代码。
协程-示例

以下是一个使用Python asyncio实现的简单协程示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import asyncio

# 定义一个异步函数hello,它将打印"Hello",然后暂停1秒钟,最后打印"World"
async def hello():
print("Hello")
await asyncio.sleep(1) # 模拟耗时操作(如网络请求、I/O等),不会阻塞事件循环
print("World")

# 定义另一个异步函数main,它负责创建和管理其他异步任务
async def main():
# 创建两个hello()异步任务,并立即开始执行
task1 = asyncio.create_task(hello())
task2 = asyncio.create_task(hello())

# 等待两个任务完成
await task1
await task2

# 运行main()异步函数,启动事件循环并等待main()完成
asyncio.run(main())

以上代码的运行结果:

Hello

Hello

(大约1秒后)

World

World

在这个例子中,hello()函数是一个协程,它会在打印"Hello"后暂停1秒,然后打印"World"。通过使用asyncio库,我们可以同时运行多个协程,而无需创建多个线程

避免伪共享

  • 在并发编程中,伪共享是一个常见的性能瓶颈,尤其在多核处理器系统中更为突出。
    • 为了避免伪共享,一种有效的方法是在数据结构之间插入填充字段
    • Go语言的x/sys/cpu包提供了一个名为CacheLinePad的类型,可以帮助开发者在数据结构之间引入足够的间隔,确保它们不会共享同一个缓存行。
  • 通过使用CacheLinePad,开发者可以在关键的数据结构之间引入间隔,减少伪共享的影响,从而提高程序的并发性能。
    • 这种方法特别适用于需要频繁访问的小型数据结构,如计数器或标志位。
    • 然而,使用CacheLinePad可能会略微增加内存使用量,因此应在性能优化和资源利用之间权衡。

无锁编程

无锁编程-简介
  • 无锁编程是一种先进的并发编程范式,旨在通过避免使用传统的互斥锁来提高多线程程序的性能和可靠性。
  • 核心思想:依赖于原子操作其他并发原语来实现线程安全,从而减少线程之间的依赖和竞争。
    • 这种方法不仅能提高并发性能,还能避免死锁和优先级反转等问题。
无锁编程-实现- CAS
  • 在实现方面,无锁编程通常采用特殊的算法来组织数据和控制数据流动,以达到在没有锁的情况下保持数据一致性的目的。
  • 一个典型的实现方式是使用 比较并交换(CAS) 操作,这是一种由硬件支持的原子操作,用于在多线程环境中实现安全的变量更新
无锁编程-CAS-基本原理
  1. 比较目标变量的当前值是否等于预期值
  2. 如果相等,则将变量更新为新值
  3. 否则,不做任何操作并返回当前值

这种方法可以有效减少线程阻塞和上下文切换的开销,从而提高系统的并发性能

无锁编程-实际应用
  • 在实际应用中,无锁编程广泛应用于高性能数据库系统实时多任务处理系统等领域,显著提升了性能和响应速度。
  • 然而,无锁编程的实现通常比基于锁的算法更为复杂,需要开发者对底层机制有深入的理解。
    • 为此,一些现代编程语言和库提供了原子操作的支持,如Java的java.util.concurrent.atomic包和C++的std::atomic类,这些工具大大简化了无锁编程的实现难度。
  • Copyrights © 2024-2025 brocademaple
  • 访问人数: | 浏览次数:

      请我喝杯咖啡吧~

      支付宝
      微信