基本使用

头文件

include

创建线程

std::thread t(hello);

  1. #include <iostream>
  2. #include <thread>
  3. void hello() {
  4. std::cout << "Hello Concurrent World!" << std::endl;
  5. std::cout << "i am " << std::this_thread::get_id() << std::endl;
  6. }
  7. int main() {
  8. std::thread t(hello);
  9. t.join();
  10. }

创建含参线程

  1. #include <iostream>
  2. #include <thread>
  3. #include <string>
  4. void hello(const std::string& s) {
  5. std::cout << "Hello Concurrent World!" << std::endl;
  6. std::cout << "i am " << std::this_thread::get_id() << std::endl;
  7. std::cout << s << std::endl;
  8. }
  9. int main() {
  10. std::thread t(hello,static_cast<std::string>("ni hao"));
  11. t.join();
  12. }

用类的成员函数创建线程

  1. class Hello{
  2. public:
  3. void hello(){
  4. std::cout<<"Hello Concurrent World!"<<std::endl;
  5. }
  6. };
  7. int main(){
  8. Hello h;
  9. std::thread t(&Hello::hello,&h);
  10. t.join();
  11. return 0;
  12. }

分离线程

  1. t.detach();

获取线程id

  1. this_thread::get_id(); //获取当前线程id
  2. t1.get_id(); //获取指定线程id

互斥

头文件

include

主要类

  1. std::mutex
  2. std::lock_guard
  3. std::unique_lock

std::lock_guard构造函数

  1. locking (1) explicit lock_guard (mutex_type& m);
  2. adopting (2) lock_guard (mutex_type& m, adopt_lock_t tag);
  3. copy [deleted](3) lock_guard (const lock_guard&) = delete;
  1. locking 初始化
    lock_guard 对象管理 Mutex 对象 m,并在构造时对 m 进行上锁(调用 m.lock())。
  2. adopting初始化
    lock_guard 对象管理 Mutex 对象 m,与 locking 初始化(1) 不同的是, Mutex 对象 m 已被当前线程锁住。

    std::lock_guard的使用

    1. #include <iostream> // std::cout
    2. #include <thread> // std::thread
    3. #include <mutex> // std::mutex, std::lock_guard
    4. #include <stdexcept> // std::logic_error
    5. std::mutex mtx;
    6. void print_even (int x) {
    7. if (x%2==0) std::cout << x << " is even\n";
    8. else throw (std::logic_error("not even"));
    9. }
    10. void print_thread_id (int id) {
    11. try {
    12. // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:
    13. std::lock_guard<std::mutex> lck (mtx);
    14. print_even(id);
    15. }
    16. catch (std::logic_error&) {
    17. std::cout << "[exception caught]\n";
    18. }
    19. }
    20. int main ()
    21. {
    22. std::thread threads[10];
    23. // spawn 10 threads:
    24. for (int i=0; i<10; ++i)
    25. threads[i] = std::thread(print_thread_id,i+1);
    26. for (auto& th : threads) th.join();
    27. return 0;
    28. }

    std::unique_lock

    提供比lock_guard更细粒度的操作

    1. class LogFile {
    2. ofstream f;
    3. mutex m1,m2;
    4. public:
    5. LogFile() {
    6. f.open("log.txt");
    7. }
    8. void shared_print(string msg,int idx) {
    9. unique_lock<mutex> locker(m1,defer_lock); //第三种加锁方式
    10. locker.unlock();
    11. do_something(); //此代码块不想加锁
    12. locker.lock(); // 解锁之后还可以加锁,增加灵活性。
    13. f << msg << ":" << idx << endl;
    14. locker.unlock();
    15. do_some_other_thing();
    16. locker.lock();
    17. //此外unique_lock可以被移动,lock_guard则不能被移动
    18. unique_lock<mutex> locker2 = move(locker);
    19. }
    20. };

    条件变量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)

  1. 需要注意的一点是, wait有时会在没有任何线程调用notify的情况下返回,这种情况就是有名的[**spurious wakeup**](https://docs.microsoft.com/zh-cn/windows/desktop/api/synchapi/nf-synchapi-sleepconditionvariablecs)。因此当wait返回时,你需要再次检查wait的前置条件是否满足,如果不满足则需要再次wait。wait提供了重载的版本,用于提供前置检查。
  2. ```cpp
  3. template <typename Predicate>
  4. void wait(unique_lock<mutex> &lock, Predicate pred) {
  5. while(!pred()) {
  6. wait(lock);
  7. }
  8. }

虚假唤醒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实现