网站正在建设 敬请期待,企业网站空间买虚拟主机,wordpress cdn 非插件,知名品牌策划公司文章目录thread 创建mutexmutexrecursive_mutextimed_mutexlock_guard原子操作atomic条件变量condition_variable其他线程安全问题shared_ptr单例模式C 线程库是 C11 标准中引入的一个特性#xff0c;它使得 C 在语言级别上支持多线程编程#xff0c;不需要依赖第三方库或操作…
文章目录thread 创建mutexmutexrecursive_mutextimed_mutexlock_guard原子操作atomic条件变量condition_variable其他线程安全问题shared_ptr单例模式C 线程库是 C11 标准中引入的一个特性它使得 C 在语言级别上支持多线程编程不需要依赖第三方库或操作系统 API。C 线程库主要包括以下几个部分std::thread创建和管理子线程的类std::mutex互斥锁用于保护共享数据的访问std::condition_variable条件变量用于同步线程之间的状态std::future 和 std::promise异步任务和结果的封装std::async启动异步任务的函数
thread 创建
thread
std::thread 类的构造函数 std::thread 是一个类它表示一个可执行的线程。你可以用它来创建和管理子线程让它们并发地执行不同的任务。std::thread 有以下几个特点
它不可拷贝只能移动。如上(3)(4)它可以被 join 或 detachjoin 表示等待线程结束detach 表示让线程自行运行它有一个唯一的标识符 std::thread::id可以用来区分不同的线程 在命名空间 this_thread下有一个 get_id 函数可以帮助我们获取线程 id
yield可以使当前线程让出时间片
sleep_untilsleep 到特定时间点
sleep_forsleep 一段时间 一个双线程循环打印的例子
#include iostream
#include threadusing namespace std;void print(int n) {for (int i 0; i n; i) {cout this_thread::get_id() : i endl; // 打印 [线程id]:ithis_thread::sleep_for(chrono::seconds(1)); // sleep 1 秒}
}int main() {thread t1(print, 100);thread t2(print, 100);t1.join();t2.join();return 0;
}mutex
mutex
例一给上面的代码加锁
#include iostream
#include thread
#include mutexusing namespace std;mutex mtx; // 创建一把锁void print(int n) {mtx.lock(); // 加锁for (int i 0; i n; i) {cout this_thread::get_id() : i endl; // 打印 [线程id]:ithis_thread::sleep_for(chrono::milliseconds(100)); // sleep 100 ms}mtx.unlock(); // 解锁
}int main() {thread t1(print, 100);thread t2(print, 100);t1.join();t2.join();return 0;
}例二
如果锁是创建在局部则要通过函数参数传入
由于锁不支持拷贝所以必须通过引用传入又由于可变模板参数会默认识别成传值所以必须先使用 ref 来强制转换成引用。
#include iostream
#include thread
#include mutexusing namespace std;void print(int n, mutex mtx) {mtx.lock(); // 加锁for (int i 0; i n; i) {cout this_thread::get_id() : i endl; // 打印 [线程id]:ithis_thread::sleep_for(chrono::milliseconds(100)); // sleep 100 ms}mtx.unlock(); // 解锁
}int main() {mutex mtx; // 创建一把锁thread t1(print, 100, ref(mtx)); // 必须使用 ref 来传引用thread t2(print, 100, ref(mtx));t1.join();t2.join();return 0;
}例三
配合 vector 和 lambda 使用
#include iostream
#include thread
#include mutex
#include vectorusing namespace std;int main() {mutex mtx; // 创建一把锁int x 0, n 10, m;cin m;vectorthread v(m);for (int i 0; i m; i) {v[i] thread([]() {mtx.lock(); // 加锁for (int i 0; i n; i) {cout this_thread::get_id() : i endl; // 打印 [线程id]:ithis_thread::sleep_for(chrono::milliseconds(100)); // sleep 100 ms}mtx.unlock(); // 解锁});}for (auto t : v) {t.join();}return 0;
}recursive_mutex
如果在递归函数中使用普通的互斥锁会造成死锁因为当一个线程执行到 unlock 之前就可能会递归调用自己然后从头开始执行当再次遇到 lock 时由于之前没有执行 unlock就会死锁。
而 recursive_mutex 可以防止递归造成的死锁它在每次 lock 的时候会判断是不是当前线程如果是就不用阻塞可以继续向下执行了。
timed_mutex
定时互斥锁该类锁可以设置锁住的时间。
由它的两个成员实现 try_lock_for 和 try_lock_until.
到时间后我们不需要手动解锁而是自动解锁。
lock_guard
智能锁通过 RAII 技术实现。
它的实现类似于如下方法
templateclass Lock
class LockGuard {
public:LockGuard(Lock lk) : _lock(lk) {_lock.lock();}~LockGuard() {_lock.unlock();}
private:Lock _lock;
};例
int main() {mutex mtx; // 创建一把锁atomicint x 0;//int x 0;int n 100, m;cin m;vectorthread v(m);for (int i 0; i m; i) {v[i] thread([]() {for (int i 0; i n; i) {lock_guardmutex lk(mtx); // 智能锁cout this_thread::get_id() : i endl;this_thread::sleep_for(chrono::microseconds(100));}});}for (auto t : v) {t.join();}cout x endl;return 0;
}注unique_guard 比 lock_guard 多一个支持手动加锁的解锁的功能。
原子操作
atomic
我们知道 操作不是线程安全的要避免多线程同时修改造成的数据不一致的问题可以通过加锁来解决。
但是互斥锁会产生上下文切换的开销容易产生死锁。更好的解决方法是使用原子操作。 CAS Compare Set或 Compare Swap是一种原子操作也就是说它是一个不会被其他线程打断的操作。CAS 有三个操作数内存值 V旧的预期值 A要修改的新值 B。CAS 的过程是这样的先比较内存值 V 和旧的预期值 A 是否相等如果相等就用新值 B 替换内存值 V如果不相等就放弃操作或者重试。CAS 可以用于实现无锁算法避免多线程同时修改同一数据时产生的数据不一致问题。
C11 中的 atomic 类就是 CAS 的一种实现。
例
#include iostream
#include thread
#include vectorusing namespace std;int main() {//int x 0;atomicint x 0;int n 100000, m;cin m;vectorthread v(m);for (int i 0; i m; i) {v[i] thread([]() {for (int i 0; i n; i) {x; // 原子操作}});}for (auto t : v) {t.join();}cout x endl;return 0;
}条件变量
condition_variable
条件变量是一种同步原语用于让线程在某个条件发生时才继续执行。 条件变量与互斥锁配合使用让线程在等待条件时释放锁从而避免竞争状态。条件变量提供了一种原子操作即解锁并睡眠的操作以及唤醒并加锁的操作。条件变量有两个动作等待wait和通知notify。等待动作会让线程挂起并释放已经持有的锁。通知动作会唤醒一个或多个等待的线程并让它们重新获取锁。
在 C11 中你可以使用 std::condition_variable 类来创建和操作条件变量。你需要配合一个互斥锁std::mutex和一个谓词std::functionbool()来使用条件变量。你可以调用条件变量的成员函数如wait()notify_one()notify_all()等来实现线程间的同步。 predicate(2) 是增加了谓词的版本pred 是一个 std::functionbool()它返回 false 则表示阻塞返回 true 时解除阻塞。
例创建两个线程交替打印 0~100如线程 t2 先打印 0则下面必须是另一个线程 t1 打印 1然后 t2 打印 2 …
这是一个运用条件变量解决的经典场景我们可以让一个线程打印完一个数之后立马通知另一个线程从而保证两个线程交替进行。
#include iostream
#include thread
#include mutex
#include condition_variableusing namespace std;int main() {int i 0;int n 100;mutex mtx;condition_variable cv;bool ready true; // 开始标志初始为 true可以使线程 t2 先打印thread t1([]() {while (i n) {unique_lockmutex lock(mtx); // 条件变量需要的互斥锁cv.wait(lock, [ready]() {return !ready; }); // 如果 ready 为 true 则阻塞cout this_thread::get_id() : i endl;i;ready true; // 更改条件cv.notify_one();// 唤醒另一个线程}});thread t2([]() {while (i n) {unique_lockmutex lock(mtx);cv.wait(lock, [ready]() {return ready; }); // 如果 ready 为 false 则阻塞cout this_thread::get_id() : i endl;i;ready false; // 更改条件cv.notify_one();// 唤醒另一个线程}});t1.join();t2.join();return 0;
}例2生产者消费者模型
#include iostream
#include thread
#include mutex
#include condition_variable
#include queuestd::mutex mtx; // 互斥锁
std::condition_variable cv; // 条件变量
std::queueint q; // 共享队列
bool finished false; // 结束标志void producer(int n) {for (int i 0; i n; i) {std::unique_lockstd::mutex lock(mtx); // 加锁std::this_thread::sleep_for(std::chrono::milliseconds(100));q.push(i); // 生产数据std::cout produced i \n;lock.unlock(); // 解锁cv.notify_one(); // 通知消费者}{std::unique_lockstd::mutex lock(mtx); // 加锁finished true; // 设置结束标志}cv.notify_all(); // 通知所有消费者
}void consumer() {while (true) {std::unique_lockstd::mutex lock(mtx); // 加锁cv.wait(lock, [] {return finished || !q.empty(); }); // 等待条件成立结束或队列非空if (finished q.empty()) {break; // 结束且队列空退出循环}std::this_thread::sleep_for(std::chrono::milliseconds(100));int x q.front(); // 取出数据q.pop();std::cout std::this_thread::get_id() : consumed x \n;}
}int main() {std::thread t1(producer, 10); // 创建生产者线程生产10个数据std::thread t2(consumer); // 创建消费者线程1std::thread t3(consumer); // 创建消费者线程2t1.join();t2.join();t3.join();return 0;
}运行结果 其他线程安全问题
shared_ptr
Qshared_ptr 是线程安全的吗
Ashared_ptr 的引用计数保证是线程安全的但访问资源不是如果你想在多个线程之间共享同一个 shared_ptr 指向的对象你需要使用互斥锁或原子操作来保护它。 单例模式
饿汉模式
特点 不允许随便创建对象构造函数私有防拷贝main 函数之前就创建初始化对象 缺点 对象初始化麻烦影响程序启动速度多个单例类如果有依赖顺序关系无法控制
懒汉模式
特点 不允许随便创建对象构造函数私有防拷贝第一次使用对象时创建对象 缺点 多个线程使用第一次调用存在竞争的线程安全问题。
所以饿汉模式不用考虑线程安全问题而懒汉模式有线程安全问题
下面是一个懒汉模式的代码示例
class Singleton {
public:static Singleton* GetInstance() {if (_spInst nullptr) {_spInst new Singleton;}return _spInst;}
private:Singleton() {}Singleton(const Singleton) delete;static Singleton* _spInst;
};Singleton* Singleton::_spInst nullptr;如果有多个线程同时调用 GetInstance可能会创建多个对象实例违反了单例模式的原则。
这个问题可以通过加锁解决
下面的代码是一个双重检查加锁的懒汉模式的实现双重检查加锁是一种用于减少获取锁的开销的软件设计模式。程序先检查锁定条件只有当检查表明需要锁定时才获取锁然后检查是否已经实例化对象防止创建多个对象。
class Singleton {
public:static Singleton* GetInstance() {if (_spInst nullptr) { // 第一重检查std::unique_lockstd::mutex lock(_mtx);if (_spInst nullptr) { // 第二重检查_spInst new Singleton;}}return _spInst;}
private:Singleton() {}Singleton(const Singleton) delete;static Singleton* _spInst;static std::mutex _mtx;
};Singleton* Singleton::_spInst nullptr;
std::mutex Singleton::_mtx;你会发现这两重检查缺一不可如果缺少了第一重检查那么每次调用 GetInstance 都会加锁解锁增加了获取锁的开销。如果缺少了第二重检查那么两个线程会竞争一把锁当其中一个线程加锁之后另一个线程会阻塞当一个线程完成了对象的实例化并释放锁另一个线程就会获取锁此时如果没有第二重检查那么它就会再创建一个对象违反了单例模式的原则。 第二种线程安全的懒汉模式的实现方式
它利用了静态局部对象只会在第一次调用时初始化特性。
class Singleton {
public:static Singleton* GetInstance() {static Singleton _s;return _s;}
private:Singleton() {}Singleton(const Singleton) delete;Singleton operator(Singleton const) delete;
};但是这种实现方式有一个缺点它在 C11 之前不能保证是线程安全的因为 C11 之前局部静态对象的构造函数并不能保证是线程安全的。