多进程与多线程
- 多进程并发
使用多进程并发是将一个应用程序划分为多个独立的进程(每个进程只有一个线程),这些独立的进程间可以互相通信,共同完成任务。由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。但是这也造就了多进程并发的两个缺点:
- 在进程间的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之。
- 运行多个进程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。
由于多个进程并发完成同一个任务时,不可避免的是:操作同一个数据和进程间的相互通信,上述的两个缺点也就决定了多进程的并发不是一个好的选择。
- 多线程并发
多线程并发指的是在同一个进程中执行多个线程。
优点:
有操作系统相关知识的应该知道,线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。
缺点:
由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)。
多线程
传统的C++(C++11之前)中并没有引入线程这个概念,在C++11出来之前,如果我们想要在C++中实现多线程,需要借助操作系统平台提供的API,比如Linux的,或者windows下的 。
C++11提供了语言层面上的多线程,包含在头文件中。它解决了跨平台的问题,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。C++11 新标准中引入了5个头文件来支持多线程编程,如下图所示:
线程间通信
线程间通信:互斥锁、条件变量condition_variable、读写锁shared_lock、原子操作、信号量Semaphore(自己实现)
- 互斥锁是为上锁而优化的;
- 条件变量是为等待而优化的;
- 互斥锁,条件变量都只用于同一个进程的各线程间。
- 读写锁与互斥量类似,不过读写锁允许更高的并行性。读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当他以写模式锁住时,它是以独占模式锁住的。
- 信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性。
信号量可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。
condition_variable的一个用法是实现信号量。信号量(semaphore)是一种同步机制,但在C++11中并没有原生提供该机制,那么就需要自己去实现。
1. mutex
mutex头文件主要声明了与互斥量(mutex)相关的类。mutex提供了4种互斥类型,如下表所示。
类型 | 说明 |
std::mutex | 最基本的 Mutex 类。 |
std::recursive_mutex | 递归 Mutex 类。 |
std::time_mutex | 定时 Mutex 类。 |
std::recursive_timed_mutex | 定时递归 Mutex 类。 |
std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以递归地对互斥量对象上锁。
1.1 lock与unlock
mutex常用操作:
- lock():资源上锁
- unlock():解锁资源
- trylock():查看是否上锁,它有下列3种类情况:
(1)未上锁返回false,并锁住;
(2)其他线程已经上锁,返回true;
(3)同一个线程已经对它上锁,将会产生死锁。
死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
1.2 lock_guard
创建lock_guard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。
lock_guard的特点:
- 创建即加锁,作用域结束自动析构并解锁,无需手工解锁
- 不能中途解锁,必须等作用域结束才解锁
- 不能复制
#include
#include
#include int g_i = 0;
std::mutex g_i_mutex; // protects g_i,用来保护g_ivoid safe_increment()
{const std::lock_guard<std::mutex> lock(g_i_mutex);++g_i;
std::cout << std::this_thread::get_id() << ": " << g_i << '\n';// g_i_mutex自动解锁
}int main()
{
std::cout << "main id: " <<std::this_thread::get_id()<<std::endl;
std::cout << "main: " << g_i << '\n'; std::thread t1(safe_increment);
std::thread t2(safe_increment); t1.join();
t2.join(); std::cout << "main: " << g_i << '\n';
}
1.3 unique_lock
简单地讲,unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,同时又具有其他很多方法,使用起来更强灵活方便,能够应对更复杂的锁定需要。
unique_lock的特点:
- 创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
- 可以随时加锁解锁
- 作用域规则同 lock_grard,析构时自动释放锁
- 不可复制,可移动
- 条件变量需要该类型的锁作为参数(此时必须使用unique_lock)
所有 lock_guard 能够做到的事情,都可以使用 unique_lock 做到,反之则不然。那么何时使lock_guard呢?很简单,需要使用锁的时候,首先考虑使用 lock_guard,因为lock_guard是最简单的锁。
#include
#include
#include
struct Box {explicit Box(int num) : num_things{num} {}int num_things;
std::mutex m;
};void transfer(Box &from, Box &to, int num)
{// defer_lock表示暂时unlock,默认自动加锁
std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);//两个同时加锁
std::lock(lock1, lock2);//或者使用lock1.lock() from.num_things -= num;
to.num_things += num;//作用域结束自动解锁,也可以使用lock1.unlock()手动解锁
}int main()
{
Box acc1(100);
Box acc2(50);
std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
t1.join();
t2.join();
std::cout << "acc1 num_things: " << acc1.num_things << std::endl;
std::cout << "acc2 num_things: " << acc2.num_things << std::endl;
}
2. condition_variable
condition_variable的头文件有两个variable类,一个是condition_variable,另一个是condition_variable_any。condition_variable必须结合unique_lock使用。condition_variable_any可以使用任何的锁。下面以condition_variable为例进行介绍。
condition_variable条件变量可以阻塞(wait、wait_for、wait_until)调用的线程直到使用(notify_one或notify_all)通知恢复为止。condition_variable是一个类,这个类既有构造函数也有析构函数,使用时需要构造对应的condition_variable对象,调用对象相应的函数来实现上面的功能。
类型 | 说明 |
condition_variable | 构建对象 |
析构 | 删除 |
wait | Wait until notified |
wait_for | Wait for timeout or until notified |
wait_until | Wait until notified or time point |
notify_one | 解锁一个线程,如果有多个,则未知哪个线程执行 |
notify_all | 解锁所有线程 |
cv_status | 这是一个类,表示variable 的状态,如下所示 |
enum class cv_status { no_timeout, timeout };
2.1 wait
当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lck),直到另外某个线程调用 notify_* 唤醒了当前线程。
在线程被阻塞时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_* 唤醒了当前线程),wait()函数也是自动调用 lck.lock(),使得lck的状态和 wait 函数被调用时相同。
#include // std::cout
#include // std::thread, std::this_thread::yield
#include // std::mutex, std::unique_lock
#include // std::condition_variablestd::mutex mtx;
std::condition_variable cv;int cargo = 0;
bool shipment_available() {return cargo!=0;}void consume (int n) {
for (int i=0; i
std::unique_lock lck(mtx);//自动上锁
//第二个参数为false才阻塞(wait),阻塞完即unlock,给其它线程资源
cv.wait(lck,shipment_available);
// consume:
std::cout << cargo << '\n';
cargo=0;
}
}int main ()
{
std::thread consumer_thread (consume,10);
for (int i=0; i<10; ++i) {
//每次cargo每次为0才运行。
while (shipment_available()) std::this_thread::yield();
std::unique_lock lck(mtx);
cargo = i+1;
cv.notify_one();
}
consumer_thread.join();,
return 0;
}
2.2 wait_for
与std::condition_variable::wait() 类似,不过 wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了其他线程的通知,wait_for返回,剩下的处理步骤和 wait()类似。
template
cv_status wait_for (unique_lock& lck,
const chrono::duration& rel_time);
另外,wait_for 的重载版本的最后一个参数pred表示 wait_for的预测条件,只有当 pred条件为false时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred为 true时才会被解除阻塞。
template <class Rep, class Period, class Predicate>bool wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time, Predicate pred);
#include // std::cout
#include // std::thread
#include // std::chrono::seconds
#include // std::mutex, std::unique_lock
#include // std::condition_variable, std::cv_statusstd::condition_variable cv;int value;void read_value() {
std::cin >> value;
cv.notify_one();
}int main ()
{
std::cout << "Please, enter an integer (I'll be printing dots): \n";
std::thread th (read_value); std::mutex mtx;
std::unique_lock<std::mutex> lck(mtx);while (cv.wait_for(lck,std::chrono::seconds(1))==std::cv_status::timeout) {
std::cout << '.' << std::endl;}
std::cout << "You entered: " << value << '\n';
th.join();return 0;
}
3. atomic
3.1 atomic
#include
#include
#include
#include
#include
using namespace std;atomic<int> num (0);// 线程函数,内部对num自增1000万次
void Add()
{for(int i=0;i<10000000;i++) {
num++;}
}int main()
{
clock_t startClock = clock(); // 记下开始时间// 3个线程,创建即运行
thread t1(Add);
thread t2(Add);
thread t3(Add);// 等待3个线程结束
t1.join();
t2.join();
t3.join();
clock_t endClock = clock(); // 记下结束时间
cout<<"耗时:"<<endClock-startClock<<",单位:"<<CLOCKS_PER_SEC<<",result:"<<num<<endl;return 0;
}