C++11新特性

  1. thread、condition、mutex
  2. atomic
  3. function、bind
  4. 使用新特性实现线程池(支持可变参数列表)
  5. 异常
  6. 协程

1.1 线程thread

std::thread 在 #include 头文件中声明,因此使用 std::thread 时需要包含 #include 头文件。编译的时候后面也要链接上-lpthread

初始化构造函数

//创建std::thread执行对象,该thread对象可被joinable,新产生的线程会调用threadFun函数,该函
数的参数由 args 给出
template<class Fn, class... Args>
explicit thread(Fn&& fn, Args&&... args);

这里我们可以看到线程第一个参数是函数, 而函数的参数如果对应的是左值引用类型的话, 我们在传入后面几个参数的时候, 如果不表明ref强转为引用的话, 会导致后面的参数是值传递进来的, 并且对应的是右值, 而左值引用是不能绑定右值的, 即我们在传入线程参数过程中, 如果有引用尽量使用ref进行显式转换

默认删除拷贝构造

支持移动拷贝, 将线程的所有权转移至另一个句柄

主要成员函数

  • get_id()
    • 获取线程ID,返回类型std::thread::id对象。
  • joinable()
    • 判断线程是否可以加入等待
  • join()
    • 等该线程执行完成后才返回。
  • detach()
    • detach调用之后,目标线程就成为了守护线程,驻留后台运行,与之关联的std::thread对象
      失去对目标线程的关联,无法再通过std::thread对象取得该线程的控制权。当线程主函数执
      行完之后,线程就结束了,运行时库负责清理与该线程相关的资源。
    • 调用 detach 函数之后:*this不再持有线程执行实例, joinable为false
      例如:
#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
 
void f1(int n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 1 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
void f2(int& n)
{
    for (int i = 0; i < 5; ++i) {
        std::cout << "Thread 2 executing\n";
        ++n;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}
 
class foo
{
public:
    void bar()
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 3 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};
 
class baz
{
public:
    void operator()()
    {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread 4 executing\n";
            ++n;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    int n = 0;
};
 
int main()
{
    int n = 0;
    foo f;
    baz b;
    std::thread t1; // t1 不是线程
    std::thread t2(f1, n + 1); // 按值传递
    std::thread t3(f2, std::ref(n)); // 按引用传递
    std::thread t4(std::move(t3)); // t4 现在运行 f2() 。 t3 不再是线程
    std::thread t5(&foo::bar, &f); // t5 在对象 f 上运行 foo::bar()
    std::thread t6(b); // t6 在对象 b 的副本上运行 baz::operator()
    t2.join();
    t4.join();
    t5.join();
    t6.join();
    std::cout << "Final value of n is " << n << '\n';
    std::cout << "Final value of f.n (foo::n) is " << f.n << '\n';
    std::cout << "Final value of b.n (baz::n) is " << b.n << '\n';
}

互斥量

mutex又称互斥量,C++ 11中与 mutex相关的类(包括锁类型)和函数都声明在 头文件中,所以如果
你需要使用 std::mutex,就必须包含 头文件。
C++11提供如下4种语义的互斥量(mutex)

  • std::mutex,独占的互斥量,不能递归使用。
  • std::time_mutex,带超时的独占互斥量,不能递归使用。
  • std::recursive_mutex,递归互斥量,不带超时功能。
  • std::recursive_timed_mutex,带超时的递归互斥量。

unique_lock,lock_guard的区别

  • unique_lock与lock_guard都能实现自动加锁和解锁,但是前者更加灵活,能实现更多的功能。
  • unique_lock可以进行临时解锁和再上锁,如在构造对象之后使用lck.unlock()就可以进行解锁,
    lck.lock()进行上锁,而不必等到析构时自动解锁。

条件变量的目的就是为了,在没有获得某种提醒时长时间休眠; 如果正常情况下, 我们需要一直循环
(+sleep), 这样的问题就是CPU消耗+时延问题,条件变量的意思是在cond.wait这里一直休眠直到
cond.notify_one唤醒才开始执行下一句; 还有cond.notify_all()接口用于唤醒所有等待的线程。

  • wait: 如果线程被唤醒或者超时那么会先进行lock获取锁, 再判断条件(传入的参数)是否成立, 如果成立则wait函数返回否则释放锁继续休眠
  • notify: 进行notify动作并不需要获取锁

条件变量

wait

template< class Lock, class Predicate >
void wait( Lock& lock, Predicate pred );

  1. 原子地解锁 lock ,阻塞当前执行线程,并将它添加到于 *this 上等待的线程列表。线程将在执行 notify_all() 或 notify_one() 时被解除阻塞。解阻塞时,无关乎原因, lock 再次锁定且 wait 退出。
  2. 等价于
while (!pred()) {
    wait(lock);
}
  1. 可中断等待:在 wait() 期间注册 condition_variable_any ,使得若在给定的 stoken 的关联停止状态上作出停止请求,则提醒它;它等价于
while (!stoken.stop_requested()) {
    if (pred()) return true;
    wait(lock);
}
return pred();

注意返回值指示 pred 是否求值为 true ,无关乎是否作出了停止请求。

若这些函数不能满足后置条件(调用方线程锁定 lock ),则调用 std::terminate 。例如,这可能在重锁定互斥抛异常的情况下发生。

实例:

#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>
 
std::condition_variable_any cv;
std::mutex cv_m; // 此互斥用于三个目的:
                 // 1) 同步到 i 的访问
                 // 2) 同步到 std::cerr 的访问
                 // 3) 为条件变量 cv
int i = 0;
 
void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cerr << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cerr << "...finished waiting. i == 1\n";
}
 
void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lk(cv_m);
        std::cerr << "Notifying...\n";
    }
    cv.notify_all();
 
    std::this_thread::sleep_for(std::chrono::seconds(1));
 
    {
        std::lock_guard<std::mutex> lk(cv_m);
        i = 1;
        std::cerr << "Notifying again...\n";
    }
    cv.notify_all();
}
 
int main()
{
    std::thread t1(waits), t2(waits), t3(waits), t4(signals);
    t1.join(); 
    t2.join(); 
    t3.join();
    t4.join();
}
OPUTUT: ****
Waiting...
Waiting...
Waiting...
Notifying...
Notifying again...
...finished waiting. i == 1
...finished waiting. i == 1
...finished waiting. i == 1

包含两种重载,第一种只包含unique_lock对象,另外一个Predicate 对象(等待条件),这里必须使用
unique_lock,因为wait函数的工作原理:

  • 当前线程调用wait()后将被阻塞并且函数会解锁互斥量,直到另外某个线程调用notify_one或者
    notify_all唤醒当前线程;一旦当前线程获得通知(notify),wait()函数也是自动调用lock(),同理不能使用lock_guard对象。
  • 如果wait没有第二个参数,第一次调用默认条件不成立,直接解锁互斥量并阻塞到本行,直到某一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后无条件地继续进行后面的操作。
  • 如果wait包含第二个参数,如果第二个参数不满足,那么wait将解锁互斥量并堵塞到本行,直到某一个线程调用notify_one或notify_all为止,被唤醒后,wait重新尝试获取互斥量,如果得不到,线程会卡在这里,直到获取到互斥量,然后继续判断第二个参数,如果表达式为false,wait对互斥量解锁,然后休眠,如果为true,则进行后面的操作。

wait_for函数

函数原型:

template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time);
template <class Rep, class Period, class Predicate>
bool wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time, Predicatepred);

和wait不同的是,wait_for可以执行一个时间段,在线程收到唤醒通知或者时间超时之前,该线程都会处于阻塞状态,如果收到唤醒通知或者时间超时,wait_for返回,剩下操作和wait类似。

wait_until

notify_one

notify_all

简单实例

#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>
 
std::condition_variable_any cv;
std::mutex cv_m;
int i = 0;
bool done = false;
 
void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cout << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cout << "...finished waiting. i == 1\n";
    done = true;
}
 
void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Notifying falsely...\n";
    cv.notify_one(); // 等待线程被通知 i == 0.
                     // cv.wait 唤醒,检查 i ,再回到等待
 
    std::unique_lock<std::mutex> lk(cv_m);
    i = 1;
    while (!done) 
    {
        std::cout << "Notifying true change...\n";
        lk.unlock();
        cv.notify_one(); // 等待线程被通知 i == 1 , cv.wait 返回
        std::this_thread::sleep_for(std::chrono::seconds(1));
        lk.lock();
    }
}
 
int main()
{
    std::thread t1(waits), t2(signals);
    t1.join(); 
    t2.join();
}

可能的输出

Waiting... 
Notifying falsely...
Notifying true change...
...finished waiting. i == 1

这里需要注意的是,wait函数中会释放mutex,而lock_guard这时还拥有mutex,它只会在出了作用域之后才会释放mutex,所以这时它并不会释放,但执行wait时会提前释放mutex。
从语义上看这里使用lock_guard会产生矛盾,但是实际上并不会出问题,因为wait提前释放锁之后会处于等待状态,在被notify_one或者notify_all唤醒后会先获取mutex,这相当于lock_guard的mutex在释放之后又获取到了,因此,在出了作用域之后lock_guard自动释放mutex不会有问题。
这里应该用unique_lock,因为unique_lock不像lock_guard一样只能在析构时才释放锁,它可以随时释放锁,因此在wait时让unique_lock释放锁从语义上更加准确。

原子变量

定义于头文件 <memory>
注意:
尽量不要进行简单的赋值初始化

atomic<int> flag = 1;   // 不建议如此操作 错误初始化
atomic<int> flag(1);   // 建议如此操作   准确初始化

operator = 等价于 store

另外再C++20中atomic还拥有wait和nofity_one函数已经原子指针
定义于头文件 <stdatomic.h>
一定条件下可以使用std::atomic<std::shared_ptr>作为原子共享指针来进行编写代码
wait(C++20)
阻塞线程直至被提醒且原子值更改
notify_one(C++20)
提醒至少一个在原子对象上的等待中阻塞的线程

异步操作

  • std::future : 异步指向某个任务,然后通过future特性去获取任务函数的返回结果。
  • std::aysnc: 异步运行某个任务函数
  • std::packaged_task :将任务和feature绑定在一起的模板,是一种封装对任务的封装。
  • std::promise
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值