c++11 新特性实战 (一)

c++11多线程操作

  • 线程

    • thread
    int main()
    {
        thread t1(Test1);
        t1.join();
        thread t2(Test2);
        t2.join();
        thread t3 = t1;
        thread t4(t1);
        thread t5 = std::move(t1);
        thread t6(std::move(t1));
        return 0;
    }
    

    t3,t4创建失败,因为thread的拷贝构造和赋值运算符重载的原型是:

    thread(const thread&) = delete;
    thread& operator=(const thread&) = delete;
    

    被禁用了,但是t5, t6线程是创建成功的。std::move把t1转换为右值,调用的是函数原型为thread& operator=(thread&& _Other) noexceptthread(thread&& _Other) noexcept

    当线程对象t1被移动拷贝和移动赋值给t5和t6的时候,t1就失去了线程控制权,也就是一个线程只能同时被一个线程对象所控制。最直观的是t1.joinable()返回值为false,joinable()函数后面介绍。

    使用类成员函数作为线程参数

    class Task
    {
    public:
        Task(){}
        void Task1() {}
        void Task2() {}
    private:
    };
    
    int main()
    {
        Task task;
        thread t3(&Task::Task1, &task);
        t3.join();
        return 0;
    }
    

    关键点是要创建一个类对象,并作为第二个参数传入thread()线程的构造函数中去。

  • 管理当前线程的函数

    • yield

    此函数的准确性为依赖于实现,特别是使用中的 OS 调度器机制和系统状态。例如,先进先出实时调度器( Linux 的 SCHED_FIFO )将悬挂当前线程并将它放到准备运行的同优先级线程的队列尾(而若无其他线程在同优先级,则 yield 无效果)。

    #include <iostream>
    #include <chrono>
    #include <thread>
    
    // 建议其他线程运行一小段时间的“忙睡眠”
    void little_sleep(std::chrono::microseconds us)
    {
        auto start = std::chrono::high_resolution_clock::now();
        auto end = start + us;
        do {
            std::this_thread::yield();
        } while (std::chrono::high_resolution_clock::now() < end);
    }
    
    int main()
    {
        auto start = std::chrono::high_resolution_clock::now();
    
        little_sleep(std::chrono::microseconds(100));
    
        auto elapsed = std::chrono::high_resolution_clock::now() - start;
        std::cout << "waited for "
                  << std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count()
                  << " microseconds\n";
    }
    
    • get_id

    这个函数不用过多介绍了,就是用来获取当前线程id的,用来标识线程的身份。

     std::thread::id this_id = std::this_thread::get_id();
    
    • sleep_for

    位于this_thread命名空间下,msvc下支持两种时间参数。

    std::this_thread::sleep_for(2s);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    
    • sleep_untile

    参数构建起来挺麻烦的,一般场景下要求线程睡眠的就用sleep_for就行了

    using std::chrono::system_clock;
    time_t tt = system_clock::to_time_t(system_clock::now());
    struct std::tm *ptm = localtime(&tt);
     std::this_thread::sleep_until(system_clock::from_time_t(mktime(ptm)));
    
  • 互斥

    • mutex

    对于互斥量看到一个很好的比喻:

    代码示例:

    mutex mtx;
    
    int gNum = 0;
    void Test1()
    {
        mtx.lock();
        for(int n = 0; n < 5; ++n)
            gNum++;
        mtx.unlock();
    }
    
    void Test2()
    {
        std::cout << "gNum = " << gNum << std::endl;
    }
    
    int main()
    {
        thread t1(Test1);
        t1.join();
        thread t2(Test2);
        t2.join();
        return 0;
    }
    
    

    join()表示主线程等待子线程结束再继续执行,如果我们的期望是打印循环自增之后的gNum的值,那t1.join()就放在t2创建之前调用。因为t2的创建就标志着t2线程创建好然后开始执行了

    通常mutex不单独使用,因为lock和unlock必须配套使用,如果忘记unlock很可能造成死锁,即使unlock写了,但是如果在执行之前程序捕获到异常,也还是一样会死锁。如何解决使用mutex造成的死锁问题呢?下面介绍unique_gard和lock_guard的时候详细说明。

    • timed_mutex

    提供互斥设施,实现有时限锁定

    std::mutex cout_mutex; // 控制到 std::cout 的访问
    std::timed_mutex mutex;
    
    void job(int id)
    {
        using Ms = std::chrono::milliseconds;
        std::ostringstream stream;
    
        for (int i = 0; i < 3; ++i) {
            if (mutex.try_lock_for(Ms(100))) {
                stream << "success ";
                std::this_thread::sleep_for(Ms(100));
                mutex.unlock();
            } else {
                stream << "failed ";
            }
            std::this_thread::sleep_for(Ms(100));
        }
    
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "[" << id << "] " << stream.str() << "\n";
    }
    
    int main()
    {
        std::vector<std::thread> threads;
        for (int i = 0; i < 4; ++i) {
            threads.emplace_back(job, i);
        }
    
        for (auto& i: threads) {
            i.join();
        }
    }
    

    这里的第28行衍生出一个知识点:STL的emplace_back函数。这是c++11新增的容器类的操作函数,如果第二个参数忽略,用法和push_back相似,都是在stl后面追加元素。函数原型:

    template<class... _Valty>
    decltype(auto) emplace_back(_Valty&&... _Val);
    

    是一个变长的模板函数,例子中的代码传递的是一个函数指针jobemplace_back的实现会把job传递给std::thread的构造函数,与push_back需要是std::thread类型作为参数不同,所以emplace_back是直接在容器中构造了要添加的元素,省去了再次把参数拷贝到stl中的过程,效率更高。目前来看还没有什么副作用,所以推荐以后在使用stl的时候使用emplace_back取代push_back.

    使用timed_mutex的时候也无法用unique_lock这样的RAII机制来控制加解锁操作,所以不同的互斥量的使用场景要区分清楚。在对的时候使用对的东西也是码农进阶的一个标志~

    try_lock_until的例子:

    void f()
    {
        auto now=std::chrono::steady_clock::now();
        test_mutex.try_lock_until(now + std::chrono::seconds(10));
        std::cout << "hello world\n";
    }
    
    int main()
    {
        std::lock_guard<std::timed_mutex> l(test_mutex);
        std::thread t(f);
        t.join();
    }
    
    • recursive_mutex

    提供能被同一线程递归锁定的互斥设施

    recursive_mutex 类是同步原语,能用于保护共享数据免受从个多线程同时访问。

    recursive_mutex 提供排他性递归所有权语义:

    1. 调用方线程在从它成功调用 locktry_lock 开始的时期里占有 recursive_mutex 。此时期间,线程可以进行对 locktry_lock 的附加调用。所有权的时期在线程调用 unlock 匹配次数时结束。

    2. 线程占有 recursive_mutex 时,若其他所有线程试图要求 recursive_mutex 的所有权,则它们将阻塞(对于调用 lock )或收到 false 返回值(对于调用 try_lock )。

    3. 可锁定 recursive_mutex 次数的最大值是未指定的,但抵达该数后,对 lock 的调用将抛出 std::system_error 而对 try_lock 的调用将返回 false 。

    recursive_mutex 在仍为某线程占有时被销毁,则程序行为未定义。 recursive_mutex 类满足互斥体 (Mutex) 标准布局类型**(StandardLayoutType) 的所有要求。

    #include <iostream>
    #include <thread>
    #include <string>
    #include <mutex>
    
    class X {
        std::recursive_mutex m;
        std::string shared;
      public:
        void fun1() {
          std::lock_guard<std::recursive_mutex> lk(m);
          shared = "fun1";
          std::cout << "in fun1, shared variable is now " << shared << '\n';
        }
        void fun2() {
          std::lock_guard<std::recursive_mutex> lk(m);
          shared = "fun2";
          std::cout << "in fun2, shared variable is now " << shared << '\n';
          fun1(); // 递归锁在此处变得有用
          std::cout << "back in fun2, shared variable is " << shared << '\n';
        };
    };
    
    int main()
    {
        X x;
        std::thread t1(&X::fun1, &x);
        std::thread t2(&X::fun2, &x);
        t1.join();
        t2.join();
    }
    
    • recursive_timed_mutex

    提供能被同一线程递归锁定的互斥设施,并实现有时限锁定

  • 通用互斥管理

    • lock_guard
    void Test1()
    {
        std::lock_guard<std::mutex> lg(mtx);
        for(int n = 0; n < 5; ++n)
        {
            gNum++;
            std::cout << "gNum = " << gNum << std::endl;
        }
    }
    int main()
    {
        thread t1(Test1);
        thread t2(Test1);
        t1.join();
        t2.join();
        return 0;
    }
    

    lock_guard相当于利用RAII机制(“资源获取就是初始化”)把mutex封装了一下,在构造中lock,在析构中unlock。避免了中间过程出现异常导致的mutex不能够正常unlock.

    • scoped_lock(c++17)
    • unique_lock
    • defer_lock_t
    • try_to_lock_t
    • adopt_lock_t
    • defer_lock
    • try_to_lock
    • adopt_lock
  • 通用锁算法

    • try_lock
    • lock
  • 单次调用

    • once_flag
    • call_once
  • 条件变量

    • condition_variable
    • condition_variable_any
    • notify_all_at_thread_exit
    • cv_status
  • Future

    • promise
    • packaged_task
    • future
    • shared_future
    • async
    • launch
    • future_status
    • Future错误
      • future_error
      • future_category
      • future_errc
09-27 14:37