C++并发编程实战——第四章 同步操作

第四章 同步操作

4.1 条件变量(condition_variable)

  • 使用C++标准库提供的工具去等待事件的发生。通过另一线程触发等待事件的机制是最基本的唤醒方式(例如:流水线上存在额外的任务时),这种机制就称为**“条件变量”。从概念上来说, 条件变量会与多个事件或其他条件相关,并且一个或多个线程会等待条件的达成。当某些线程被终止时,为 了唤醒等待线程(允许等待线程继续执行),终止线程将会向等待着的线程广播**“条件达成”的信息。
  • 条件变量的好处:
    • 线程同步:条件变量通常用于线程之间的同步和协调,以确保它们按照特定的顺序执行。
    • 避免忙等待:条件变量允许线程在等待某个条件成立时进入休眠状态,而不是忙等待,从而节省了 CPU 资源。
      • 如果不使用条件变量,线程可能会通过忙等待(例如使用循环)来等待某个条件的满足。这会占用大量 CPU 资源,降低性能。
  • 在并发编程中,一种常见的需求是,一个线程等待另一个线程完成某个事件后,再继续执行任务。对于这种情况,标准库提供了 std::condition_variable
#include <condition_variable>
#include <mutex>
#include <iostream>
#include <thread>
using namespace std;

class A {
private:
    mutex m;
    condition_variable cv;
    bool isStep1Finished = false;
public:
    void step_1() {
        lock_guard<mutex> lock(m);
        cout << "step1 finished" << endl;
        isStep1Finished = true;
        cv.notify_one();
    }

    void step_2() {
        unique_lock<mutex> lock(m);
        cv.wait(lock, [this] {return isStep1Finished;});
        cout << "step2 finished" << endl;
    }
};

int main() {
    A a;
    thread t_2(&A::step_2, &a);
    thread t_1(&A::step_1, &a);

    t_1.join();
    t_2.join();
}
  • 有多个能唤醒的任务时,notify_one()会随机唤醒一个
#include <condition_variable>
#include <mutex>
#include <iostream>
#include <thread>
using namespace std;

class A {
private:
    mutex m;
    condition_variable cv;
    bool isStep1Finished = false;
public:
    void step_1() {
        lock_guard<mutex> lock(m);
        cout << "step1 finished" << endl;
        isStep1Finished = true;
        cv.notify_one();
    }

    void step_2() {
        unique_lock<mutex> lock(m);
        cv.wait(lock, [this] {return isStep1Finished;});
        cout << "step2 finished" << endl;
        cv.notify_one();
    }

    void step_3() {
        unique_lock<mutex> lock(m);
        cv.wait(lock, [this] {return isStep1Finished;});
        cout << "step3 finished" << endl;
        cv.notify_one();
    }

    void step_4() {
        unique_lock<mutex> lock(m);
        cv.wait(lock, [this] {return isStep1Finished;});
        cout << "step4 finished" << endl;
        cv.notify_one();
    }
};

int main() {
    A a;
    thread t_4(&A::step_4, &a);
    thread t_3(&A::step_3, &a);
    thread t_2(&A::step_2, &a);
    thread t_1(&A::step_1, &a);

    t_1.join();
    t_2.join();
    t_3.join();
    t_4.join();
}
  • 但要注意死锁问题
#include <condition_variable>
#include <mutex>
#include <iostream>
#include <thread>
using namespace std;

class A {
private:
    mutex m;
    condition_variable cv;
    bool isStep1Finished = false;
    bool isStep2Finished = false;
public:
    void step_1() {
        lock_guard<mutex> lock(m);
        cout << "step1 finished" << endl;
        isStep1Finished = true;
        cv.notify_one();
    }

    void step_2() {
        unique_lock<mutex> lock(m);
        cv.wait(lock, [this] {return isStep1Finished;});
        cout << "step2 finished" << endl;
        isStep2Finished = true;
        cv.notify_one();
    }

    void step_3() {
        unique_lock<mutex> lock(m);
        cv.wait(lock, [this] {return isStep2Finished;});
        cout << "step3 finished" << endl;
    }
};

int main() {
    A a;
    thread t_3(&A::step_3, &a);
    thread t_2(&A::step_2, &a);
    thread t_1(&A::step_1, &a);

    t_1.join();
    t_2.join();
    t_3.join();
}
  • 上面代码本意是想step1结束之后唤醒step2,但是由于是随机唤醒一个阻塞的线程,如果此时唤醒的是step3但是唤醒条件不满足继续阻塞,而step2又没有唤醒,导致系统一直等待。
  • 可以使用notify_all()所有阻塞的进程都会进行通知。
#include <condition_variable>
#include <mutex>
#include <iostream>
#include <thread>
using namespace std;

class A {
private:
    mutex m;
    condition_variable cv;
    bool isStep1Finished = false;
    bool isStep2Finished = false;
public:
    void step_1() {
        lock_guard<mutex> lock(m);
        cout << "step1 finished" << endl;
        isStep1Finished = true;
        cv.notify_all();
    }

    void step_2() {
        unique_lock<mutex> lock(m);
        cv.wait(lock, [this] {return isStep1Finished;});
        cout << "step2 finished" << endl;
        isStep2Finished = true;
        cv.notify_all();
    }

    void step_3() {
        unique_lock<mutex> lock(m);
        cv.wait(lock, [this] {return isStep2Finished;});
        cout << "step3 finished" << endl;
    }
};

int main() {
    A a;
    thread t_3(&A::step_3, &a);
    thread t_2(&A::step_2, &a);
    thread t_1(&A::step_1, &a);

    t_1.join();
    t_2.join();
    t_3.join();
}
  • std::condition_variable 只能与 std::unique_lock 协作,为此标准库提供了更通用的 std::condition_variable_any
#include <condition_variable>
#include <mutex>
#include <iostream>
#include <thread>
using namespace std;

#include <condition_variable>
#include <mutex>
#include <iostream>
#include <thread>
using namespace std;

class my_Mutex {
public:
    void lock() {}
    void unlock() {}
};

class A {
private:
    my_Mutex m;
    condition_variable_any cv;
public:
    void step_1() {
        my_Mutex lock(m);
        cout << "step1 finished" << endl;
        cv.notify_one();
    }

    void step_2() {
        my_Mutex lock(m);
        cv.wait(lock);
        cout << "step2 finished" << endl;
        cv.notify_one();
    }

    void step_3() {
        my_Mutex lock(m);
        cv.wait(lock);
        cout << "step3 finished" << endl;
        cv.notify_one();
    }
};

int main() {
    A a;
    thread t_3(&A::step_3, &a);
    thread t_2(&A::step_2, &a);
    thread t_1(&A::step_1, &a);

    t_1.join();
    t_2.join();
    t_3.join();
}

4.2 信号量(semaphore)

  • 信号量类是标准库提供的同步原语,用于控制多个线程之间的资源访问
4.2.1 std::counting_semaphore
  • std::counting_semaphore表示计数信号量,可以用于限制多个线程同时访问共享资源的数量。它的构造函数接受一个计数值,表示初始的可用资源数量。
  • acquire()方法用于获取信号量,如果没有可用资源,则线程会阻塞。获取资源后,计数减少。
  • release()方法用于释放信号量,增加计数,唤醒等待的线程。
#include <iostream>
#include <semaphore>
#include <thread>
#include <mutex>
using namespace std;

class A {
private:
    counting_semaphore<2> sem{ 0 };
    mutex m;
public:
    void take(int x) {
        sem.acquire();

        //互斥访问 打印机
        lock_guard<mutex> lock(m);
        cout << "take " << x << endl;
    }
    void release() {
        sem.release(2);

        lock_guard<mutex> lock(m);
        cout << "release " << endl;
    }
};

int main() {
    A a;
    thread t_1(&A::take, &a, 10);
    thread t_2(&A::take, &a, 20);
    thread t_3(&A::release, &a);

    t_3.join();
    t_2.join();
    t_1.join();
}

输出结果:

release
take 10
take 20
4.2.2 std::binary_semaphore
  • std::binary_semaphore表示二进制信号量,只有两个状态:可用(1)和不可用(0)。它通常用于互斥访问共享资源,类似于互斥锁。
  • acquire()方法用于获取信号量,如果信号量不可用,则线程会阻塞。获取后信号量变为不可用。
  • release()方法用于释放信号量,将其置为可用状态。
#include <iostream>
#include <semaphore>
#include <thread>
#include <mutex>
using namespace std;

binary_semaphore sem{ 1 };

void printer(string s) {
    cout << s << endl;
}

void fun_1() {
    sem.acquire();
    printer("fun_1");
    sem.release();
}

void fun_2() {
    sem.acquire();
    printer("fun_2");
    sem.release();
}

void fun_3() {
    sem.acquire();
    printer("fun_3");
    sem.release();
}

int main() {
    thread t_1(fun_1);
    thread t_2(fun_2);
    thread t_3(fun_3);

    t_3.join();
    t_2.join();
    t_1.join();
}

4.3 屏障(barrier)

  • C++20 提供了 std::barrier,它用一个值作为要等待的线程的数量来构造,调用 std::barrier::arrive_and_wait 会阻塞至所有线程完成任务(因此称为屏障),当最后一个线程完成任务时,所有线程被释放,barrier 重置。构造std::barrier时可以额外设置一个 noexcept 函数,当所有线程到达阻塞点时,由其中一个线程运行该函数。arrive_and_drop()函数用于将当前线程从屏障中删除,而不是等待其他线程到达。

    #include <iostream>
    #include <semaphore>
    #include <thread>
    #include <mutex>
    #include <barrier>
    using namespace std;
    
    std::mutex mtx;
    std::barrier bar(9);
    
    void thread_func(int x) {
        std::unique_lock<std::mutex> lock(mtx);
        // 对共享资源进行操作
        // ...
        cout << x << " ready" << endl;
        lock.unlock();
        bar.arrive_and_wait();  // 等待其他线程
        lock.lock();
        cout << "run " << x << endl;
    }
    
    int main() {
        std::thread t1(thread_func,10);
        std::thread t2(thread_func,20);
        std::thread t3(thread_func, 30);
        std::thread t4(thread_func, 40);
        std::thread t5(thread_func, 50);
        std::thread t6(thread_func, 60);
        std::thread t7(thread_func, 70);
        std::thread t8(thread_func, 80);
        std::thread t9(thread_func, 90);
        t1.join();
        t2.join();
        t3.join();
        t4.join();
        t5.join();
        t6.join();
        t7.join();
        t8.join();
        t9.join();
        return 0;
    }
    

    输出结果

    10 ready
    90 ready
    30 ready
    40 ready
    50 ready
    60 ready
    70 ready
    80 ready
    20 ready
    run 80
    run 70
    run 60
    run 50
    run 20
    run 30
    run 90
    run 10
    run 40
    

4.4 期值(future)与异步工具

异步工具线程同步是多线程编程中的两个不同概念,但它们通常在多线程应用程序中一起使用以实现特定的功能和需求。

  1. 异步工具
    • 异步工具是用于在程序中执行异步操作的工具,通常包括异步任务、异步函数、异步事件等。
    • 异步工具的目的是允许程序在执行某些操作时不阻塞主线程,从而提高程序的响应性和性能。
    • 异步工具的典型示例包括 std::asyncstd::futurestd::promisestd::packaged_task、异步事件处理等。
  2. 线程同步
    • 线程同步是一种机制,用于协调多个线程之间的执行,以确保它们按照预期的顺序或协作来执行任务。
    • 线程同步的目的是防止竞态条件、避免数据竞争和确保多线程程序的正确性。
    • 线程同步的典型示例包括互斥锁(std::mutex)、条件变量(std::condition_variable)、信号量、屏障、读写锁等。

关系和使用场景:

  • 异步工具通常用于执行耗时的任务或操作,**以便在后台线程中执行,而不阻塞主线程。**这可以提高程序的性能和响应性。
  • **线程同步机制用于协调多个线程之间的操作,以确保数据的一致性和正确性。**在使用异步工具时,可能需要线程同步来确保数据的安全访问,例如使用互斥锁来保护共享数据。
  • 异步工具和线程同步可以一起使用,以实现复杂的多线程应用程序。例如,异步任务可能需要在完成后通知主线程,这时可以使用线程同步机制(如条件变量)来实现通知和等待。
  • 使用正确的线程同步机制是确保多线程程序正确运行的关键,因为在没有适当的同步措施的情况下,可能会导致竞态条件和数据竞争,从而引发难以诊断和修复的问题。
4.4.1 future与async
  • std::thread 只能运行函数,无法获取函数的返回值,为此标准库提供了std::future来关联线程运行的函数和函数的返回结果,这种获取结果的方式是异步的。通过std::async()创建异步任务的 std::futurestd::async 的创建任务的传参方式和 std::thread 一样
#include <iostream>
#include <future>
#include <thread>
using namespace std;

int add(int x, int y) {
    return x + y;
}

int main() {
    future<int> future = async(add, 3, 5);

    cout << future.get() << endl;
}
  • future只能get一次
#include <iostream>
#include <future>
#include <thread>
using namespace std;

int add(int x, int y) {
    return x + y;
}

int main() {
    future<int> future = async(add, 3, 5);

    cout << future.get() << endl;

    try {
        future.get();
    }
    catch (const future_error& e) {
        cout << e.what() << endl;
    }
}
  • 也可以在函数 调用之前向std::async传递一个额外参数,这个参数的类型是std::launch,还可以是std::launch::defered, 表明函数调用延迟到wait()get()函数调用时才执行,std::launch::async表明函数必须在其所在的独立线程上 执行, std::launch::deferred | std::launch::async 表明实现可以选择这两种方式的一种。
auto f6=std::async(std::launch::async,Y(),1.2); // 在新线程上执行

auto f7=std::async(std::launch::deferred,baz,std::ref(x)); // 在wait()或get()调用时执行

auto f8=std::async(std::launch::deferred | std::launch::async,baz,std::ref(x)); // 实现选择执行方式

auto f9=std::async(baz,std::ref(x));

f7.wait(); // 调用延迟函数,在这个函数之后可以通过get()获取值
4.4.2 packaged_task
  • packaged_task``std::packaged_task 是 C++ 标准库提供的一个用于将函数包装成可异步执行的任务的工具。它通常用于将函数的执行结果与 std::future 结合使用,以便在一个线程中执行任务,并在另一个线程中获取任务的结果。

  • 下面是关于 std::packaged_task 的详细说明:

    • 创建 std::packaged_task 对象

      std::packaged_task<ReturnType(Args...)> task(function_to_wrap);
      
      • 这里 ReturnType 是函数的返回类型,Args... 是函数的参数类型列表,function_to_wrap 是要包装的函数。
    • 将任务与 std::future 关联

      std::future<ReturnType> future = task.get_future();
      
      • 使用 task.get_future() 可以获取一个与 std::packaged_task 关联的 std::future 对象,该 std::future 对象可以用于等待任务的完成并获取任务的返回值。
    • 执行任务

      task(args...); // 使用括号运算符执行任务
      
      • 您可以使用括号运算符 () 来执行任务,并将参数传递给包装的函数。执行任务后,函数将在调用线程中执行。
    • 等待任务完成并获取结果

      cppCopy code
      ReturnType result = future.get();
      
      • 使用 future.get() 可以等待任务完成并获取任务的结果。这将导致调用线程阻塞,直到任务完成为止。
#include <iostream>
#include <future>
#include <thread>
using namespace std;

int main() {
    auto f = [](int x, int y) { return x + y; };
    packaged_task<int(int, int)> task(f);
    future<int> future = task.get_future();

    thread t(move(task), 1, 3);
    t.join();
    cout << future.get() << endl;
}
  • 这里一定要使用右值传递,因为packaged_task中的拷贝函数被删除了
packaged_task(const packaged_task&) = delete;
packaged_task& operator=(const packaged_task&) = delete;
4.4.3 promises
  • std::promise 是 C++ 标准库中用于在一个线程中产生值,然后在另一个线程中获取值的一种机制。它通常与 std::future 结合使用,std::promise 用于设置值,而 std::future 用于获取值。
  • std::promise 只能关联一个 std::future
  • 下面是关于如何使用 std::promise 的示例:
#include <future>
#include <thread>
#include <iostream>
using namespace std;

void produceValue(promise<int>& p) {
    this_thread::sleep_for(std::chrono::seconds(3));

    p.set_value(3);
}

int main() {
    promise<int> p;
    future<int> future = p.get_future();

    thread t(produceValue, ref(p));

    cout << future.get() << endl;
    t.join();
}

std::promisestd::packaged_task 都是 C++ 标准库中用于异步任务的工具,但它们有不同的用途和特点:

  1. std::promise
    • 主要用于单向传递值,即从生产者线程传递值给消费者线程。
    • 通常与 std::future 结合使用,std::promise 用于设置值,std::future 用于获取值。
    • 可以设置一个值,但不能包装可调用的函数
    • 适用于生产者-消费者模式,其中一个线程生成数据,另一个线程获取数据
    • std::promise 不包含任务,只是用于传递值
  2. std::packaged_task
    • 用于包装可调用的函数(函数指针、函数对象、Lambda 表达式等),将其封装成可以异步执行的任务
    • 通常与 std::future 结合使用,std::packaged_task 包装的函数会在后台线程中执行,结果可以通过与之关联的 std::future 获取。
    • 允许封装并异步执行带有返回值的函数。
    • 适用于需要异步执行可调用函数的情况,例如任务池。

总结来说,std::promise 主要用于值的传递,而 std::packaged_task 主要用于包装可调用的函数以实现异步执行。选择使用哪个取决于您的需求,如果只需要传递值,可以使用 std::promisestd::future。如果需要异步执行可调用的函数并获取其结果,则可以使用 std::packaged_taskstd::future

  • 但是promise也能用于进程同步
#include <future>
#include <thread>
#include <iostream>
using namespace std;

class A {
private:
    promise<void> p;
public:
    void signal() {
        this_thread::sleep_for(std::chrono::seconds(3));
        cout << "signal finished" << endl;
        p.set_value();
    }

    void wait() {
        future<void> f = p.get_future();
        f.wait();
        cout << "wait finished" << endl;
    }
};

int main() {
    A a;
    thread t1(&A::signal, &a);
    thread t2(&A::wait, &a);

    t1.join();
    t2.join();
}
  • 不同于 std::condition_variable 的是,std::promise 只能通知一次,因此通常用来创建暂停状态的线程
#include <future>
#include <thread>
#include <iostream>
using namespace std;

class A {
private:
    promise<void> p;
public:
    void signal() {
        p.set_value();
    }

    void wait() {
        p.get_future().wait();
        cout << "wait finished" << endl;
    }
};


int main() {
    A a;
    thread t(&A::wait, &a);
    a.signal();
    t.join();
}
4.4.4 future存储异常
  • future能够存储异常
#include <future>
#include <thread>
#include <iostream>
#include <exception>
using namespace std;

void fun() {
    throw logic_error("error");
}

int main() {
    future<void> future = async(fun);
    try {
        future.get();
    }
    catch (const exception& e) {
        cout << e.what() << endl;
    }
}
  • promise需要手动存储异常,set_exception
  • 注意set_value()时的异常不会被设置到 future 中
#include <future>
#include <thread>
#include <iostream>
#include <exception>
using namespace std;

class A {
private:
    promise<void> p;
public:
    void set_error() {
        try {
            throw logic_error("error");
        }
        catch (...) {
            p.set_exception(current_exception());
        }
    }

    future<void> get_promise() {
        return p.get_future();
    }
};

int main() {
    A a;
    future<void> f = a.get_promise();
    a.set_error();
    try {
        f.get();
    }
    catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }

}
  • 如果std::packaged_taskstd::promise直到析构都未设置值,std::future::get() 会抛异常
#include <future>
#include <thread>
#include <iostream>
#include <exception>
using namespace std;

int main() {
    future<void> f1;
    future<void> f2;

    {
        packaged_task<void()> task([] {});
        f1 = task.get_future();

        promise<void> p;
        f2 = p.get_future();
    }

    try {
        f1.get();
    }
    catch (exception& e) {
        cout << e.what() << endl;
    }
    cout << "----------------------------------------------------------------" << endl;
    try {
        f2.get();
    }
    catch (exception& e) {
        cout << e.what() << endl;
    }
}
4.4.5 shared_future
  • std::shared_future 可以多次获取结果,它可以通过std::future的右值构造。每一个std::shared_future对象上返回的结果不同步,多线程访问std::shared_future需要加锁防止 race condition,更好的方法是给每个线程拷贝一个 std::shared_future 对象,这样就可以安全访问而无需加锁
#include <iostream>
#include <future>
#include <thread>
#include <vector>

void fun(std::shared_future<int> shared_future) {
    std::cout << shared_future.get() << std::endl;
}



int main() {
    // 创建一个promise对象
    std::promise<int> promise;
    // 获取一个与promise关联的future对象
    std::future<int> future = promise.get_future();

    auto f = [&promise]() {
        promise.set_value(10);
        };

    // 将future对象转换为shared_future对象
    std::shared_future<int> shared_future = future.share();

    std::thread t_1(fun, shared_future);
    std::thread t_2(fun, shared_future);
    std::thread t_3(f);

    t_1.join();
    t_2.join();
    t_3.join();
}
  • 可以直接用std::future::share()生成 std::shared_future
#include <future>

int main() {
    std::promise<void> ps;
    auto sf = ps.get_future().share();
    ps.set_value();
    sf.get();
    sf.get();
}

4.5 时钟

  • 对于标准库来说,时钟是提供了四种信息的类
    • 当前时间,如 std::chrono::system_clock::now()
    • 表示时间值的类型,如 std::chrono::time_point
    • 时钟节拍(一个 tick 的周期),一般一秒有 25 个 tick,一个周期则为std::ratio<1, 25>
    • 通过时钟节拍确定时钟是否稳定(steady,匀速),如 std::chrono::steady_clock::is_steady()(稳定时钟,代表系统时钟的真实时间)、std::chrono::system_clock::is_steady()(一般因为时钟可调节而不稳定,即使这是为了考虑本地时钟偏差的自动调节)、high_resolution_clock::is_steady()(最小节拍最高精度的时钟)
4.5.1 时间段(duration)
  • std::chrono::duration<> 函数模板能够对时间段进行处理(线程库使用到的所有C++时间处理工具,都在std::chrono命名空间内)。第一个模板参数是一个类型表示(比如,int,long或double),第二个模板参数是定制部分,表示每一个单元所用秒数。例如,当几分钟的时间要存在short类型中时,可以写成std::chrono::duration<short, std::ratio<60, 1>>,因为60秒是才是1分钟,所以第二个参数写成 std::ratio<60, 1> 。当需要将毫秒级计数存在double类型中时,可以写成 std::chrono::duration<double,std::ratio<1, 1000>> ,因为1秒等于1000毫秒。
  • std::chrono::duration<Rep, Period> duration;
    • Rep:表示时间段的存储类型(通常是整数类型,例如intlong等)。
    • Period:表示时间段的单位(例如std::chrono::secondsstd::chrono::milliseconds等)。
#include <iostream>
#include <chrono>

int main() {
    using namespace std::chrono;
    
    // 表示2秒的时间段
    seconds sec(2);
    std::cout << "2 seconds = " << sec.count() << " seconds." << std::endl;
    
    // 表示500毫秒的时间段
    milliseconds msec(500);
    std::cout << "500 milliseconds = " << msec.count() << " milliseconds." << std::endl;
    
    return 0;
}
namespace std {
    namespace chrono {
        using nanoseconds = duration<long long, nano>;
        using microseconds = duration<long long, micro>;
        using milliseconds = duration<long long, milli>;
        using seconds = duration<long long>;
        using minutes = duration<int, ratio<60>>;
        using hours = duration<int, ratio<3600>>;
        // C++20
        using days = duration<int, ratio_multiply<ratio<24>, hours::period>>;
        using weeks = duration<int, ratio_multiply<ratio<7>, days::period>>;
        using years = duration<int, ratio_multiply<ratio<146097, 400>, days::period>>;
        using months = duration<int, ratio_divide<years::period, ratio<12>>>;
    }  // namespace chrono
}  // namespace std
#include <iostream>
#include <chrono>

int main() {
    auto week = std::chrono::days(7);
    auto seven_weeks = 7 * week;
    std::cout << "seven_weeks = " << seven_weeks.count() << " days" << std::endl;
}
  • C++14 在std::literals::chrono_literals中提供了表示时间的后缀
#include <iostream>
#include <chrono>
#include <iomanip>
using namespace std::literals::chrono_literals;

int main() {
    auto t1 = 24h;
    std::cout << "t1 = " << t1.count() << "hours" << std::endl;
    auto t2 = 45min;
    std::cout << "t2 = " << t2.count() << "min" << std::endl;
    auto t3 = 10s;
    std::cout << "t3 = " << t3.count() << "sec" << std::endl;

}
  • 时间段的转换,但是转换会截断。
#include <iostream>
#include <chrono>
#include <iomanip>
using namespace std::literals::chrono_literals;

int main() {
    auto t1 = 2.444min;

    auto t2 = std::chrono::duration_cast<std::chrono::seconds>(t1);
    auto t3 = std::chrono::duration_cast<std::chrono::minutes>(t1);
    std::cout << "t1 = " << t1.count() << "min" << std::endl;
    std::cout << "t1 = " << t2.count() << "seconds" << std::endl;
    std::cout << "t1 = " << t3.count() << "min" << std::endl;
}

输出结果:

t1 = 2.444min
t1 = 146seconds
t1 = 2min
  • duration 支持四则运算
#include <iostream>
#include <chrono>
#include <iomanip>
using namespace std::literals::chrono_literals;

int main() {

    if (1h - 30min == 30min) {
        std::cout << "1h - 30min = 30min" << std::endl;
    }
    if ((3min - 30s).count() == 150) {
        std::cout << "3min - 30s = 150s" << std::endl;
    }
}
  • 使用 duration 设置等待时间
    • 超时时会返回std::future_status::timeout。当future状态改变,则会返回std::future_status::ready
#include <chrono>
#include <future>
#include <iostream>
#include <thread>

int f() {
    std::this_thread::sleep_for(std::chrono::seconds(5));
    return 1;
}

int main() {
    auto res = std::async(f);
    if (res.wait_for(std::chrono::seconds(3)) == std::future_status::ready) {
        std::cout << res.get();
    }
    else {
        std::cout << "超时" << std::endl;
    }
}
4.5.2 时间点(time_point)
  • 时间点可用std::chrono::time_point<>来表示,第一个参数用来指定使用的时钟,第二个函数参数用来表示时间单位(特化的 std::chrono::duration<> )。

  • 时间点就是时间戳,而时间戳是时钟的基本属性,不可以直接查询,其在C++标准中已经指定。通常,UNIX时间戳表示1970年1月1日 00:00。

  • 时钟可能共享一个时间戳,或具有独立的时间戳。当两个时钟共享一个时间戳时,其中一个time_point类型可以与另一个时钟类型中的 time_point相关联。虽然不知道UNIX时间戳的具体值,但可以通过对指定time_point类型使用 time_since_epoch()来获取时间戳,该成员函数会返回一个数值,这个数值是指定时间点与UNIX时间戳的时间间隔。

  • std::chrono::time_point 是表示时间的类型,值为从某个时间点开始计时的时间长度

// 第一个模板参数为开始时间点的时钟类型,第二个为时间单位
std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>
  • std::chrono::time_point 可以与duration加减,也可以与自身相减
#include <chrono>
#include <future>
#include <iostream>
#include <thread>

int main() {
    std::chrono::time_point a = std::chrono::system_clock::now();
    std::chrono::time_point b = a + std::chrono::hours(1);
    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;

    std::cout << (b - a).count() << std::endl;
    std::cout << std::chrono::duration_cast<std::chrono::seconds>(b - a).count() << std::endl;
}

输出结果:

a = 2023-09-30 08:19:54.909108400
b = 2023-09-30 09:19:54.909108400
3600000000000
3600
int fib(int n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    return fib(n - 1) + fib(n - 2);
}

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    fib(20);
    auto stop = std::chrono::high_resolution_clock::now();
    std::cout << "fib(20) took "
        << std::chrono::duration_cast<std::chrono::microseconds>(stop - start).count()
        << " miroseconds" << std::endl;
}

输出结果:

fib(20) took 54 miroseconds
  • 条件变量中使用时间点超时
#include <condition_variable>
#include <mutex>
#include <chrono>

std::condition_variable cv;
bool done;
std::mutex m;
bool wait_loop() {
    auto const timeout = std::chrono::steady_clock::now() +
        std::chrono::milliseconds(500);
    std::unique_lock<std::mutex> lk(m);
    while (!done) {
        if (cv.wait_until(lk, timeout) == std::cv_status::timeout)
            break;
    }
    return done;
}
4.5.3 使用超时
  • 如下函数支持设置超时时间,函数最多阻塞至时间到期
std::this_thread::sleep_for
std::this_thread::sleep_until
    
std::condition_variable::wait_for
std::condition_variable::wait_until
    
std::condition_variable_any::wait_for
std::condition_variable_any::wait_until
    
std::timed_mutex::try_lock_for
std::timed_mutex::try_lock_until
    
std::recursive_timed_mutex::try_lock_for
std::recursive_timed_mutex::try_lock_until
    
std::unique_lock::try_lock_for
std::unique_lock::try_lock_until
    
std::future::wait_for
std::future::wait_until
    
std::shared_future::wait_for
std::shared_future::wait_until
    
std::counting_semaphore::try_acquire_for
std::counting_semaphore::try_acquire_until

4.6 简化代码

4.6.1 使用future的函数化编程
  • 函数化编程(functional programming)是一种编程方式,函数结果只依赖于传入函数的参数。使用相同的参数调用函数,不管多少次都会获得相同的结果。C++标准库中与数学相关的函数都有这个特性,例如:sin,cos 和sqrt。基本类型间的简单运算,例如:3+3,6*9,或1.3/4.7。纯粹的函数不会改变任何外部状态,并且这种特性限制了函数的返回值。

  • 不修改共享数据,就不存在条件竞争,并且没有必要使用互斥量保护共享数据。这是对编程极大的简化。

  • 快速排序

    • 串行版:
    #include <iostream>
    #include <algorithm>
    #include <list>
    #include <thread>
    #include <future>
    #include <chrono>
    using namespace std;
    
    template<typename T>
    list<T> quick_sort(list<T> v) {
        if (v.empty()) return v;
        list<T> result;
        result.splice(result.begin(), v, v.begin());
        auto pivot_it = partition(v.begin(), v.end(), [&](const T& x) { return x < result.front();});
        list<T> lower;
        lower.splice(lower.begin(), v, v.begin(), pivot_it);
        auto lower_part(quick_sort(move(lower)));
        auto higher_part(quick_sort(move(v)));
        result.splice(result.begin(), lower_part);
        result.splice(result.end(), higher_part);
    
        return result;
    }
    
    int main() {
        list<int> l{ 1,3,2,4,6,5,99,87,44,25,9 };
        l = quick_sort_1(move(l));
    }
    
    • 并行版
    #include <iostream>
    #include <algorithm>
    #include <list>
    #include <thread>
    #include <future>
    #include <chrono>
    using namespace std;
    
    template <typename T>
    std::list<T> quick_sort(std::list<T> v) {
        if (v.empty()) {
            return v;
        }
        std::list<T> res;
        res.splice(res.begin(), v, v.begin());
        auto it = std::partition(v.begin(), v.end(),
            [&](const T& x) { return x < res.front(); });
        std::list<T> low;
        low.splice(low.end(), v, v.begin(), it);
        // 用另一个线程对左半部分排序
        std::future<std::list<T>> l(std::async(&quick_sort<T>, std::move(low)));
        auto r(quick_sort(std::move(v)));
        res.splice(res.end(), r);
        res.splice(res.begin(), l.get());
        return res;
    }
    
    int main() {
        list<int> l{ 1,3,2,4,6,5,99,87,44,25,9 };
        l = quick_sort_1(move(l));
    }
    
  • 这样的递归创建线程,当递归次数过多时,会造成线程数量超过硬件最大线程数。

  • 另外可以写一个spawn_task()函数对 std::packaged_taskstd::thread 做一下包装。其本身并没有太多优势(事实上会造成大规模的超额任务),但可为转型成一个更复杂的实现进行铺垫,实现会向队列添加任务,而后使用线程池的方式来运行。

template<typename F, typename A>
std::future<std::result_of<F(A&&)>::type>
spawn_task(F&& f, A&& a) {
    typedef std::result_of<F(A&&)>::type result_type;
    std::packaged_task<result_type(A&&)>
        task(std::move(f)));
        std::future<result_type> res(task.get_future());
        std::thread t(std::move(task), std::move(a));
        t.detach();
        return res;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值