C++11线程异步-创新互联

文章目录
    • 1. 线程异步的概念
    • 2. future
      • 2.1 共享状态
      • 2.2 常用成员函数
    • 3. promise
      • 3.1 常用成员函数
      • 3.2 promise的基本使用
    • 4. package_task
      • 4.1 常用成员函数
      • 4.2 package_task的基本使用
    • 5. async
      • 5.1 async的基本使用
    • 6. promise、package_task、async的对比与总结

创新互联为企业级客户提高一站式互联网+设计服务,主要包括成都网站建设、做网站手机APP定制开发微信小程序开发、宣传片制作、LOGO设计等,帮助客户快速提升营销能力和企业形象,创新互联各部门都有经验丰富的经验,可以确保每一个作品的质量和创作周期,同时每年都有很多新员工加入,为我们带来大量新的创意。 1. 线程异步的概念

问题1: 如何理解线程异步?

 异步的反义词是同步。异步与同步的区别见此处: [[Linux/计算机网络基础知识点/高级IO#同步通信 vs 异步通信]]。实际上在多线程下,大部分时候都是存在过异步这一状态的。主线程在创建了子线程后,也去干自己的任务了。

问题2: 线程异步的应用场景?

  1. 主线程想要得到某一子线程运行的任务函数的运行结果。这里的结果可以使用future对象进行存储。(future是个模板类, 能存储任意类型, 包括void)//子—>主
  2. 主线程想要通知子线程, 依靠future值和状态 来达成某一目的(让子线程结束/满足条件/…), 此时主线程在外面设置future对象的共享状态及future值。子线程那边可以根据future对象的状态和值进行一些逻辑判断然后到达想要的结果。//主—>子

2. future
//包含于头文件
templatefuture;
templatefuture;     // specialization : T is a reference type (R&)
template<> future;   // specialization : T is void
//---------------------------------------------------
//构造函数
future() noexcept;					//(1) default
future (const future&) = delete;	//(2) copy [deleted]
future (future&& x) noexcept;		//(3) move

//赋值
future& operator=(future&& other) noexcept;
future& operator=(const future& other) = delete;


 future对象是用于存储某一类型的值的,只不过这个值往往是在未来才能获取到。 它被用来作为线程异步的中间存储值。future的值由以下三个异步任务的提供者(Provider)提供:

  1. std::promise
  2. std::package_task
  3. std::async

 我们根据future的构造函数可以发现,future不支持拷贝构造。future的operator=()会去调用移动构造。

2.1 共享状态

 future在线程异步当中扮演的是一个被动角色。它需要与promisepackage_taskasync配合来实现线程异步。由于它必须要进行共享关联,因此future对象时存在共享状态是否有效的问题的。只有共享状态有效,才能获取future的值。

 future对象是有"共享状态"这一概念的。共享状态必须依靠上面提到的三者对应的方法:promise::get_future()package_task::get_future()async()获取。否则单纯的创建一个future对象, 它的共享状态是无效的!

共享状态解释
future_status::deferred子线程中的任务函仍未启动
future_status::timeout子线程中的任务正在执行中,指定等待时长已用完
future_status::ready子线程中的任务已经执行完毕,结果已就绪

 实际上,为了方便我们理解,还应该加一个状态: 无效状态(invalid)。这个状态存在于:
①future对象没有接收任何提供者的共享关联;
②future对象ready完毕后,被调用者通过get()获取过了。

2.2 常用成员函数
成员函数功能
valid()判断共享状态是否有效
wait()等待共享状态ready
wait_for()等待一段时间
wait_until()等待到某个时间点
get()获取future的值

注意:

  • 在调用get()时,如果future的共享状态不是ready, 则调用者会被阻塞。
  • get()只能被调用一次,第二次会抛出异常。(因为第一次完成后,future的状态就是无效的了)
  • 调用wait()方法会阻塞式等待共享状态为ready。
  • wait_for()wait_until()无法保证等待结束后的future对象的状态一定是ready! (所以它们不太常用, 因为调用完毕后还需要使用valid()判断共享状态)
  • wait_for()wait_until()的返回值是std::future_status。因此我们可以通过接收它们的返回值来循环判断future对象是否ready。

3. promise
//包含于头文件
templatepromise;
templatepromise;  // specialization : T is a reference type (R&)
template<> promise;// specialization : T is void

//构造函数
promise();								//(1)
promise(promise&& other) noexcept;		//(2) 移动构造
promise(const promise& other) = delete;	//(3) 禁止拷贝构造

//赋值
promise& operator= (promise&& rhs) noexcept; //允许移动赋值
promise& operator= (const promise&) = delete;//禁止拷贝赋值

 promise是一个协助线程赋值的类,在promise类的内部管理着一个future对象。因此它能够提供一些将数据和future对象绑定起来的接口。

3.1 常用成员函数
成员函数功能
get_future()获取future对象
set_value()设置future对象的值(立刻)
set_value_at_thread_exit()在线程结束时,才会设置future对象的值,


• get_future()

 get_future()会返回一个future对象, 此时如果去接收它的返回值则会触发移动赋值, 将资源转移。

• set_value()

 设置future对象的值,并立即设置future对象的共享状态为ready

• set_value_at_thread_exit()

 设置future对象的值,但是不会立刻让future对象的共享状态为ready。在子线程退出时,子线程资源被销毁,再令共享状态为ready

3.2 promise的基本使用

①: 子线程set_value—>给主线程

  1. 在主线程中创建promise对象
  2. 将这个promise对象通过引用传递的方式传给子线程的任务函数(ref)
  3. 子线程在合适的时候调用set_value()方法, 设置future对象的值以及状态(ready)
  4. 主线程通过调用promise对象中的get_future()方法获取到future对象 (这里是移动构造了)
  5. 主线程调用future对象中的get()方法获取到子线程set_value()所设置的值。
void func(promise& pr)
{cout<< "Child Thread Working~~~"<< endl;
    cout<< "Child Thread: Waiting 3 seconds!"<< endl;
    this_thread::sleep_for(chrono::seconds(3));
    
    pr.set_value(3);
    this_thread::sleep_for(chrono::seconds(1));
    cout<< "Child Exit"<< endl;
}

int main()
{promisepr;
    thread t(func, ref(pr));
    auto f = pr.get_future();
    this_thread::sleep_for(chrono::seconds(1));
    cout<< "Get Future: "<< f.get()<< endl;
    t.join();
    return 0;
}

注意:

 根据现象, 我们可以发现主线程在调用f.get()时阻塞了一会。此时说明子线程还没有执行到set_value(), 此时的future对象中的共享状态不是ready, 因此主线程会被阻塞。


②: 主线程set_value–>给子线程

  1. 在主线程中创建promise对象
  2. 将这个promise对象通过引用传递的方式传给子线程的任务函数(ref)
  3. 主线程在合适的时候调用set_value()方法, 设置future对象的值以及状态(ready)
  4. 在编码子线程时,设置依future对象的值的判断条件,当future的共享状态或者值满足条件时,执行某一任务(或终止)
void func2(promise& pr)
{int i = 0;
    auto val = pr.get_future().get();
    if(val == 1){cout<< "Get Value: "<< val<< endl;
        //do something
    }
    else{cout<< "Get Value: "<< val<< endl;
        //do something
    }
}

int main()
{promisepr;
    thread t(func2, ref(pr));
    cout<< "Main Thread: Waiting 3 seconds!"<< endl;
    this_thread::sleep_for(chrono::seconds(3));
    pr.set_value(1);
    t.join();
}

输出:

Main Thread: Waiting 3 seconds!
Get Value: 1

4. package_task
//包含于头文件
templatepackaged_task;     // undefined
templateclass packaged_task;

//构造函数
packaged_task() noexcept;						//default (1)
templateexplicit packaged_task (Fn&& fn);				//initialization (2)
packaged_task (const packaged_task&) = delete;	//copy [deleted] (3)
packaged_task (packaged_task&& x) noexcept;		//move (4)

//赋值
packaged_task& operator=(packaged_task&& rhs) noexcept; //move (1)
packaged_task& operator=(const packaged_task&) = delete;//copy [deleted] (2)

 package_task包装了一个函数对象(类似于function), 我们可以把它当做函数对象来使用。package_task可以将内部包装的函数和future绑定到一起,以便于进行后续的异步调用。因此我们可以将其理解为它自带了一个函数,并且该函数和future对象绑定到了一起,我们不需要额外定义函数方法了,直接实现package_task中的函数对象即可。

 但package_task相比于promise有个缺点,它里面包装了函数,而该函数的返回值就是future对象的值。它无法像使用promise那样灵活。

4.1 常用成员函数

 package_task中最常用的就是get_future()方法了。它能够获取到package_task中的future对象。

4.2 package_task的基本使用

 将package_task作为线程的启动函数传过去,传参方式必须是引用传递 ref()。

int main()
{packaged_taskpt_Add([](int x, int y)
    {cout<< "Running~~~~~~~~~~"<< endl;
        this_thread::sleep_for(chrono::seconds(3));
        return x + y;
    });

    futurefi = pt_Add.get_future();

    cout<< "Start Thread!"<< endl;
    thread t(ref(pt_Add), 10, 20);

    cout<< "before get"<< endl;
    int val = fi.get();
    cout<< "val: "<< val<< endl;
    t.join();
    return 0;
}

输出:

Start Thread!
before get
Running~~~~~~~~~~
val: 30

5. async
//构造函数
// (1)
templatefuture::type>async (Fn&& fn, Args&&... args);

// (2)
templatefuture::type>async (launch policy, Fn&& fn, Args&&... args);	//policy是启动策略

 async是一个函数,它相比于前面的promise和package_task要高级一些。async可以直接启动一个子线程,然后让这个子线程执行对应的任务函数,任务函数的返回值就会被存储到future对象当中,future对象也就是async函数的返回值。主线程只需要接收asnyc的返回值,然后调用get()方法即可获取到future对象中保存的值。

注: 更高级并不代表更好用,只是它的集成度更高一些,省去了我们要自己创建线程的步骤。async仍然有package_task的缺点,它无法像promise那样自由控制future在何时赋值。

• launch policy启动策略

策略解释
std::launch::async调用async函数时会创建新的线程,让该线程执行任务函数
std::launch::deferred调用async函数时不会创建新的线程,也不会去执行该任务函数。只有调用了async返回的future的get()方法或者wait()方法时才会去执行任务。(执行该任务的是主线程)

5.1 async的基本使用

• 使用默认的启动策略 — 调用async创建子线程, 并让该线程去执行任务

int main()
{futuref = async([](int x, int y)
   {   cout<< "Child Thread: Waiting 3 seconds!"<< endl;
       this_thread::sleep_for(chrono::seconds(3));
       return x + y;
   }, 10, 20);

   this_thread::sleep_for(chrono::seconds(1));
   cout<< "Get Value: "<< f.get()<< endl;
   return 0;
}

输出:

Child Thread: Waiting 3 seconds!
Get Value: 30


• 使用deferred启动策略 — 调用async不创建子线程

int main()
{futuref = async(launch::deferred, [](int x, int y)
    {cout<< "Child Thread "<< this_thread::get_id()<< ": Waiting 3 seconds!"<< endl;
        this_thread::sleep_for(chrono::seconds(3));
        return x + y;
    }, 10, 20);

    cout<< "Main Thread "<< this_thread::get_id()<<": Working!!!"<< endl;
    auto val = f.get();
    cout<< "Get Value: "<< val<< endl;
	return 0;
}

输出:

Main Thread 1: Working!!!
Child Thread 1: Waiting 3 seconds!
Get Value: 30

 我们可以发现使用deferred策略时,是不会创建新的线程的。也就是说async的任务函数依然是由主线程自己去执行的,只不过执行的时机可以控制 (在调用get()方法时会去执行),这个机制类似于回调函数,你主动去调用get()才会去回调执行async的任务。

6. promise、package_task、async的对比与总结
  1. promise类的使用相对灵活,但是需要自己创建线程,并且需要自己写一个函数对象。
  2. package_task类受限于只能使用函数返回值作为future对象的值。使用它也需要自己创建线程,但不需要额外写函数对象,直接将package_task当做函数对象去使用即可。
  3. async类集合度较高,它也受限于只能使用函数返回值作为future对象的值。但是async定义时可以自动创建线程,并让线程执行async中的任务函数。async的使用最简单,但是自由度较低。

细节总结:

  1. 调用get_future()方法, 并不会让线程被阻塞。只要调用future对象的get()方法,才可能被阻塞。(future共享状态没有ready就会被阻塞, 前提是future共享状态有效)
  2. 创建线程时,给线程传参要注意使用ref()的时机。

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


新闻标题:C++11线程异步-创新互联
分享URL:http://myzitong.com/article/djgoje.html