基本概念
- 并发:宏观上一段时间内有多个程序在同时运行,可以由操作系统来实现并发(进程/线程/协程)
- 并行:同一时刻有多个指令同时运行,并行需要由硬件来支撑(多核处理器/分布式系统等)
- 进程:资源分配和拥有的基本单位
- 线程:程序执行/调度的基本单位。线程是轻量级的线程,共享其所属进程的资源(代码段、堆、静态区等)。若没有线程,当进程 A A A 的任务 1 阻塞时,需要拷贝进程A当前的数据及运行状态,新创建一个进程 A ′ A' A′ 来执行任务 2 ,相较于使用线程来说内存的开销更大,并且在进程间切换的开销也大于线程切换的开销。
线程基本操作Thread
创建一个线程
thread ThreadName(ThreadInit)
传入线程的入口函数 T h r e a d I n i t ThreadInit ThreadInit 来创建一个线程 T h r e a d N a m e ThreadName ThreadName。也可以使用类/对象(需重写 operator())、智能指针、带参的方法、类的成员函数或是 lambda 表达式创建一个线程。
join
thread.join()。会阻塞主线程直到当前线程执行结束后子线程与主线程汇合。
detach
thread.detach()。分离主线程与子线程,主线程运行结束后子线程会在后台执行,执行完成后由运行时库回收相关资源。若主线程中存在实例对象,则在子线程中会调用复制构造函数构建一个新的实例给子线程,因此主线程运行结束并被销毁时不会影响子线程的运行(引用同理)。
joinable
thread.joinable()。当一个线程 detach 后不可再 join,所以 join 前可以使用 joinable 判断是否可以 join。
多线程中的数据共享与互斥
在多线程并发的过程中,多个线程可能会对一块内存同时读/写,可能会导致结果数据与我们期望的数据不一致的问题,因此我们需要通过线程同步的方式来对其进行控制。
交替打印foobar
此处使用一道力扣上的多线程题目作为例题。
给你一个类:
class FooBar {
public void foo() {
for (int i = 0; i < n; i++) {
print("foo");
}
}
public void bar() {
for (int i = 0; i < n; i++) {
print("bar");
}
}
}
两个不同的线程将会共用一个 FooBar 实例:
线程 A 将会调用 foo() 方法,而
线程 B 将会调用 bar() 方法
请设计修改程序,以确保 “foobar” 被输出 n 次。
示例 1:
输入:n = 1
输出:"foobar"
解释:这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,"foobar" 将被输出一次。
示例 2:
输入:n = 2
输出:"foobarfoobar"
解释:"foobar" 将被输出两次。
依题意需要控制调用两线程的运行顺序,达到控制foo和bar交替输出的效果。
互斥锁
设置一互斥锁后可以使用lock()函数给临界区加锁,并且只有一个线程能获得锁,若当前线程无法上锁,则阻塞;反之会在加锁后继续运行。
此处对 foo 和 bar 分别设置一互斥锁,初始化时对 bar 上锁,因此输出 foo 的线程可以执行,而输出 bar 的线程被阻塞。输出完成 foo 后只打开 bar 的锁,foo 仍处于锁定状态;bar 同理。
class FooBar {
private:
int n;
mutex fooLock, barLock;
public:
FooBar(int n) {
this->n = n;
barLock.lock();
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
fooLock.lock();
printFoo();
barLock.unlock();
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
barLock.lock();
printBar();
fooLock.unlock();
}
}
};
也可以使用 try_lock() 尝试上锁,但 try_lock() 不会阻塞线程,若无法加锁,则返回 false,反之返回true。
lock_guard()
类似于智能指针,使用了 RAII 技术,不需要手动 unlock,等离开作用域后就会自动释放。
recursive_lock() 递归互斥锁
允许同一线程多次获得互斥锁
timed_mutex() 超时独占互斥锁
获取互斥锁时若超出超时时长,则放弃等待,解除阻塞。
unique_lock()
比 lock_guard 效率更低,内存占用更大,但比 unique_lock() 更灵活。同时在第二个参数中指定 adopt_lock(),则创建锁时不会再调用构造函数上锁(但这个锁必须要被锁定过),而仍会调用析构函数解锁。
信号量
#include<semaphore.h>
class FooBar {
private:
int n;
sem_t mutex_foo, mutex_bar;
public:
FooBar(int n) {
this->n = n;
sem_init(&mutex_foo, 0, 1);
sem_init(&mutex_bar, 0, 0);
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
sem_wait(&mutex_foo);
printFoo();
sem_post(&mutex_bar);
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
sem_wait(&mutex_bar);
printBar();
sem_post(&mutex_foo);
}
}
};
原子操作
class FooBar {
private:
int n;
atomic<bool> foo_done=false;
public:
FooBar(int n) {
this->n = n;
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
while(foo_done) {
this_thread::yield();
}
printFoo();
foo_done = true;
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
while(!foo_done) {
this_thread::yield();
}
printBar();
foo_done = false;
}
}
};
条件变量
class FooBar {
private:
int n;
mutex lock;
condition_variable cond;
bool foo_done = false;
public:
FooBar(int n) {
this->n = n;
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
unique_lock<mutex> locker(lock);
cond.wait(locker, [&](){return foo_done == false;});
printFoo();
foo_done = true;
cond.notify_one();
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
unique_lock<mutex> locker(lock);
cond.wait(locker, [&](){return foo_done;});
printBar();
foo_done = false;
cond.notify_one();
}
}
};