写给开发者的软件架构实战:处理并发和多线程的策略

50 阅读7分钟

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 是一种常用的并发控制机制,可以减少线程上下文切换的开销,提高系统吞吐量。