1.背景介绍
写给开发者的软件架构实战:处理并发和多线程的策略
作者:禅与计算机程序设计艺术
背景介绍
并发与并行
在计算机科学中,并发(concurrency)和并行(parallelism)是两个常见的 buzzwords。它们在表面上似乎很相似,但实际上有着本质的区别。
并发是指多个任务在同一个时间段内执行,而这些任务会交替执行,即每个时刻只能有一个任务执行。并发通常是利用CPU时间片调度实现的。
并行则是指多个任务在同一个时刻同时执行。并行通常需要多个处理器核心来完成。
并发与分布式系统
并发和分布式系统密切相关。在分布式系统中,由于网络延迟、故障等原因,并发问题变得尤其重要。因此,分布式系统通常需要更强大的并发处理能力。
并发问题
并发问题主要包括:
- 互斥:当多个线程访问共享资源时,如果没有控制措施,可能导致数据不一致。
- 死锁:当两个或两个以上的线程相互等待对方释放资源时,就会导致死锁。
- 活锁:当两个或两个以上的线程在争夺资源时,总是被打断或失败,从而导致无限重试。
- 竞态条件:当多个线程同时执行相同的代码段时,如果没有适当的同步机制,可能导致数据不一致。
核心概念与联系
线程
线程是操作系统中轻量级的进程,可以理解为进程内的一个执行单元。线程共享进程的内存空间,因此它比进程更轻量级。
进程
进程是操作系统中的一种基本单位,它拥有自己的内存空间、文件描述符等资源。进程间是相互隔离的。
协程
协程(coroutine)是一种更高级别的线程控制机制。与线程相比,协程更轻量级,因为它不需要操作系统的支持。协程可以在单个线程中实现并发。
信号量
信号量(semaphore)是一种常用的并发控制机制。信号量管理一组公共资源,可以用来避免互斥和死锁等问题。
锁
锁是一种常用的并发控制机制。锁可以用来保护共享资源,避免互斥和死锁等问题。常见的锁有互斥锁、读写锁等。
事件循环
事件循环(event loop)是一种常用的并发控制机制。事件循环可以用来实现非阻塞 I/O 和异步编程。
核心算法原理和具体操作步骤以及数学模型公式详细讲解
生产者消费者模式
生产者消费者模式是一种经典的并发模型。它包括两种角色:生产者和消费者。生产者负责生产数据,消费者负责消费数据。生产者和消费者之间通过一个缓冲区进行数据传递。
生产者消费者模式的实现可以使用信号量、锁等并发控制机制。
信号量实现
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> queue;
const int MAX_QUEUE_SIZE = 10;
// 生产者
void producer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
// 如果队列已满,则等待
cv.wait(lock, [] { return queue.size() < MAX_QUEUE_SIZE; });
queue.push(i);
printf("producer: %d\n", i);
lock.unlock();
cv.notify_one(); // 通知消费者
}
}
// 消费者
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 如果队列为空,则等待
cv.wait(lock, [] { return !queue.empty(); });
int data = queue.front();
queue.pop();
printf("consumer: %d\n", data);
lock.unlock();
cv.notify_one(); // 通知生产者
if (data == 9) break;
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
锁实现
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> queue;
const int MAX_QUEUE_SIZE = 10;
bool done = false;
// 生产者
void producer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
while (queue.size() >= MAX_QUEUE_SIZE) {
cv.wait(lock);
}
queue.push(i);
printf("producer: %d\n", i);
lock.unlock();
cv.notify_one(); // 通知消费者
}
done = true;
}
// 消费者
void consumer() {
while (!done || !queue.empty()) {
std::unique_lock<std::mutex> lock(mtx);
while (queue.empty()) {
cv.wait(lock);
}
int data = queue.front();
queue.pop();
printf("consumer: %d\n", data);
lock.unlock();
cv.notify_one(); // 通知生产者
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
读写锁
读写锁是一种常用的并发控制机制。读写锁支持多个线程同时读取共享资源,但只允许一个线程写入共享资源。这样可以提高系统吞吐量。
实现
#include <shared_mutex>
#include <iostream>
class MyClass {
public:
void read() {
std::shared_lock<std::shared_mutex> lock(m);
do_read();
}
void write() {
std::unique_lock<std::shared_mutex> lock(m);
do_write();
}
private:
void do_read() {
std::cout << "reading..." << std::endl;
}
void do_write() {
std::cout << "writing..." << std::endl;
}
std::shared_mutex m;
};
int main() {
MyClass obj;
std::thread t1([&] { obj.read(); });
std::thread t2([&] { obj.read(); });
std::thread t3([&] { obj.write(); });
t1.join();
t2.join();
t3.join();
return 0;
}
非阻塞 I/O
非阻塞 I/O 是一种常用的并发控制机制。非阻塞 I/O 可以减少线程上下文切换的开销,提高系统吞吐量。
实现
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
int create_nonblock_socket() {
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
return sockfd;
}
void handle_client(int sockfd) {
char buffer[1024];
ssize_t n = recv(sockfd, buffer, sizeof(buffer), MSG_DONTWAIT);
if (n > 0) {
// handle data
std::cout << "received: " << buffer << std::endl;
} else if (n == 0) {
// client closed connection
close(sockfd);
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
// error occurred
close(sockfd);
}
}
int main() {
int listen_sockfd = create_nonblock_socket();
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listen_sockfd, (struct sockaddr*) &server_addr, sizeof(server_addr));
listen(listen_sockfd, 10);
while (true) {
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(listen_sockfd, &read_fds);
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
select(listen_sockfd + 1, &read_fds, NULL, NULL, &timeout);
if (FD_ISSET(listen_sockfd, &read_fds)) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int sockfd = accept(listen_sockfd, (struct sockaddr*) &client_addr, &addr_len);
handle_client(sockfd);
}
}
return 0;
}
具体最佳实践:代码实例和详细解释说明
使用信号量实现生产者消费者模式
背景
在某个系统中,有两类任务:生产者任务和消费者任务。生产者任务负责生成数据,而消费者任务负责处理数据。由于生产者和消费者之间存在依赖关系,因此需要一个缓冲区来存储数据。当缓冲区满时,生产者应该等待;当缓冲区为空时,消费者应该等待。
问题
如何实现这种生产者消费者模式?
解决方案
使用信号量可以很好地实现生产者消费者模式。具体来说,可以创建两个信号量:empty_sem 和 full_sem。empty_sem 表示缓冲区的剩余空间数量,full_sem 表示缓冲区已经填满的数据数量。当 empty_sem 的值大于零时,生产者可以向缓冲区中添加数据;当 full_sem 的值大于零时,消费者可以从缓冲区中取出数据。
代码示例
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> queue;
const int MAX_QUEUE_SIZE = 10;
// 生产者
void producer(int id) {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
// 如果队列已满,则等待
cv.wait(lock, [] { return queue.size() < MAX_QUEUE_SIZE; });
queue.push(id * 10 + i);
printf("producer %d: %d\n", id, i);
lock.unlock();
cv.notify_one(); // 通知消费者
}
}
// 消费者
void consumer(int id) {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 如果队列为空,则等待
cv.wait(lock, [] { return !queue.empty(); });
int data = queue.front();
queue.pop();
printf("consumer %d: %d\n", id, data);
lock.unlock();
cv.notify_one(); // 通知生产者
if (data == 99) break;
}
}
int main() {
std::thread t1(producer, 0);
std::thread t2(producer, 1);
std::thread t3(consumer, 0);
std::thread t4(consumer, 1);
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
使用读写锁实现并发读取
背景
在某个系统中,有一个共享资源需要被多个线程同时访问。其中,大多数的操作是只读操作,但也会有少数的写入操作。
问题
如何实现高效的并发读取?
解决方案
可以使用读写锁来实现高效的并发读取。具体来说,可以创建一个 std::shared_mutex 对象,然后在读操作中使用 std::shared_lock,在写操作中使用 std::unique_lock。这样可以保证在有读锁时,允许其他线程加入读锁,而在有写锁时,不允许其他线程加入读锁或写锁。
代码示例
#include <shared_mutex>
#include <iostream>
class MyClass {
public:
void read() {
std::shared_lock<std::shared_mutex> lock(m);
do_read();
}
void write() {
std::unique_lock<std::shared_mutex> lock(m);
do_write();
}
private:
void do_read() {
std::cout << "reading..." << std::endl;
}
void do_write() {
std::cout << "writing..." << std::endl;
}
std::shared_mutex m;
};
int main() {
MyClass obj;
std::thread t1([&] { obj.read(); });
std::thread t2([&] { obj.read(); });
std::thread t3([&] { obj.write(); });
t1.join();
t2.join();
t3.join();
return 0;
}
使用非阻塞 I/O 实现高并发服务器
背景
在某个系统中,需要实现一个高并发的服务器。客户端可能会频繁地连接和断开连接,因此服务器需要支持高并发。
问题
如何实现高并发的服务器?
解决方案
可以使用非阻塞 I/O 来实现高并发的服务器。具体来说,可以将 socket 设置为非阻塞模式,然后使用 select 函数来监听socket的事件。当 select 函数返回时,可以判断哪些 socket 发生了事件,然后进行相应的处理。
代码示例
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
int create_nonblock_socket() {
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
fcntl(sockfd, F_SETFL, O_NONBLOCK);
return sockfd;
}
void handle_client(int sockfd) {
char buffer[1024];
ssize_t n = recv(sockfd, buffer, sizeof(buffer), MSG_DONTWAIT);
if (n > 0) {
// handle data
std::cout << "received: " << buffer << std::endl;
} else if (n == 0) {
// client closed connection
close(sockfd);
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
// error occurred
close(sockfd);
}
}
int main() {
int listen_sockfd = create_nonblock_socket();
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listen_sockfd, (struct sockaddr*) &server_addr, sizeof(server_addr));
listen(listen_sockfd, 10);
while (true) {
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(listen_sockfd, &read_fds);
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
select(listen_sockfd + 1, &read_fds, NULL, NULL, &timeout);
if (FD_ISSET(listen_sockfd, &read_fds)) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int sockfd = accept(listen_sockfd, (struct sockaddr*) &client_addr, &addr_len);
handle_client(sockfd);
}
}
return 0;
}
实际应用场景
高并发网络服务器
高并发网络服务器是最常见的应用场景之一。这种场景下,需要处理大量的请求,因此必须使用并发技术来提高系统吞吐量。
分布式系统
分布式系统也是一个重要的应用场景。在分布式系统中,由于网络延迟、故障等原因,需要使用并发技术来保证系统的稳定性和可靠性。
嵌入式系统
嵌入式系统也是一个重要的应用场景。在嵌入式系统中,资源有限,因此需要使用高效的并发技术来提高系统的性能。
工具和资源推荐
Boost.Asio
Boost.Asio 是一个 C++ 库,提供了异步 I/O 和网络编程的支持。它可以帮助开发者快速实现高并发的服务器。
libevent
libevent 是一个事件驱动的网络 I/O 库。它可以帮助开发者实现高效的 I/O 多路复用。
POCO
POCO 是一个 C++ 框架,提供了大量的网络编程组件。它可以帮助开发者快速实现高并发的服务器。
总结:未来发展趋势与挑战
多核时代
随着计算机硬件的发展,多核 CPU 已经成为主流。因此,如何利用多核来提高系统性能成为一个重要的研究方向。
分布式系统
分布式系统在互联网时代变得越来越重要。因此,如何在分布式系统中实现高效的并发控制成为一个重要的研究方向。
异步编程
异步编程是一种新的编程模型,可以更好地利用多核 CPU。因此,如何在异步编程中实现高效的并发控制成为一个重要的研究方向。
附录:常见问题与解答
Q: 什么是并发?
A: 并发是指多个任务在同一个时间段内执行,而这些任务会交替执行,即每个时刻只能有一个任务执行。
Q: 什么是并行?
A: 并行则是指多个任务在同一个时刻同时执行。
Q: 什么是生产者消费者模式?
A: 生产者消费者模式是一种经典的并发模型,包括两种角色:生产者和消费者。生产者负责生产数据,消费者负责消费数据。生产者和消费者之间通过一个缓冲区进行数据传递。
Q: 什么是读写锁?
A: 读写锁是一种常用的并发控制机制,支持多个线程同时读取共享资源,但只允许一个线程写入共享资源。
Q: 什么是非阻塞 I/O?
A: 非阻塞 I/O 是一种常用的并发控制机制,可以减少线程上下文切换的开销,提高系统吞吐量。