c++11新特性实战 (一):多线程操作

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

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

单位上有一台打印机(共享数据a),你要用打印机(线程1要操作数据a),同事老王也要用打印机(线程2也要操作数据a),但是打印机同一时间只能给一个人用,此时,规定不管是谁,在用打印机之前都要向领导申请许可证(lock),用完后再向领导归还许可证(unlock),许可证总共只有一个,没有许可证的人就等着在用打印机的同事用完后才能申请许可证(阻塞,线程1lock互斥量后其他线程就无法lock,只能等线程1unlock后,其他线程才能lock),那么,这个许可证就是互斥量。互斥量保证了使用打印机这一过程不被打断。

代码示例:

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();
}

这里的 std::recursive_mutex m;如果修改成std::mutex m;程序运行的时候会直接崩溃,原因是两个std::lock_guard使用的是同一个std::mutex,造成func1()加锁的时候死锁 。

std::recursive_mutexstd::mutex最显著的区别是,前者对同一个互斥量重复加锁,释放的时候是递归释放,所以在此场景中不会产生std::mutex一样的死锁问题。

recursive_timed_mutex

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

使用方法和std::recursive_mutex类似,不做详细介绍~

shared_mutex(C++17)

shared_timed_mutex(C++14)

这两个属于更高标准的std包含的内容,感兴趣的读者自行了解~

通用互斥管理

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.自己在写代码的过程中也可以通过RAII机制封装一个简单的堆内存管理的类:

template<typename Type>
class WRaii
{
public:
    WRaii(const Type& value){
        m_Type = new Type(value);
    }
    ~WRaii(){
        if(m_Type)
            delete m_Type;
    }
    Type* operator ->() const
    {
        return m_Type;
    }
private:
    Type*   m_Type  = nullptr;
};

class Test
{
public:
    Test(int num) { m_Num = num; }
    void Do() { std::cout << __FUNCTION__ << std::endl;}
    int Num() { return m_Num; }
private:
    int m_Num = 5;
};

int main()
{
    WRaii<int> ra(5);//调用int的构造
    WRaii<Test> ra1(6);//调用Test的构造
    int res = ra1->Num();
}

其实这个例子不太合适,这样写好之后就相当于一个栈内存的变量了,不如直接int a(5)这样写,哈哈,其实想要实现的是如果有需要成对呈现的操作,且有逻辑上的先后关系的话就可以把成对的操作放到上面模板类的构造和析构里面,这样就能够确保成对的操作成对出现了

scoped_lock(c++17)

unique_lock

unique_lock和lock_guard不同的是:unique_lock的成员函数lockunlock允许使用者更灵活的加锁和解锁。

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock

std::mutex mtx;           // mutex for critical section

void print_block (int n, char c) {
  // critical section (exclusive access to std::cout signaled by lifetime of lck):
  std::unique_lock<std::mutex> lck (mtx, std::defer_lock);
  bool res = lck.try_lock();
  res = mtx.try_lock();
  for (int i=0; i<n; ++i) { std::cout << c; }
  std::cout << '\n';
}

int main ()
{
  std::thread th1 (print_block,50,'*');
  std::thread th2 (print_block,50,'$');

  th1.join();
  th2.join();

  return 0;
}

c++11有三种加锁策略:

策略 tag type 描述
(默认) 请求锁,阻塞当前线程直到成功获得锁。
std::defer_lock std::defer_lock_t 不请求锁。
std::try_to_lock std::try_to_lock_t 尝试请求锁,但不阻塞线程,锁不可用时也会立即返回。
std::adopt_lock std::adopt_lock_t 假定当前线程已经获得互斥对象的所有权,所以不再请求锁。

上面的例子中用到了std::defer_lock策略,第十行res为true,是一行res为false,不能对同一mutex重复加锁。这里如果加锁策略是默认值,运行到第十行的时候程序会直接崩溃,原因我还不太清楚~,总之try_lock和默认的锁策略是冲突的,不能一起使用。

std::mutex mt;
std::unique_lock<std::mutex> lck(mt, std::defer_lock);
assert(lck.owns_lock() == false);
lck.lock();
assert(lck.owns_lock() == true);

演示第一种加锁策略,其他策略请读者自行尝试~

defer_lock_t

try_to_lock_t

adopt_lock_t

defer_lock

try_to_lock

adopt_lock

上面6个代表了三种加锁策略的类型。

通用锁算法

try_lock

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::try_lock
#include <chrono>

std::mutex foo,bar;

void task_a () {
    foo.lock();
    std::cout << "task a\n";
    bar.lock();
    // ...
//    std::this_thread::sleep_for(std::chrono::microseconds(1000)); //1
    foo.unlock();
//    std::this_thread::sleep_for(std::chrono::microseconds(1000)); //2
    bar.unlock();
}

void task_b () {
    int x = try_lock(foo, bar);
    std::cout << "x = " << x << std::endl;
    if (x==-1) {
        std::cout << "task b\n";
        // ...
        bar.unlock();
        foo.unlock();
    }
    else {
        std::cout << "[task b failed: mutex " << (x?"bar":"foo") << " locked]\n";
    }
}

int main ()
{
    std::thread th1 (task_a);
    std::thread th2 (task_b);
    th1.join();
    th2.join();
    return 0;
}

std::try_lock全局函数

尝试使用其try_lock成员函数锁定所有作为参数传递的对象(非阻塞)。

该函数为每个参数(第一个a,然后b,最后是cde中的其他参数,以相同的顺序)调用try_lock成员函数,直到所有调用成功,或者其中一个调用失败(通过返回 错误或引发异常)。

如果函数由于调用失败而结束,则对try_lock调用成功的所有对象都将解锁,并且该函数将返回锁定失败的对象的参数顺序号。 对参数列表中的其余对象不执行进一步的调用。

13和15行都注释掉的话, x = -1 表示foo和bar都加锁成功

13去掉注释, x = 0, 表示参数列表里第一个加锁失败的是foo

15行去掉注释, x = 1,表示参数列表里第一个加锁失败的是bar

lock

// std::lock example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock

std::mutex foo,bar;

void task_a () {
  // foo.lock(); bar.lock(); // replaced by:
  std::lock (foo,bar);
  std::cout << "task a\n";
  foo.unlock();
  bar.unlock();
}

void task_b () {
  // bar.lock(); foo.lock(); // replaced by:
  std::lock (bar,foo);
  std::cout << "task b\n";
  bar.unlock();
  foo.unlock();
}

int main ()
{
  std::thread th1 (task_a);
  std::thread th2 (task_b);

  th1.join();
  th2.join();

  return 0;
}

锁定作为参数传递的所有对象,并在必要时阻止调用线程。

该函数使用对对象的成员lock,try_lock和unlock的未指定调用序列来锁定对象,以确保所有参数在返回时都被锁定(不产生任何死锁)。

如果该函数无法锁定所有对象(例如,由于内部调用之一引发了异常),则该函数会在失败之前首先解锁成功锁定的所有对象(如果有)。

注:有点像数据库事务的逻辑,要成功都成功,有一个失败就rollback。

单次调用

once_flag

call_once

#include <iostream>       // std::cout
#include <thread>         // std::thread, std::this_thread::sleep_for
#include <chrono>         // std::chrono::milliseconds
#include <mutex>          // std::call_once, std::once_flag

int winner;
void set_winner (int x) { winner = x; }
std::once_flag winner_flag;

void wait_1000ms (int id) {
  // count to 1000, waiting 1ms between increments:
  for (int i=0; i<1000; ++i)
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  // claim to be the winner (only the first such call is executed):
  std::call_once (winner_flag,set_winner,id);
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(wait_1000ms,i+1);

  std::cout << "waiting for the first among 10 threads to count 1000 ms...\n";

  for (auto& th : threads) th.join();
  std::cout << "winner thread: " << winner << '\n';

  return 0;
}

调用传递参数args的fn,除非另一个线程已经执行(或正在执行)具有相同标志的对call_once的调用。

如果另一个线程已经在主动执行带有相同标志的对call_once的调用,则将导致被动执行:被动执行不调用fn但不会返回,直到主动执行本身返回并且此时所有可见副作用都已同步在所有使用相同标志的并发调用中。

如果对call_once的主动调用通过抛出异常(传播到其调用线程)而结束,并且存在被动执行,则在这些被动执行中选择一个,并称为新的主动调用。

请注意,一旦返回了主动执行,所有当前的被动执行和将来对call_once的调用(具有相同标志)也将返回而不会变为主动执行。

活动执行使用fn和args的左值或右值引用的衰变副本,而忽略fn返回的值

条件变量

condition_variable

// condition_variable example
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id (int id) {
  //(id != 1)
     //std::this_thread::sleep_for(MS(10));
  std::unique_lock<std::mutex> lck(mtx);
  while (!ready) cv.wait(lck);//RAII机制阻塞线程,ready的判断是为了防止如果有的线程还没有跑到这一步主线程就调用了go()函数,则会造成部分线程未执行wait,也就不会接收到notify_all()通知
  // ...
  std::cout << "thread " << id << '\n';
}

void go() {
  std::unique_lock<std::mutex> lck(mtx);
  ready = true;
  cv.notify_all();
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_id,i);

  std::cout << "10 threads ready to race...\n";
  go();                       // go!

  for (auto& th : threads) th.join();

  return 0;
}

代码分析:代码实现的是创建10个线程,然后让线程都处于等待的状态,然后通过对同一个mutex操作的notify_all函数同时唤醒10个线程,打印顺序就是10个线程的名次。

这里需要简单说一下std::condition_variable::wait()函数,在wait()的时候会释放锁,在被notify_one或者notify_all唤醒之后重新上锁。wait()的一个重载形式是:

template<class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred);

_Pred是一个返回bool的表达式,当返回值为true,wait()接收到notify信号就解除阻塞;当返回值为false,wait()接收到notify信号依然阻塞。

所以通过我们对wait()的了解就可以清楚的分析出程序的运行流程,目前程序运行的结果是:

1 2 5 8 9 0 3 4 6 7

如果在15行的循环里加上打印语句会发现所有的子线程都wait()成功了,然后释放了锁,go()函数获取到锁就可以走接下来的流程,通知所有的线程竞争执行。

如果加上12、13行,先执行go()了 ready = true之后就只有id=1的线程会阻塞了,所以例子中的写法还是稍微有点问题的,感兴趣的朋友可以自己去拓展~

    #include <iostream>
    #include <string>
    #include <thread>
    #include <mutex>
    #include <condition_variable>
    
    std::mutex m;
    std::condition_variable cv;
    std::string data;
    bool ready = false;
    bool processed = false;
    
    void worker_thread()
    {
        // 等待直至 main() 发送数据
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return ready;});
    
        // 等待后,我们占有锁。
        std::cout << "Worker thread is processing data\n";
        data += " after processing";
    
        // 发送数据回 main()
        processed = true;
        std::cout << "Worker thread signals data processing completed\n";
    
        // 通知前完成手动解锁,以避免等待线程才被唤醒就阻塞(细节见 notify_one )
        lk.unlock();
        cv.notify_one();
    }
    
    int main()
    {
        std::thread worker(worker_thread);
    
        data = "Example data";
        // 发送数据到 worker 线程
        {
            std::lock_guard<std::mutex> lk(m);
            ready = true;
            std::cout << "main() signals data ready for processing\n";
        }
        cv.notify_one();
    
        // 等候 worker
        {
            std::unique_lock<std::mutex> lk(m);
            cv.wait(lk, []{return processed;});
        }
        std::cout << "Back in main(), data = " << data << '\n';
    
        worker.join();
    }
    

执行结果:

main() signals data ready for processing
Worker thread is processing data
Worker thread signals data processing completed
Back in main(), data = Example data after processing

condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable

有意修改变量的线程必须

  1. 获得 std::mutex (常通过 std::lock_guard

  2. 在保有锁时进行修改

  3. std::condition_variable 上执行 notify_onenotify_all (不需要为通知保有锁)

    即使共享变量是原子的,也必须在互斥下修改它,以正确地发布修改到等待的线程。

    任何有意在 std::condition_variable 上等待的线程必须

    1. 在与用于保护共享变量者相同的互斥上获得 std::unique_lock<std::mutex>
    2. 执行下列之一:
      1. 检查条件,是否为已更新或提醒它的情况
      2. 执行 waitwait_forwait_until ,等待操作自动释放互斥,并悬挂线程的执行。
      3. condition_variable 被通知时,时限消失或虚假唤醒发生,线程被唤醒,且自动重获得互斥。之后线程应检查条件,若唤醒是虚假的,则继续等待。

    放一下wait_for的例子:

    // condition_variable::wait_for example
    #include <iostream>           // std::cout
    #include <thread>             // std::thread
    #include <chrono>             // std::chrono::seconds
    #include <mutex>              // std::mutex, std::unique_lock
    #include <condition_variable> // std::condition_variable, std::cv_status
    
    std::condition_variable cv;
    
    int value;
    
    void read_value() {
      std::cin >> value;
      cv.notify_one();
    }
    
    int main ()
    {
      std::cout << "Please, enter an integer (I'll be printing dots): \n";
      std::thread th (read_value);
    
      std::mutex mtx;
      std::unique_lock<std::mutex> lck(mtx);
      while (cv.wait_for(lck,std::chrono::seconds(1))==std::cv_status::timeout) {
        std::cout << '.' << std::endl;
      }
      std::cout << "You entered: " << value << '\n';
    
      th.join();
    
      return 0;
    }
    

condition_variable_any

与condition_variable相同,不同之处在于其等待函数可以将任何可锁定类型用作参数(condition_variable对象只能采用unique_lock )。 除此之外,它们是相同的。

// condition_variable_any::wait (with predicate)
#include <iostream>           // std::cout
#include <thread>             // std::thread, std::this_thread::yield
#include <mutex>              // std::mutex
#include <condition_variable> // std::condition_variable_any

std::mutex mtx;
std::condition_variable_any cv;

int cargo = 0;
bool shipment_available() {return cargo!=0;}

void consume (int n) {
  for (int i=0; i<n; ++i) {
    mtx.lock();
    cv.wait(mtx,shipment_available);
    // consume:
    std::cout << cargo << '\n';
    cargo=0;
    mtx.unlock();
  }
}

int main ()
{
  std::thread consumer_thread (consume,10);

  // produce 10 items when needed:
  for (int i=0; i<10; ++i) {
    while (shipment_available()) std::this_thread::yield();
    mtx.lock();
    cargo = i+1;
    cv.notify_one();
    mtx.unlock();
  }

  consumer_thread.join();

  return 0;
}

notify_all_at_thread_exit

// notify_all_at_thread_exit
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id (int id) {
  std::unique_lock<std::mutex> lck(mtx);
  while (!ready) cv.wait(lck);
  // ...
  std::cout << "thread " << id << '\n';
}

void go() {
  std::unique_lock<std::mutex> lck(mtx);
  std::notify_all_at_thread_exit(cv,std::move(lck));
  ready = true;
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_id,i);
  std::cout << "10 threads ready to race...\n";

  std::thread(go).detach();   // go!

  for (auto& th : threads) th.join();

  return 0;
}

跟之前的例子差不多,看一下应该就能理解~

cv_status

wait_for()的返回值类型。

Future

promise

// promise example
#include <iostream>       // std::cout
#include <functional>     // std::ref
#include <thread>         // std::thread
#include <future>         // std::promise, std::future

void print_int (std::future<int>& fut) {
  int x = fut.get();
  std::cout << "value: " << x << '\n';
}

int main ()
{
  std::promise<int> prom;                      // create promise

  std::future<int> fut = prom.get_future();    // engagement with future

  std::thread th1 (print_int, std::ref(fut));  // send future to new thread

  prom.set_value (10);                         // fulfill promise
                                               // (synchronizes with getting the future)
  th1.join();
  return 0;
}

Promise是一个对象,它可以存储要由Future对象(可能在另一个线程中)检索的T类型的值,并提供一个同步点。

在构造上,promise对象与新的共享状态相关联,在它们上可以存储T类型的值或从std :: exception派生的异常。

可以通过调用成员get_future将该共享状态与Future的对象相关联。 调用之后,两个对象共享相同的共享状态:
-Promise对象是异步提供程序,应在某个时候为共享状态设置一个值。
-Future对象是一个异步返回对象,可以检索共享状态的值,并在必要时等待其准备就绪。

共享状态的生存期至少要持续到与之关联的最后一个对象释放它或销毁它为止。 因此,如果它也与Future相关联,则它可以幸免最初获得它的Promise对象。

std::future::get()是阻塞的,直到另外一个线程调用std::promise::set_value()。

packaged_task

// packaged_task example
#include <iostream>     // std::cout
#include <future>       // std::packaged_task, std::future
#include <chrono>       // std::chrono::seconds
#include <thread>       // std::thread, std::this_thread::sleep_for

// count down taking a second for each value:
int countdown (int from, int to) {
  for (int i=from; i!=to; --i) {
    std::cout << i << '\n';
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
  std::cout << "Lift off!\n";
  return from-to;
}

int main ()
{
  std::packaged_task<int(int,int)> tsk (countdown);   // set up packaged_task
  std::future<int> ret = tsk.get_future();            // get future

  std::thread th (std::move(tsk),10,0);   // spawn thread to count down from 10 to 0

  // ...

  int value = ret.get();                  // wait for the task to finish and get result

  std::cout << "The countdown lasted for " << value << " seconds.\n";

  th.join();

  return 0;
}

std::packaged_task用法类似std::function,但是返回结果可以通过关联的std::future获取。

future

// future example
#include <iostream>       // std::cout
#include <future>         // std::async, std::future
#include <chrono>         // std::chrono::milliseconds

// a non-optimized way of checking for prime numbers:
bool is_prime (int x) {
  for (int i=2; i<x; ++i) if (x%i==0) return false;
  return true;
}

int main ()
{
  // call function asynchronously:
  std::future<bool> fut = std::async (is_prime,444444443); 

  // do something while waiting for function to set future:
  std::cout << "checking, please wait";
  std::chrono::milliseconds span (100);
  while (fut.wait_for(span)==std::future_status::timeout)
    std::cout << '.' << std::flush;

  bool x = fut.get();     // retrieve return value

  std::cout << "\n444444443 " << (x?"is":"is not") << " prime.\n";

  return 0;
}

shared_future

async

// async example
#include <iostream>       // std::cout
#include <future>         // std::async, std::future

// a non-optimized way of checking for prime numbers:
bool is_prime (int x) {
  std::cout << "Calculating. Please, wait...\n";
  for (int i=2; i<x; ++i) if (x%i==0) return false;
  return true;
}

int main ()
{
  // call is_prime(313222313) asynchronously:
  std::future<bool> fut = std::async (is_prime,313222313);

  std::cout << "Checking whether 313222313 is prime.\n";
  // ...

  bool ret = fut.get();      // waits for is_prime to return

  if (ret) std::cout << "It is prime!\n";
  else std::cout << "It is not prime.\n";

  return 0;
}

launch

future_status

Future错误

future_error
future_category
future_errc

Future目前本身用的比较少,目前只是列出来比较常见的一些类和函数的用法,之后再遇到复杂的使用场景的时候再把相关内容补上~

线程池代码:

  • threadpool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include <deque>
#include <thread>
#include <vector>
#include <mutex>
#include <condition_variable>

class Task
{
public:
    Task(int id);
    void Do();
private:
    int m_ID;
};

class MyThread : public std::thread
{
public:
    MyThread();
private:
    bool isFree;
};

class ThreadPool
{
public:
    ThreadPool();
~ThreadPool();
    void Start(int tNum);
    void AppendTask(const Task* task);
    void Stop();
    void Join();

private:
    void work();
private:
    std::deque<Task*> m_Tasks;
    std::vector<std::thread*> m_Threads;
    std::mutex  m_Mutex;
    std::condition_variable m_Cond;

    bool m_IsRunning;
};

#endif // THREADPOOL_H

  • threadpool.cpp
#include <functional>
#include <iostream>
#include <chrono>
#include "threadpool.h"

using DoTask = std::function<void()>();
using MS = std::chrono::seconds;

ThreadPool::ThreadPool()
{

}

ThreadPool::~ThreadPool()
{
    if (m_IsRunning)
        Stop();
}

void ThreadPool::Start(int tNum)
{
    std::unique_lock<std::mutex> ul(m_Mutex);
    m_IsRunning = true;
    for(int n = 0; n < tNum; ++n)
        m_Threads.emplace_back(new std::thread(&ThreadPool::work, this));

    std::cout << __FUNCTION__ << std::endl;
}

void ThreadPool::AppendTask(const Task *task)
{
    m_Tasks.push_back(const_cast<Task*>(task));
    m_Cond.notify_one();
}

void ThreadPool::Stop()
{
    {
        std::unique_lock<std::mutex> mt(m_Mutex);
        m_IsRunning = false;
        m_Cond.notify_all();
    }

    for(std::thread* t : m_Threads)
    {
        if(t->joinable())
            t->join();
    }
}

void ThreadPool::Join()
{

}

void ThreadPool::work()
{
    while(m_IsRunning)
    {
        Task* task = nullptr;
        {
            std::unique_lock<std::mutex> ul(m_Mutex);
            if(!m_Tasks.empty())
            {
                task = m_Tasks.back();
                m_Tasks.pop_back();
            }
            else
            {
                std::cout << "wait id = " << std::this_thread::get_id() << std::endl;
                m_Cond.wait(ul);
            }
        }
        if(task)
            task->Do();
    }
}

Task::Task(int id)
    : m_ID(id)
{

}

void Task::Do()
{
    std::mutex mu;
    std::unique_lock<std::mutex> ul(mu);
    std::this_thread::sleep_for(MS(m_ID));
    std::cout << "thread id: " << std::this_thread::get_id() << " do task: " << m_ID << std::endl;
}
  • main.cpp
#include <iostream>
#include "threadpool.h"
using namespace std;

int main()
{
    ThreadPool tp;
    tp.Start(10);

    int n = 0;
    Task task1(++n);
    Task task2(++n);
    Task task3(++n);
    Task task4(++n);

    while(1)
    {
        char c;
        std::cout << "please input char c to input tasks:" << std::endl;
        std::cin >> c;
        if(c == 'c')
        {
            tp.AppendTask(&task1);
            tp.AppendTask(&task2);
            tp.AppendTask(&task3);
            tp.AppendTask(&task4);
        }
        else
        {
            continue;
        }
    }

    tp.Join();
    return 0;
}

关键点:

  1. 刚开始根据参数创建固定数量的线程
  2. 线程函数是成员函数
  3. 每个线程是一个跑在一个死循环里面
    1. 等待 ->执行任务->等待
    2. 通过一个isRunning标志位决定该线程是否退出,在stop函数里设置isRunning = false
  4. 标准的生产者-消费者模型
posted @ 2020-09-29 12:15  鬼谷子com  阅读(1229)  评论(0编辑  收藏  举报