基本使用
头文件
include
创建线程
std::thread t(hello);
#include <iostream>#include <thread>void hello() {std::cout << "Hello Concurrent World!" << std::endl;std::cout << "i am " << std::this_thread::get_id() << std::endl;}int main() {std::thread t(hello);t.join();}
创建含参线程
#include <iostream>#include <thread>#include <string>void hello(const std::string& s) {std::cout << "Hello Concurrent World!" << std::endl;std::cout << "i am " << std::this_thread::get_id() << std::endl;std::cout << s << std::endl;}int main() {std::thread t(hello,static_cast<std::string>("ni hao"));t.join();}
用类的成员函数创建线程
class Hello{public:void hello(){std::cout<<"Hello Concurrent World!"<<std::endl;}};int main(){Hello h;std::thread t(&Hello::hello,&h);t.join();return 0;}
分离线程
t.detach();
获取线程id
this_thread::get_id(); //获取当前线程idt1.get_id(); //获取指定线程id
互斥
头文件
include
主要类
std::mutexstd::lock_guardstd::unique_lock
std::lock_guard构造函数
locking (1) explicit lock_guard (mutex_type& m);adopting (2) lock_guard (mutex_type& m, adopt_lock_t tag);copy [deleted](3) lock_guard (const lock_guard&) = delete;
- locking 初始化
lock_guard 对象管理 Mutex 对象 m,并在构造时对 m 进行上锁(调用 m.lock())。 adopting初始化
lock_guard 对象管理 Mutex 对象 m,与 locking 初始化(1) 不同的是, Mutex 对象 m 已被当前线程锁住。std::lock_guard的使用
#include <iostream> // std::cout#include <thread> // std::thread#include <mutex> // std::mutex, std::lock_guard#include <stdexcept> // std::logic_errorstd::mutex mtx;void print_even (int x) {if (x%2==0) std::cout << x << " is even\n";else throw (std::logic_error("not even"));}void print_thread_id (int id) {try {// using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:std::lock_guard<std::mutex> lck (mtx);print_even(id);}catch (std::logic_error&) {std::cout << "[exception caught]\n";}}int main (){std::thread threads[10];// spawn 10 threads:for (int i=0; i<10; ++i)threads[i] = std::thread(print_thread_id,i+1);for (auto& th : threads) th.join();return 0;}
std::unique_lock
提供比lock_guard更细粒度的操作
class LogFile {ofstream f;mutex m1,m2;public:LogFile() {f.open("log.txt");}void shared_print(string msg,int idx) {unique_lock<mutex> locker(m1,defer_lock); //第三种加锁方式locker.unlock();do_something(); //此代码块不想加锁locker.lock(); // 解锁之后还可以加锁,增加灵活性。f << msg << ":" << idx << endl;locker.unlock();do_some_other_thing();locker.lock();//此外unique_lock可以被移动,lock_guard则不能被移动unique_lock<mutex> locker2 = move(locker);}};
条件变量std::condition_variable
wait()```cpp std::mutex mutex; std::condition_variable cv;
// 条件变量与临界区有关,用来获取和释放一个锁,因此通常会和mutex联用。 std::unique_lock lock(mutex); // 此处会释放lock,然后在cv上等待,直到其它线程通过cv.notify_xxx来唤醒当前线程,cv被唤醒后会再次对lock进行上锁,然后wait函数才会返回。 // wait返回后可以安全的使用mutex保护的临界区内的数据。此时mutex仍为上锁状态 cv.wait(lock)
需要注意的一点是, wait有时会在没有任何线程调用notify的情况下返回,这种情况就是有名的[**spurious wakeup**](https://docs.microsoft.com/zh-cn/windows/desktop/api/synchapi/nf-synchapi-sleepconditionvariablecs)。因此当wait返回时,你需要再次检查wait的前置条件是否满足,如果不满足则需要再次wait。wait提供了重载的版本,用于提供前置检查。```cpptemplate <typename Predicate>void wait(unique_lock<mutex> &lock, Predicate pred) {while(!pred()) {wait(lock);}}
虚假唤醒spurious wakeup
一般而言线程调用wait()方法后,需要其他线程调用notify,notifyAll方法后,线程才会从wait方法中返回, 而虚假唤醒(spurious wakeup)是指线程通过其他方式,从wait方法中返回。
下面是一个买票退票的操作例子,买票时,线程A买票,如果发现没有余票,则会调用wait方法,线程进入等待队列中,线程B进行退票操作,余票数量加一,然后调用notify 方法通知等待线程,此时线程A被唤醒执行购票操作。
从程序的顺序性来看 if (remainTicketNum<=0)没有问题,但是为什么会出现虚假唤醒呢?
因为wait方法可以分为三个操作:
(1)释放锁并阻塞
(2)等待条件cond发生
(3)获取通知后,竞争获取锁
假设此时有线程A,C买票,线程A调用wait方法进入等待队列,线程C买票时发现线程B在退票,获取锁失败,线程C阻塞,进入阻塞队列,线程B退票时,余票数量+1(满足条件2 等待条件发生),线程B调用notify方法后,线程C马上竞争获取到锁,购票成功后余票为0,而线程A此时正处于wait方法醒来过程中的第三步(竞争获取锁获取锁),当线程C释放锁,线程A获取锁后,会执行购买的操作,而此时是没有余票的。
notify()
notify_one和notify_all。
- notify_one 唤醒等待的一个线程,注意只唤醒一个。
- notify_all 唤醒所有等待的线程。使用该函数时应避免出现惊群效应。 ```cpp std::mutex mutex; std::condition_variable cv;
std::unique_lock lock(mutex); // 所有等待在cv变量上的线程都会被唤醒。但直到lock释放了mutex,被唤醒的线程才会从wait返回。 cv.notify_all(lock) ```
notify和unlock的顺序
notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。
将notify_one放在解锁之前:
notifying thread: notify -> eventually release lock
notified thread: awaken -> attempt to acquire lock and fail -> block until lock available -> acquire lock after notifying thread releases it
在解锁后放置notify_one:
notifying thread: notify
notified thread: awaken -> attempt to acquire lock and succeed
所以先解锁后唤醒可以节省进程切换的消耗
信号量Semaphore
用mutex与condition_variable实现
