中国桥梁建设网站,最新热搜榜,室内设计好学吗,接私活做网站设计文章目录 1.线程的使用1.1 函数构造1.2 公共成员函数1.2.1 get_id()1.2.2 join()2.2.3 detach()2.2.5 joinable()2.2.6 operator 1.3 静态函数1.4 call_once 2. this_thread 命名空间2.1 get_id()2.2 sleep_for()2.3 sleep_until()2.4 yield() 3. 线程同步之互斥锁3.1 std:mute… 文章目录 1.线程的使用1.1 函数构造1.2 公共成员函数1.2.1 get_id()1.2.2 join()2.2.3 detach()2.2.5 joinable()2.2.6 operator 1.3 静态函数1.4 call_once 2. this_thread 命名空间2.1 get_id()2.2 sleep_for()2.3 sleep_until()2.4 yield() 3. 线程同步之互斥锁3.1 std:mutex3.1.1 成员函数3.1.2 线程同步 3.2 std::lock_guard3.3 std::recursive_mutex3.4 std::timed_mutex 4.线程同步之条件变量4.1 condition_variable4.1.1 成员函数4.1.2 生产者和消费者模型 4.2 condition_variable_any4.2.1 成员函数4.2.2 生产者和消费者模型 5. 原子变量5.1 atomic 类成员5.1.1 构造函数5.1.2 公共成员函数5.1.3 特化成员函数5.1.4 内存顺序约束5.1.5 C20新增成员 5.2 原子变量的使用5.2.1 互斥锁版本5.2.2 原子变量版本 6. 多线程异步操作6.1 std:future6.2 std::promise6.2.1 类成员函数6.2.2 promise的使用 6.3. std::packaged_task6.3.1 类成员函数6.3.2 packaged_task的使用 6.4 std::async6.4.1 方式16.4.2 方式2 1.线程的使用 C11中增加了线程以及线程相关的类支持了并发编程提高了编写的多线程程序的可移植性 C11中提供的线程类叫做std::thread基于这个类创建一个新的线程非常的简单只需要提供线程函数或者函数对象即可并且可以同时指定线程函数的参数。 以下了解以下常用API 1.1 函数构造
// 1
thread() noexcept;
// 2
thread(thread other) noexcept;
// 3
template class Function, class... Args
explicit thread( Function f, Args... args );
// 4
thread( const thread ) delete;。构造函数①默认构造函数构造一个线程对象在这个线程中不执行任何处理动作 构造函数②移动构造函数将 other 的线程所有权转移给新的thread 对象。 之后 other 不再表示执行线程。 构造函数③创建线程对象并在该线程中执行函数f中的业务逻辑args是要传递给函数f的参数 任务函数f的可选类型有很多具体如下 普通函数类成员函数匿名函数仿函数这些都是可调用对象类型可以是可调用对象包装器类型也可是使用绑定器绑定之后得到的类型仿函数 构造函数④使用delete显示删除拷贝构造, 不允许线程对象之间的拷贝 1.2 公共成员函数
1.2.1 get_id() 应用程序启动之后默认只有一个线程这个线程一般称之为主线程或父线程通过线程类创建出的线程一般称之为子线程每个被创建出的线程实例都对应一个线程ID这个ID是唯一的可以通过这个ID来区分和识别各个已经存在的线程实例这个获取线程ID的函数叫做get_id() //原型
std::thread::id get_id() const noexcept;eg:
#include iostream
#include thread
#include chrono
using namespace std;void func(int num, string str)
{for (int i 0; i 10; i){cout 子线程: i i num: num , str: str endl;}
}void func1()
{for (int i 0; i 10; i){cout 子线程: i i endl;}
}int main()
{cout 主线程的线程ID: this_thread::get_id() endl;thread t(func, 520, i love you);thread t1(func1);cout 线程t 的线程ID: t.get_id() endl;cout 线程t1的线程ID: t1.get_id() endl;
}thread t(func, 520, “i love you”); 线程类的构造函数③是一个变参函数因此无需担心线程任务函数的参数个数问题任务函数func()一般返回值为void因为子线程在调用这个函数的时候不会处理其返回值 thread t1(func1); 子线程对象t1中的任务函数func1()没有参数因此在线程构造函数中就无需指定了 通过线程对象调用get_id()就可以知道这个子线程的线程ID:t.get_id(),t1.get_id() 但在上面的示例程序中有bug 有可能子线程中的任务还没有执行完毕主线程就结束了,最后也得不到我们想要的结果。 当启动了一个线程创建了一个thread对象之后在这个线程结束的时候std::terminate()我们如何去回收线程所使用的资源呢thread库给我们两种选择
加入式join()分离式detach()
我们必须要在线程对象销毁之前在二者之间作出选择否则程序运行期间会有bug产生。 1.2.2 join() join()字面意思是连接一个线程意味着主动地等待线程的终止线程阻塞。 在某个线程中通过子线程对象调用join()函数调用这个函数的线程被阻塞 子线程对象中的任务函数会继续执行当任务执行完毕之后join()会清理当前子线程中的相关资源然后返回同时调用该函数的线程解除阻塞继续向下执行。 //原型
void join();解决后如下:
int main()
{cout 主线程的线程ID: this_thread::get_id() endl;thread t(func, 520, i love you);thread t1(func1);cout 线程t 的线程ID: t.get_id() endl;cout 线程t1的线程ID: t1.get_id() endl;t.join();t1.join();
}为了更好的理解join()的使用再举一个例子场景如下 程序中一共有三个线程其中两个子线程负责分段处理函数完毕之后由主线程对这个文件进行下一步处理 #include iostream
#include thread
using namespace std;void download1()
{...
}void download2()
{...
}void doSomething()
{...
}int main()
{thread t1(download1);thread t2(download2);// 阻塞主线程等待所有子线程任务执行完毕再继续向下执行t1.join();t2.join();doSomething();
}2.2.3 detach() detach()函数的作用是进行线程分离分离主线程和创建出的子线程。 在线程分离之后主线程退出也会一并销毁创建出的所有子线程 在主线程退出之前它可以脱离主线程继续独立的运行 任务执行完毕之后这个子线程会自动释放自己占用的系统资源。 //原型
void detach();线程分离函数没有参数也没有返回值只需要在线程成功之后通过线程对象调用该函数即可
int main()
{cout 主线程的线程ID: this_thread::get_id() endl;thread t(func, 520, i love you);thread t1(func1);cout 线程t 的线程ID: t.get_id() endl;cout 线程t1的线程ID: t1.get_id() endl;t.detach();t1.detach();// 让主线程休眠, 等待子线程执行完毕this_thread::sleep_for(chrono::seconds(5));
}注意detach()不会阻塞线程 子线程和主线程分离之后主线程就不能再对这个子线程做任何控制 比如通过join()阻塞主线程等待子线程中的任务执行完毕或调用get_id()获取子线程的线程ID。 2.2.5 joinable()
joinable()函数用于判断主线程和子线程是否处理关联连接状态
返回值为true主线程和子线程之间有关联连接关系返回值为false主线程和子线程之间没有关联连接关系
//原型
bool joinable() const noexcept;eg:
#include iostream
#include thread
#include chrono
using namespace std;void foo()
{this_thread::sleep_for(std::chrono::seconds(1));
}int main()
{thread t;cout before starting, joinable: t.joinable() endl;t thread(foo);cout after starting, joinable: t.joinable() endl;t.join();cout after joining, joinable: t.joinable() endl;thread t1(foo);cout after starting, joinable: t1.joinable() endl;t1.detach();cout after detaching, joinable: t1.joinable() endl;
}before starting, joinable: 0
after starting, joinable: 1
after joining, joinable: 0
after starting, joinable: 1
after detaching, joinable: 0给予结果我们可以得到如下结论
在创建的子线程对象的时候如果没有指定任务函数那么子线程不会启动主线程和这个子线程也不会进行连接在创建的子线程对象的时候如果指定了任务函数子线程启动并执行任务主线程和这个子线程自动连接成功子线程调用了detach()函数之后父子线程分离同时二者的连接断开调用joinable()返回false在子线程调用了join()函数子线程中的任务函数继续执行直到任务处理完毕 这时join()会清理回收当前子线程的相关资源所以这个子线程和主线程的连接也就断开了因此调用join()之后再调用joinable()会返回false。 2.2.6 operator 线程中的资源是不能被复制的因此通过操作符进行赋值操作最终并不会得到两个完全相同的对象。 // move (1)
thread operator (thread other) noexcept;
// copy [deleted] (2)
thread operator (const other) delete;通过以上操作符的重载声明可以得知
如果other是一个右值会进行资源所有权的转移如果other不是右值禁止拷贝该函数被显示删除delete不可用 1.3 静态函数 thread线程类还提供了一个静态方法用于获取当前计算机的CPU核心数 根据这个结果可以在程序中创建出数量相等的线程 每个线程独占一个CPU核心这些线程就不用分时复用CPU时间片此时程序的并发效率是最高的。 //函数原型
static unsigned hardware_concurrency() noexcept;eg:
#include iostream
#include thread
using namespace std;int main()
{int num thread::hardware_concurrency();cout CPU number: num endl;
}1.4 call_once 在某些特定情况下某些函数只能在多线程环境下调用一次比如要初始化某个对象而这个对象只能被初始化一次 可以使用std::call_once()来保证函数在多线程环境下只能被调用一次。使用call_once()的时候需要一个once_flag作为call_once()的传入参数 //函数原型
// 定义于头文件 mutex
template class Callable, class... Args
void call_once( std::once_flag flag, Callable f, Args... args );flagonce_flag类型的对象要保证这个对象能够被多个线程同时访问到f回调函数可以传递一个有名函数地址也可以指定一个匿名函数args作为实参传递给回调函数
eg:
#include iostream
#include thread
#include mutex
using namespace std;once_flag g_flag;
void do_once(int a, string b)
{cout name: b , age: a endl;
}void do_something(int age, string name)
{static int num 1;call_once(g_flag, do_once, 19, luffy);cout do_something() function num num endl;
}int main()
{thread t1(do_something, 20, ace);thread t2(do_something, 20, sabo);thread t3(do_something, 19, luffy);t1.join();t2.join();t3.join();return 0;
}name: luffy, age: 19
do_something() function num 1
do_something() function num 2
do_something() function num 32. this_thread 命名空间 在C11中不仅添加了线程类还添加了一个关于线程的命名空间std::this_thread 在这个命名空间中提供了四个公共的成员函数 2.1 get_id() 所说曾相识,但还是有所不同 调用命名空间std::this_thread中的get_id()方法可以得到当前线程的线程ID //原型
thread::id get_id() noexcept;eg:
#include iostream
#include thread
using namespace std;void func()
{cout 子线程: this_thread::get_id() endl;
}int main()
{cout 主线程: this_thread::get_id() endl;thread t(func);t.join();
}2.2 sleep_for() 线程被创建后有这五种状态创建态就绪态运行态阻塞态(挂起态)退出态(终止态) 关于状态之间的转换和进程是一样的 线程和进程的执行有很多相似之处在计算机中启动的多个线程都需要占用CPU资源 但是CPU的个数是有限的并且每个CPU在同一时间点不能同时处理多个任务。 为了实现并发处理多个线程都是分时复用CPU时间片快速的交替处理各个线程中的任务。 因此多个线程之间需要争抢CPU时间片抢到了就执行抢不到则无法执行 因为默认所有的线程优先级都相同内核也会从中调度不会出现某个线程永远抢不到CPU时间片的情况。
命名空间this_thread中提供了一个休眠函数sleep_for() 调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长 因为阻塞态的线程已经让出了CPU资源代码也不会被执行所以线程休眠过程中对CPU来说没有任何负担。
//原型
template class Rep, class Period
void sleep_for (const chrono::durationRep,Period rel_time);参数需要指定一个休眠时长是一个时间段
eg:
#include iostream
#include thread
#include chrono
using namespace std;void func()
{for (int i 0; i 10; i){this_thread::sleep_for(chrono::seconds(1));cout 子线程: this_thread::get_id() , i i endl;}
}int main()
{thread t(func);t.join();
}在func()函数的for循环中使用了this_thread::sleep_for(chrono::seconds(1));之后每循环一次程序都会阻塞1秒钟也就是说每隔1秒才会进行一次输出。 注意程序休眠完成之后会从阻塞态重新变成就绪态就绪态的线程需要再次争抢CPU时间片抢到之后才会变成运行态这时候程序才会继续向下运行。 2.3 sleep_until() 命名空间this_thread中提供了另一个休眠函数sleep_until()和sleep_for()不同的是它的参数类型不一样 sleep_until()指定线程阻塞到某一个指定的时间点time_point类型之后解除阻塞 sleep_for()指定线程阻塞一定的时间长度duration 类型之后解除阻塞 //原型
template class Clock, class Duration
void sleep_until (const chrono::time_pointClock,Duration abs_time);eg:
#include iostream
#include thread
#include chrono
using namespace std;void func()
{for (int i 0; i 10; i){// 获取当前系统时间点auto now chrono::system_clock::now();// 时间间隔为2schrono::seconds sec(2);// 当前时间点之后休眠两秒this_thread::sleep_until(now sec);cout 子线程: this_thread::get_id() , i i endl;}
}int main()
{thread t(func);t.join();
}sleep_until()和sleep_for()函数的功能是一样的 只不过前者是基于时间点去阻塞线程 后者是基于时间段去阻塞线程 2.4 yield() 命名空间this_thread中提供了一个非常绅士的函数yield() 在线程中调用这个函数后处于运行态的线程会主动让出自己已经抢到的CPU时间片最终变为就绪态 这样其它的线程就有更大的概率能够抢到CPU时间片了。 注意:线程调用了yield()之后,这个线程会马上参与到下一轮CPU的抢夺中 //原型
void yield() noexcept;eg:
#include iostream
#include thread
using namespace std;void func()
{for (int i 0; i 100000000000; i){cout 子线程: this_thread::get_id() , i i endl;this_thread::yield();}
}int main()
{thread t(func);thread t1(func);t.join();t1.join();
}在上面的程序中执行func()中的for循环会占用大量的时间 极端情况下如果当前线程占用CPU资源不释放就会导致其他线程中的任务无法被处理或者该线程每次都能抢到CPU时间片导致其他线程中的任务没有机会被执行。 解决方案就是每执行一次循环让该线程主动放弃CPU资源重新和其他线程再次抢夺CPU时间片如果其他线程抢到了CPU时间片就可以执行相应的任务了。
结论std::this_thread::yield() 的目的是避免一个线程长时间占用CPU资源从而导致多线程处理性能下降 3. 线程同步之互斥锁
进行多线程编程如果多个线程需要对同一块内存进行操作比如同时读、同时写、同时读写对于后两种情况来说如果不做任何的人为干涉就会出现各种各样的错误数据。 这是因为线程在运行的时候需要先得到CPU时间片时间片用完之后需要放弃已获得的CPU资源就这样线程频繁地在就绪态和运行态之间切换更复杂一点还可以在就绪态、运行态、挂起态之间切换这样就会导致线程的执行顺序并不是有序的而是随机的混乱的
解决多线程数据混乱的方案就是进行线程同步最常用的就是互斥锁在C11中一共提供了四种互斥锁
std::mutex独占的互斥锁不能递归使用std::timed_mutex带超时的独占互斥锁不能递归使用std::recursive_mutex递归互斥锁不带超时功能std::recursive_timed_mutex带超时的递归互斥锁
互斥锁在有些资料中也被称之为互斥量二者是一个东西。
3.1 std:mutex
不论是在C还是C中进行线程同步的处理流程基本上是一致的C的mutex类提供了相关的API函数
3.1.1 成员函数
lock()函数用于给临界区加锁并且只能有一个线程获得锁的所有权它有阻塞线程的作用
//函数原型
void lock();独占互斥锁对象有两种状态锁定和未锁定。 如果互斥锁是打开的调用lock()函数的线程会得到互斥锁的所有权并将其上锁其它线程再调用该函数的时候由于得不到互斥锁的所有权就会被lock()函数阻塞。 当拥有互斥锁所有权的线程将互斥锁解锁此时被lock()阻塞的线程解除阻塞抢到互斥锁所有权的线程加锁并继续运行没抢到互斥锁所有权的线程继续阻塞。
除了使用lock()还可以使用try_lock()获取互斥锁的所有权并对互斥锁加锁
//函数原型
bool try_lock();二者的区别在于try_lock()不会阻塞线程lock()会阻塞线程
如果互斥锁是未锁定状态得到了互斥锁所有权并加锁成功函数返回true如果互斥锁是锁定状态无法得到互斥锁所有权加锁失败函数返回false
当互斥锁被锁定之后可以通过unlock()进行解锁但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁其它线程是没有权限做这件事情的。
//函数原型
void unlock();使用互斥锁进行线程同步的大致思路主要分为以下几步
找到多个线程操作的共享资源(全局变量、堆内存、类成员变量等),也可以称之为临界资源找到和共享资源有关的上下文代码也就是临界区下图中的黄色代码部分在临界区的上边调用互斥锁类的lock()方法在临界区的下边调用互斥锁的unlock()方法
线程同步的目的是让多线程按照顺序依次执行临界区代码这样做线程对共享资源的访问就从并行访问变为了线性访问访问效率降低但是保证了数据的正确性。 3.1.2 线程同步
让两个线程共同操作一个全局变量二者交替数数
#include iostream
#include chrono
#include thread
#include mutex
using namespace std;int g_num 0; // 为 g_num_mutex 所保护
mutex g_num_mutex;void slow_increment(int id)
{for (int i 0; i 3; i) {g_num_mutex.lock();g_num;cout id g_num endl;g_num_mutex.unlock();this_thread::sleep_for(chrono::seconds(1));}
}int main()
{thread t1(slow_increment, 0);thread t2(slow_increment, 1);t1.join();t2.join();
}在上面的示例程序中两个子线程执行的任务的一样的可以不一样不同的任务中也可以对共享资源进行读写操作在任务函数中把与全局变量相关的代码加了锁两个线程只能顺序访问这部分代码如果不进行线程同步打印出的数据是混乱且无序的。
注意:
在所有线程的任务函数执行完毕之前互斥锁对象是不能被析构的一定要在程序中保证这个对象的可用性。互斥锁的个数和共享资源的个数相等也就是说每一个共享资源都应该对应一个互斥锁对象。互斥锁对象的个数和线程的个数没有关系。 3.2 std::lock_guard lock_guard是C11新增的一个模板类使用这个类可以简化互斥锁lock()和unlock()的写法同时也更安全。 // 定义和常用的构造函数原型
// 类的定义定义于头文件 mutex
template class Mutex
class lock_guard;// 常用构造函数
explicit lock_guard( mutex_type m );lock_guard在使用上面的构造函数构造对象时会自动锁定互斥量 而在退出作用域后进行析构时就会自动解锁从而保证了互斥量的正确操作避免忘记unlock()操作而导致线程死锁。 lock_guard使用了RAII技术就是在类构造函数中分配资源在析构函数中释放资源保证资源出了作用域就释放。
使用lock_guard对上面的例子进行修改
void slow_increment(int id)
{for (int i 0; i 3; i) {// 使用哨兵锁管理互斥锁lock_guardmutex lock(g_num_mutex);g_num;cout id g_num endl;this_thread::sleep_for(chrono::seconds(1));}
}通过修改发现代码被精简了而且不用担心因为忘记解锁而造成程序的死锁 弊端:在上面的示例程序中整个for循环的体都被当做了临界区多个线程是线性的执行临界区代码的因此临界区越大程序效率越低 3.3 std::recursive_mutex 递归互斥锁std::recursive_mutex允许同一线程多次获得互斥锁可以用来解决同一线程需要多次获取互斥量时死锁的问题 使用独占非递归互斥量会发生死锁
#include iostream
#include thread
#include mutex
using namespace std;struct Calculate
{Calculate() : m_i(6) {}void mul(int x){lock_guardmutex locker(m_mutex);m_i * x;}void div(int x){lock_guardmutex locker(m_mutex);m_i / x;}void both(int x, int y){lock_guardmutex locker(m_mutex);mul(x);div(y);}int m_i;mutex m_mutex;
};int main()
{Calculate cal;cal.both(6, 3);return 0;
}上面的程序中执行了cal.both(6, 3);调用之后程序就会发生死锁 在both()中已经对互斥锁加锁了继续调用mult()函数已经得到互斥锁所有权的线程再次获取这个互斥锁的所有权就会造成死锁在C中程序会异常退出使用C库函数会导致这个互斥锁永远无法被解锁最终阻塞所有的线程。 要解决这个死锁的问题一个简单的办法就是使用递归互斥锁std::recursive_mutex它允许一个线程多次获得互斥锁的所有权。
#include iostream
#include thread
#include mutex
using namespace std;struct Calculate
{Calculate() : m_i(6) {}void mul(int x){lock_guardrecursive_mutex locker(m_mutex);m_i * x;}void div(int x){lock_guardrecursive_mutex locker(m_mutex);m_i / x;}void both(int x, int y){lock_guardrecursive_mutex locker(m_mutex);mul(x);div(y);}int m_i;recursive_mutex m_mutex;
};int main()
{Calculate cal;cal.both(6, 3);cout cal.m_i cal.m_i endl;return 0;
}虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题但是还是建议少用
使用递归互斥锁的场景往往都是可以简化的使用递归互斥锁很容易放纵复杂逻辑的产生从而导致bug的产生递归互斥锁比非递归互斥锁效率要低一些。递归互斥锁虽然允许同一个线程多次获得同一个互斥锁的所有权但最大次数并未具体说明一旦超过一定的次数就会抛出std::system错误。 3.4 std::timed_mutex std::timed_mutex是超时独占互斥锁主要是在获取互斥锁资源时增加了超时等待功能因为不知道获取锁资源需要等待多长时间为了保证不一直等待下去设置了一个超时时长超时后线程就可以解除阻塞去做其他事情了。 std::timed_mutex比std::_mutex多了两个成员函数try_lock_for() try_lock_until()
void lock();
bool try_lock();
void unlock();// std::timed_mutex比std::_mutex多出的两个成员函数
template class Rep, class Periodbool try_lock_for (const chrono::durationRep,Period rel_time);template class Clock, class Durationbool try_lock_until (const chrono::time_pointClock,Duration abs_time);try_lock_for函数是当线程获取不到互斥锁资源时让线程阻塞一定的时间长度try_lock_until函数是当线程获取不到互斥锁资源时让线程阻塞到某一个指定的时间点关于两个函数的返回值 当得到互斥锁的所有权之后函数会马上解除阻塞返回true 如果阻塞的时长用完或者到达指定的时间点之后函数也会解除阻塞返回false
演示std::timed_mutex的使用
#include iostream
#include thread
#include mutex
using namespace std;timed_mutex g_mutex;void work()
{chrono::seconds timeout(1);while (1){// 通过阻塞一定的时长来争取得到互斥锁所有权if (g_mutex.try_lock_for(timeout)){cout 当前线程ID: this_thread::get_id() , 得到互斥锁所有权... endl;// 模拟处理任务用了一定的时长this_thread::sleep_for(chrono::seconds(10));// 互斥锁解锁g_mutex.unlock();break;}else{cout 当前线程ID: this_thread::get_id() , 没有得到互斥锁所有权... endl;// 模拟处理其他任务用了一定的时长this_thread::sleep_for(chrono::milliseconds(50));}}
}int main()
{thread t1(work);thread t2(work);t1.join();t2.join();return 0;
}示例代码输出的结果
当前线程ID: 125776, 得到互斥锁所有权...
当前线程ID: 112324, 没有得到互斥锁所有权...
当前线程ID: 112324, 没有得到互斥锁所有权...
当前线程ID: 112324, 没有得到互斥锁所有权...
当前线程ID: 112324, 没有得到互斥锁所有权...
当前线程ID: 112324, 没有得到互斥锁所有权...
当前线程ID: 112324, 没有得到互斥锁所有权...
当前线程ID: 112324, 没有得到互斥锁所有权...
当前线程ID: 112324, 没有得到互斥锁所有权...
当前线程ID: 112324, 没有得到互斥锁所有权...
当前线程ID: 112324, 得到互斥锁所有权...在上面的例子中通过一个while循环不停的去获取超时互斥锁的所有权如果得不到就阻塞1秒钟1秒之后如果还是得不到阻塞50毫秒然后再次继续尝试直到获得互斥锁的所有权跳出循环体。
递归超时互斥锁std::recursive_timed_mutex的使用方式和std::timed_mutex是一样的 只不过它可以允许一个线程多次获得互斥锁所有权 而std::timed_mutex只允许线程获取一次互斥锁所有权。 递归超时互斥锁std::recursive_timed_mutex也拥有和std::recursive_mutex一样的弊端 4.线程同步之条件变量 条件变量是C11提供的另外一种用于等待的同步机制它能阻塞一个或多个线程直到收到另外一个线程发出的通知或者超时时才会唤醒当前阻塞的线程。 条件变量需要和互斥量配合起来使用 C11提供了两种条件变量
condition_variable需要配合std::unique_lockstd::mutex进行wait操作也就是阻塞线程的操作。condition_variable_any可以和任意带有lock()、unlock()语义的mutex搭配使用也就是说有四种 std::mutex独占的非递归互斥锁std::timed_mutex带超时的独占非递归互斥锁std::recursive_mutex不带超时功能的递归互斥锁std::recursive_timed_mutex带超时的递归互斥锁
条件变量通常用于生产者和消费者模型大致使用过程如下
拥有条件变量的线程获取互斥量循环检查某个条件如果条件不满足阻塞当前线程否则线程继续向下执行 产品的数量达到上限生产者阻塞否则生产者一直生产产品的数量为零消费者阻塞否则消费者一直消费 条件满足之后可以调用notify_one()或notify_all()唤醒一个或所有被阻塞的线程 由消费者唤醒被阻塞的生产者生产者解除阻塞继续生产由生产者唤醒被阻塞的消费者消费者解除阻塞继续消费 4.1 condition_variable
4.1.1 成员函数 condition_variable的成员函数主要分为两部分线程等待(阻塞)函数和线程通知(唤醒)函数 这些函数被定义于头文件condition_variable 等待函数
调用wait()函数的线程会被阻塞
// 1
void wait (unique_lockmutex lck);
// 2
template class Predicate
void wait (unique_lockmutex lck, Predicate pred);函数①调用该函数的线程直接被阻塞 函数②该函数的第二个参数是一个判断条件是一个返回值为布尔类型的函数 该参数可以传递一个有名函数的地址也可以直接指定一个匿名函数表达式返回false当前线程被阻塞表达式返回true当前线程不会被阻塞继续向下执行 独占的互斥锁对象不能直接传递给wait()函数需要通过模板类unique_lock进行二次处理通过得到的对象仍然可以对独占的互斥锁对象做如下操作使用起来更灵活。
公共成员函数说明lock锁定关联的互斥锁try_lock尝试锁定关联的互斥锁若无法锁定函数直接返回try_lock_for试图锁定关联的可定时锁定互斥锁若互斥锁在给定时长中仍不能被锁定函数返回try_lock_until试图锁定关联的可定时锁定互斥锁若互斥锁在给定的时间点后仍不能被锁定函数返回unlock将互斥锁解锁
如果线程被该函数阻塞这个线程会释放占有的互斥锁的所有权 当阻塞解除之后这个线程会重新得到互斥锁的所有权继续向下执行这个过程是在函数内部完成的,其目的是为了避免线程的死锁。 wait_for()函数和wait()的功能是一样的只不过多了一个阻塞时长 假设阻塞的线程没有被其他线程唤醒当阻塞时长用完之后线程就会自动解除阻塞继续向下执行。 template class Rep, class Period
cv_status wait_for (unique_lockmutex lck,const chrono::durationRep,Period rel_time);template class Rep, class Period, class Predicate
bool wait_for(unique_lockmutex lck,const chrono::durationRep,Period rel_time, Predicate pred);wait_until()函数和wait_for()的功能是一样的它是指定让线程阻塞到某一个时间点假设阻塞的线程没有被其他线程唤醒当到达指定的时间点之后线程就会自动解除阻塞继续向下执行。 template class Clock, class Duration
cv_status wait_until (unique_lockmutex lck,const chrono::time_pointClock,Duration abs_time);template class Clock, class Duration, class Predicate
bool wait_until (unique_lockmutex lck,const chrono::time_pointClock,Duration abs_time, Predicate pred);通知函数
void notify_one() noexcept;
void notify_all() noexcept;notify_one()唤醒一个被当前条件变量阻塞的线程
notify_all()唤醒全部被当前条件变量阻塞的线程4.1.2 生产者和消费者模型 我们可以使用条件变量来实现一个同步队列这个队列作为生产者线程和消费者线程的共享资源 #include iostream
#include thread
#include mutex
#include list
#include functional
#include condition_variable
using namespace std;class SyncQueue
{
public:SyncQueue(int maxSize) : m_maxSize(maxSize) {}void put(const int x){unique_lockmutex locker(m_mutex);// 判断任务队列是不是已经满了while (m_queue.size() m_maxSize){cout 任务队列已满, 请耐心等待... endl;// 阻塞线程m_notFull.wait(locker);}// 将任务放入到任务队列中m_queue.push_back(x);cout x 被生产 endl; // 通知消费者去消费m_notEmpty.notify_one();}int take(){unique_lockmutex locker(m_mutex);while (m_queue.empty()){cout 任务队列已空请耐心等待。。。 endl;m_notEmpty.wait(locker);}// 从任务队列中取出任务(消费)int x m_queue.front();m_queue.pop_front();// 通知生产者去生产m_notFull.notify_one();cout x 被消费 endl;return x;}bool empty(){lock_guardmutex locker(m_mutex);return m_queue.empty();}bool full(){lock_guardmutex locker(m_mutex);return m_queue.size() m_maxSize;}int size(){lock_guardmutex locker(m_mutex);return m_queue.size();}private:listint m_queue; // 存储队列数据mutex m_mutex; // 互斥锁condition_variable m_notEmpty; // 不为空的条件变量condition_variable m_notFull; // 没有满的条件变量int m_maxSize; // 任务队列的最大任务个数
};int main()
{SyncQueue taskQ(50);auto produce bind(SyncQueue::put, taskQ, placeholders::_1);auto consume bind(SyncQueue::take, taskQ);thread t1[3];thread t2[3];for (int i 0; i 3; i){t1[i] thread(produce, i100);t2[i] thread(consume);}for (int i 0; i 3; i){t1[i].join();t2[i].join();}return 0;
}条件变量condition_variable类的wait()还有一个重载的方法 可以接受一个条件这个条件也可以是一个返回值为布尔类型的函数条件变量会先检查判断这个条件是否满足 如满足条布尔值为true则当前线程重新获得互斥锁的所有权结束阻塞继续向下执行 如不满足布尔值为false当前线程会释放互斥锁解锁同时被阻塞等待被唤醒。 上面示例程序中的put()、take()函数可以做如下修改 put()函数
void put(const int x)
{unique_lockmutex locker(m_mutex);// 根据条件阻塞线程m_notFull.wait(locker, [this]() {return m_queue.size() ! m_maxSize;});// 将任务放入到任务队列中m_queue.push_back(x);cout x 被生产 endl;// 通知消费者去消费m_notEmpty.notify_one();
}take()函数
int take()
{unique_lockmutex locker(m_mutex);m_notEmpty.wait(locker, [this]() {return !m_queue.empty();});// 从任务队列中取出任务(消费)int x m_queue.front();m_queue.pop_front();// 通知生产者去生产m_notFull.notify_one();cout x 被消费 endl;return x;
}修改之后可以发现程序变得更加精简了而且执行效率更高了因为在这两个函数中的while循环被删掉了但是最终的效果是一样的推荐使用这种方式的wait()进行线程的阻塞。 4.2 condition_variable_any
4.2.1 成员函数 condition_variable_any的成员函数也是分为两部分线程等待阻塞函数 和线程通知唤醒函数这些函数被定义于头文件 condition_variable 等待函数
// 1
template class Lock void wait (Lock lck);
// 2
template class Lock, class Predicate
void wait (Lock lck, Predicate pred);函数①调用该函数的线程直接被阻塞函数②该函数的第二个参数是一个判断条件是一个返回值为布尔类型的函数 该参数可以传递一个有名函数的地址也可以直接指定一个匿名函数表达式返回false当前线程被阻塞表达式返回true当前线程不会被阻塞继续向下执行 可以直接传递给wait()函数的互斥锁类型有四种分别是 std::mutex、std::timed_mutex、std::recursive_mutex、std::recursive_timed_mutex如果线程被该函数阻塞这个线程会释放占有的互斥锁的所有权当阻塞解除之后这个线程会重新得到互斥锁的所有权继续向下执行这个过程是在函数内部完成的了解这个过程即可其目的是为了避免线程的死锁。 wait_for()函数和wait()的功能是一样的只不过多了一个阻塞时长假设阻塞的线程没有被其他线程唤醒当阻塞时长用完之后线程就会自动解除阻塞继续向下执行。 template class Lock, class Rep, class Period
cv_status wait_for (Lock lck, const chrono::durationRep,Period rel_time);template class Lock, class Rep, class Period, class Predicate
bool wait_for (Lock lck, const chrono::durationRep,Period rel_time, Predicate pred);wait_until()函数和wait_for()的功能是一样的它是指定让线程阻塞到某一个时间点假设阻塞的线程没有被其他线程唤醒当到达指定的时间点之后线程就会自动解除阻塞继续向下执行。 template class Lock, class Clock, class Duration
cv_status wait_until (Lock lck, const chrono::time_pointClock,Duration abs_time);template class Lock, class Clock, class Duration, class Predicate
bool wait_until (Lock lck, const chrono::time_pointClock,Duration abs_time, Predicate pred);通知函数
void notify_one() noexcept;
void notify_all() noexcept;notify_one()唤醒一个被当前条件变量阻塞的线程
notify_all()唤醒全部被当前条件变量阻塞的线程4.2.2 生产者和消费者模型 使用条件变量condition_variable_any同样可以实现上面的生产者和消费者的例子 代码只有个别细节上有所不同 #include iostream
#include thread
#include mutex
#include list
#include functional
#include condition_variable
using namespace std;class SyncQueue
{
public:SyncQueue(int maxSize) : m_maxSize(maxSize) {}void put(const int x){lock_guardmutex locker(m_mutex);// 根据条件阻塞线程m_notFull.wait(m_mutex, [this]() {return m_queue.size() ! m_maxSize;});// 将任务放入到任务队列中m_queue.push_back(x);cout x 被生产 endl;// 通知消费者去消费m_notEmpty.notify_one();}int take(){lock_guardmutex locker(m_mutex);m_notEmpty.wait(m_mutex, [this]() {return !m_queue.empty();});// 从任务队列中取出任务(消费)int x m_queue.front();m_queue.pop_front();// 通知生产者去生产m_notFull.notify_one();cout x 被消费 endl;return x;}bool empty(){lock_guardmutex locker(m_mutex);return m_queue.empty();}bool full(){lock_guardmutex locker(m_mutex);return m_queue.size() m_maxSize;}int size(){lock_guardmutex locker(m_mutex);return m_queue.size();}private:listint m_queue; // 存储队列数据mutex m_mutex; // 互斥锁condition_variable_any m_notEmpty; // 不为空的条件变量condition_variable_any m_notFull; // 没有满的条件变量int m_maxSize; // 任务队列的最大任务个数
};int main()
{SyncQueue taskQ(50);auto produce bind(SyncQueue::put, taskQ, placeholders::_1);auto consume bind(SyncQueue::take, taskQ);thread t1[3];thread t2[3];for (int i 0; i 3; i){t1[i] thread(produce, i 100);t2[i] thread(consume);}for (int i 0; i 3; i){t1[i].join();t2[i].join();}return 0;
}总结以上介绍的两种条件变量各自有各自的特点
condition_variable 配合 unique_lock 使用更灵活可以在在任何时候自由地释放互斥锁 condition_variable_any 如果和lock_guard 一起使用必须要等到其生命周期结束才能将互斥锁释放。 condition_variable_any 可以和多种互斥锁配合使用应用场景也更广 condition_variable 只能和独占的非递归互斥锁mutex配合使用有一定的局限性。 5. 原子变量
C11提供了一个原子类型std::atomicT通过这个原子类型管理的内部变量就可以称之为原子变量我们可以给原子类型指定bool、char、int、long、指针等类型作为模板参数不支持浮点类型和复合类型。
原子指的是一系列不可被CPU上下文交换的机器指令这些指令组合在一起就形成了原子操作。在多核CPU下当某个CPU核心开始运行原子操作时会先暂停其它CPU内核对内存的操作以保证原子操作不会被其它CPU内核所干扰。
由于原子操作是通过指令提供的支持因此它的性能相比锁和消息传递会好很多。 相比较于锁而言原子类型不需要开发者处理加锁和释放锁的问题同时支持修改读取等操作还具备较高的并发性能几乎所有的语言都支持原子类型。
可以看出原子类型是无锁类型但是无锁不代表无需等待因为原子类型内部使用了CAS循环当大量的冲突发生时该等待还是得等待,但是总归比锁要好。
C11内置了整形的原子变量这样就可以更方便的使用原子变量了。在多线程操作中使用原子变量之后就不需要再使用互斥量来保护该变量了用起来更简洁。 因为对原子变量进行的操作只能是一个原子操作atomic operation原子操作指的是不会被线程调度机制打断的操作这种操作一旦开始就一直运行到结束中间不会有任何上下文切换。 多线程同时访问共享资源造成数据混乱的原因就是因为CPU的上下文切换导致的使用原子变量解决了这个问题因此互斥锁的使用也就不再需要了。 CAS全称是Compare and swap 它通过一条指令读取指定的内存地址然后判断其中的值是否等于给定的前置值 如果相等则将其修改为新的值 5.1 atomic 类成员
类定义
// 定义于头文件 atomic
template class T
struct atomic;通过定义可得知在使用这个模板类的时候一定要指定模板类型。 5.1.1 构造函数
// 1
atomic() noexcept default;
// 2
constexpr atomic(T desired) noexcept;
// 3
atomic(const atomic) delete;构造函数①默认无参构造函数。构造函数②使用 desired 初始化原子变量的值。构造函数③使用delete显示删除拷贝构造函数, 不允许进行对象之间的拷贝 5.1.2 公共成员函数 原子类型在类内部重载了操作符并且不允许在类的外部使用 进行对象的拷贝。 T operator( T desired ) noexcept;
T operator( T desired ) volatile noexcept;atomic operator( const atomic ) delete;
atomic operator( const atomic ) volatile delete;以 desired 替换当前值。按照 order 的值影响内存。 void store( T desired, std::memory_order order std::memory_order_seq_cst ) noexcept;
void store( T desired, std::memory_order order std::memory_order_seq_cst ) volatile noexcept;desired存储到原子变量中的值order强制的内存顺序 原子地加载并返回原子变量的当前值。按照 order 的值影响内存。直接访问原子对象也可以得到原子变量的当前值。 T load(std::memory_order order std::memory_order_seq_cst) const noexcept;
T load(std::memory_order order std::memory_order_seq_cst) const volatile noexcept;5.1.3 特化成员函数
复合赋值运算符重载主要包含以下形式
当模板类型T为整数
操作符重载描述T operator (T val) volatile noexcept;原子地执行加法并赋值返回新的值 (volatile版本)T operator (T val) noexcept;原子地执行加法并赋值返回新的值T operator- (T val) volatile noexcept;原子地执行减法并赋值返回新的值 (volatile版本)T operator- (T val) noexcept;原子地执行减法并赋值返回新的值T operator (T val) volatile noexcept;原子地执行按位与操作并赋值返回新的值 (volatile版本)T operator (T val) noexcept;原子地执行按位与操作并赋值返回新的值T operator (T val) volatile noexcept;T operator (T val) noexcept;T operator^ (T val) volatile noexcept;原子地执行按位异或操作并赋值返回新的值 (volatile版本)T operator^ (T val) noexcept;原子地执行按位异或操作并赋值返回新的值
当模板类型T为指针
操作符重载描述T operator (ptrdiff_t val) volatile noexcept;原子地执行指针加法并赋值返回新的指针 (volatile版本)T operator (ptrdiff_t val) noexcept;原子地执行指针加法并赋值返回新的指针T operator- (ptrdiff_t val) volatile noexcept;原子地执行指针减法并赋值返回新的指针 (volatile版本)T operator- (ptrdiff_t val) noexcept;原子地执行指针减法并赋值返回新的指针
以上各个 operator 都会有对应的 fetch_* 操作详细见下表
操作符操作符重载函数等级的成员函数整形指针其他atomic::operatoratomic::fetch_add是是否-atomic::operator-atomic::fetch_sub是是否atomic::operatoratomic::fetch_and是否否|atomic::operator|atomic::fetch_or是否否^atomic::operator^atomic::fetch_xor是否否 5.1.4 内存顺序约束 通过上面的 API 函数我们可以看出在调用 atomic类提供的 API 函数的时候需要指定原子顺序 在C11给我们提供的 API中使用枚举用作执行原子操作的函数的实参以指定如何同步不同线程上的其他操作。 定义如下:
typedef enum memory_order {memory_order_relaxed, // relaxedmemory_order_consume, // consumememory_order_acquire, // acquirememory_order_release, // releasememory_order_acq_rel, // acquire/releasememory_order_seq_cst // sequentially consistent
} memory_order;memory_order_relaxed,这是最宽松的规则它对编译器和CPU不做任何限制可以乱序memory_order_release 释放设定内存屏障(Memory barrier)保证它之前的操作永远在它之前但是它后面的操作可能被重排到它前面memory_order_acquire 获取, 设定内存屏障保证在它之后的访问永远在它之后但是它之前的操作却有可能被重排到它后面往往和Release在不同线程中联合使用memory_order_consume改进版的memory_order_acquire 开销更小memory_order_acq_rel它是Acquire 和 Release 的结合同时拥有它们俩提供的保证。比如你要对一个 atomic 自增 1同时希望该操作之前和之后的读取或写入操作不会被重新排序memory_order_seq_cst 顺序一致性 memory_order_seq_cst 就像是memory_order_acq_rel的加强版它不管原子操作是属于读取还是写入的操作 只要某个线程有用到memory_order_seq_cst 的原子操作线程中该memory_order_seq_cst 操作前的数据操作绝对不会被重新排在该memory_order_seq_cst 操作之后且该memory_order_seq_cst 操作后的数据操作也绝对不会被重新排在memory_order_seq_cst 操作前。
5.1.5 C20新增成员 在C20版本中添加了新的功能函数可以通过原子类型来阻塞线程和条件变量中的等待/通知函数是一样的。 公共成员函数说明wait (C20)阻塞线程直至被提醒且原子值更改notify_one (C20)通知唤醒至少一个在原子对象上阻塞的线程notify_all (C20)通知唤醒所有在原子对象上阻塞的线程
类型别名
别名原始类型定义atomic_bool (C11)std::atomicboolatomic_char (C11)std::atomiccharatomic_schar (C11)std::atomicsigned charatomic_uchar (C11)std::atomicunsigned charatomic_short (C11)std::atomicshortatomic_ushort (C11)std::atomicunsigned shortatomic_int (C11)std::atomicintatomic_uint (C11)std::atomicunsigned intatomic_long (C11)std::atomiclongatomic_ulong (C11)std::atomicunsigned longatomic_llong (C11)std::atomiclong longatomic_ullong (C11)std::atomicunsigned long longatomic_char8_t (C20)std::atomicchar8_tatomic_char16_t (C11)std::atomicchar16_tatomic_char32_t (C11)std::atomicchar32_tatomic_wchar_t (C11)std::atomicwchar_tatomic_int8_t (C11)std::atomicstd::int8_tatomic_uint8_t (C11)std::atomicstd::uint8_tatomic_int16_t (C11)std::atomicstd::int16_tatomic_uint16_t (C11)std::atomicstd::uint16_tatomic_int32_t (C11)std::atomicstd::int32_tatomic_uint32_t (C11)std::atomicstd::uint32_tatomic_int64_t (C11)std::atomicstd::int64_tatomic_uint64_t (C11)std::atomicstd::uint64_tatomic_int_least8_t (C11)std::atomicstd::int_least8_tatomic_uint_least8_t (C11)std::atomicstd::uint_least8_tatomic_int_least16_t (C11)std::atomicstd::int_least16_tatomic_uint_least16_t (C11)std::atomicstd::uint_least16_tatomic_int_least32_t (C11)std::atomicstd::int_least32_tatomic_uint_least32_t (C11)std::atomicstd::uint_least32_tatomic_int_least64_t (C11)std::atomicstd::int_least64_tatomic_uint_least64_t (C11)std::atomicstd::uint_least64_tatomic_int_fast8_t (C11)std::atomicstd::int_fast8_tatomic_uint_fast8_t (C11)std::atomicstd::uint_fast8_tatomic_int_fast16_t (C11)std::atomicstd::int_fast16_tatomic_uint_fast16_t (C11)std::atomicstd::uint_fast16_tatomic_int_fast32_t (C11)std::atomicstd::int_fast32_tatomic_uint_fast32_t (C11)std::atomicstd::uint_fast32_tatomic_int_fast64_t (C11)std::atomicstd::int_fast64_tatomic_uint_fast64_t (C11)std::atomicstd::uint_fast64_tatomic_intptr_t (C11)std::atomicstd::intptr_tatomic_uintptr_t (C11)std::atomicstd::uintptr_tatomic_size_t (C11)std::atomicstd::size_tatomic_ptrdiff_t (C11)std::atomicstd::ptrdiff_tatomic_intmax_t (C11)std::atomicstd::intmax_tatomic_uintmax_t (C11)std::atomicstd::uintmax_t 5.2 原子变量的使用 假设我们要制作一个多线程交替数数的计数器我们使用互斥锁和原子变量的方式分别进行实现对比一下二者的差异 5.2.1 互斥锁版本
#include iostream
#include thread
#include mutex
#include atomic
#include functional
using namespace std;struct Counter
{void increment(){for (int i 0; i 10; i){lock_guardmutex locker(m_mutex);m_value;cout increment number: m_value , theadID: this_thread::get_id() endl;this_thread::sleep_for(chrono::milliseconds(100));}}void decrement(){for (int i 0; i 10; i){lock_guardmutex locker(m_mutex);m_value--;cout decrement number: m_value , theadID: this_thread::get_id() endl;this_thread::sleep_for(chrono::milliseconds(100));}}int m_value 0;mutex m_mutex;
};int main()
{Counter c;auto increment bind(Counter::increment, c);auto decrement bind(Counter::decrement, c);thread t1(increment);thread t2(decrement);t1.join();t2.join();return 0;
}5.2.2 原子变量版本
#include iostream
#include thread
#include atomic
#include functional
using namespace std;struct Counter
{void increment(){for (int i 0; i 10; i){m_value;cout increment number: m_value , theadID: this_thread::get_id() endl;this_thread::sleep_for(chrono::milliseconds(500));}}void decrement(){for (int i 0; i 10; i){m_value--;cout decrement number: m_value , theadID: this_thread::get_id() endl;this_thread::sleep_for(chrono::milliseconds(500));}}// atomicint atomic_intatomic_int m_value 0;
};int main()
{Counter c;auto increment bind(Counter::increment, c);auto decrement bind(Counter::decrement, c);thread t1(increment);thread t2(decrement);t1.join();t2.join();return 0;
}通过代码的对比可以看出使用了原子变量之后就不需要再定义互斥量了在使用上更加简便并且这两种方式都能保证在多线程操作过程中数据的正确性不会出现数据的混乱。
原子类型atomicT 可以封装原始数据最终得到一个原子变量对象操作原子对象能够得到和操作原始数据一样的效果当然也可以通过store()和load()来读写原子对象内部的原始数据。 6. 多线程异步操作
6.1 std:future C11中增加的线程类使得我们能够非常方便的创建和使用线程 但有时会有些不方便比如需要获取线程返回的结果就不能通过join()得到结果只能通过一些额外手段获得比如定义一个全局变量在子线程中赋值在主线程中读这个变量的值整个过程比较繁琐。 C提供的线程库中提供了一些类用于访问异步操作的结果。
那么什么叫做异步呢
我们去星巴克买咖啡因为都是现磨的所以需要等待但是我们付完账后不会站在柜台前死等而是去找个座位坐下来玩玩手机打发一下时间当店员把咖啡磨好之后就会通知我们过去取这就叫做异步。
顾客主线程发起一个任务子线程磨咖啡磨咖啡的过程中顾客去做别的事情了有两条时间线异步顾客主线程发起一个任务子线程磨咖啡磨咖啡的过程中顾客没去做别的事情而是死等这时就只有一条时间线同步此时效率相对较低。
因此多线程程序中的任务大都是异步的主线程和子线程分别执行不同的任务如果想要在主线中得到某个子线程任务函数返回的结果可以使用C11提供的std:future类这个类需要和其他类或函数搭配使用
先来介绍一下这个类的API函数
类的定义
通过类的定义可以得知future是一个模板类也就是这个类可以存储任意指定类型的数据。
// 定义于头文件 future
template class T class future;
template class T class futureT;
template class futurevoid;构造函数
// 1
future() noexcept;
// 2
future( future other ) noexcept;
// 3
future( const future other ) delete;构造函数①默认无参构造函数构造函数②移动构造函数转移资源的所有权构造函数③使用delete显示删除拷贝构造函数, 不允许进行对象之间的拷贝
常用成员函数public) 一般情况下使用进行赋值操作就进行对象的拷贝但是future对象不可用复制 因此会根据实际情况进行处理 如果other是右值那么转移资源的所有权如果other是非右值不允许进行对象之间的拷贝该函数被显示删除禁止使用
future operator( future other ) noexcept;
future operator( const future other ) delete;取出future对象内部保存的数据其中void get()是为futurevoid准备的 此时对象内部类型就是void该函数是一个阻塞函数当子线程的数据就绪后解除阻塞就能得到传出的数值了。 T get();
T get();
void get();因为future对象内部存储的是异步线程任务执行完毕后的结果是在调用之后的将来得到的因此可以通过调用wait()方法阻塞当前线程等待这个子线程的任务执行完毕任务执行完毕当前线程的阻塞也就解除了。 //函数原型
void wait() const;wait()方法就会死等直到子线程任务执行完毕将返回值写入到future对象中 wait_for()只会让线程阻塞一定的时长但是这样并不能保证对应的那个子线程中的任务已经执行完毕了。 wait_until()和wait_for()函数功能是差不多 前者是阻塞到某一指定的时间点后者是阻塞一定的时长。
template class Rep, class Period
std::future_status wait_for( const std::chrono::durationRep,Period timeout_duration ) const;template class Clock, class Duration
std::future_status wait_until( const std::chrono::time_pointClock,Duration timeout_time ) const;当wait_until()和wait_for()函数返回之后并不能确定子线程当前的状态因此我们需要判断函数的返回值这样就能知道子线程当前的状态了
常量解释future_status::deferred子线程中的任务函仍未启动future_status::ready子线程中的任务已经执行完毕结果已就绪future_status::timeout子线程中的任务正在执行中指定等待时长已用完 6.2 std::promise std::promise是一个协助线程赋值的类它能够将数据和future对象绑定起来为获取线程函数中的某个值提供便利。 6.2.1 类成员函数
类定义
通过std::promise类的定义可以得知这也是一个模板类我们要在线程中传递什么类型的数据模板参数就指定为什么类型。
// 定义于头文件 future
template class R class promise;
template class R class promiseR;
template class promisevoid;构造函数
// ①
promise();
// ②
promise( promise other ) noexcept;
// ③
promise( const promise other ) delete;构造函数①默认构造函数得到一个空对象构造函数②移动构造函数构造函数③使用delete显示删除拷贝构造函数, 不允许进行对象之间的拷贝
公共成员函数 在std::promise类内部管理着一个future类对象调用get_future()就可以得到这个future对象了 //函数原型
std::futureT get_future();存储要传出的 value 值并立即让状态就绪这样数据被传出其它线程就可以得到这个数据了。重载的第四个函数是为promisevoid类型的对象准备的。
void set_value( const R value );
void set_value( R value );
void set_value( R value );
void set_value();存储要传出的 value 值但是不立即令状态就绪。在当前线程退出时子线程资源被销毁再令状态就绪。
void set_value_at_thread_exit( const R value );
void set_value_at_thread_exit( R value );
void set_value_at_thread_exit( R value );
void set_value_at_thread_exit();6.2.2 promise的使用
通过promise传递数据的过程一共分为5步
在主线程中创建std::promise对象将这个std::promise对象通过引用的方式传递给子线程的任务函数在子线程任务函数中给std::promise对象赋值在主线程中通过std::promise对象取出绑定的future实例对象通过得到的future对象取出子线程任务函数中返回的值。
子线程任务函数执行期间让状态就绪
#include iostream
#include thread
#include future
using namespace std;int main()
{promiseint pr;thread t1([](promiseint p) {p.set_value(100);this_thread::sleep_for(chrono::seconds(3));cout 睡醒了.... endl;}, ref(pr));futureint f pr.get_future();int value f.get();cout value: value endl;t1.join();return 0;
}示例程序输出的结果
value: 100
睡醒了....示例程序的中子线程的任务函数指定的是一个匿名函数在这个匿名的任务函数执行期间通过p.set_value(100);传出了数据并且激活了状态 数据就绪后外部主线程中的int value f.get();解除阻塞并将得到的数据打印出来5秒钟之后子线程休眠结束匿名的任务函数执行完毕。
子线程任务函数执行结束让状态就绪
#include iostream
#include thread
#include future
using namespace std;int main()
{promiseint pr;thread t1([](promiseint p) {p.set_value_at_thread_exit(100);this_thread::sleep_for(chrono::seconds(3));cout 睡醒了.... endl;}, ref(pr));futureint f pr.get_future();int value f.get();cout value: value endl;t1.join();return 0;
}示例程序输出的结果
睡醒了....
value: 100在示例程序中子线程的这个匿名的任务函数中通过p.set_value_at_thread_exit(100);在执行完毕并退出之后才会传出数据并激活状态 数据就绪后外部主线程中的int value f.get();解除阻塞并将得到的数据打印出来因此子线程在休眠5秒钟之后主线程中才能得到传出的数据。
注意:在外部主线程中创建的promise对象必须要通过引用的方式传递到子线程的任务函数中在实例化子线程对象的时候如果任务函数的参数是引用类型那么实参一定要放到std::ref()函数中表示要传递这个实参的引用到任务函数中。 6.3. std::packaged_task std::packaged_task类包装了一个可调用对象包装器类对象可调用对象包装器包装的是可调用对象可调用对象都可以作为函数来使用恶补一下可调用对象和可调用对象包装器 这个类可以将内部包装的函数和future类绑定到一起,以便进行后续的异步调用,和std::promise有点类似 std::promise内部保存一个共享状态的值而std::packaged_task保存的是一个函数。 6.3.1 类成员函数
类的定义
通过类的定义可以看到这也是一个模板类模板类型和要在线程中传出的数据类型是一致的。
// 定义于头文件 future
template class class packaged_task;
template class R, class ...Args
class packaged_taskR(Args...);构造函数
// ①
packaged_task() noexcept;
// ②
template class F
explicit packaged_task( F f );
// ③
packaged_task( const packaged_task ) delete;
// ④
packaged_task( packaged_task rhs ) noexcept;构造函数①无参构造构造一个无任务的空对象构造函数②通过一个可调用对象构造一个任务对象构造函数③显示删除不允许通过拷贝构造函数进行对象的拷贝构造函数④移动构造函数
常用公共成员函数
通过调用任务对象内部的get_future()方法就可以得到一个future对象基于这个对象就可以得到传出的数据了。
//函数原型
std::futureR get_future();6.3.2 packaged_task的使用 packaged_task其实就是对子线程要执行的任务函数进行了包装和可调用对象包装器的使用方法相同包装完毕之后直接将包装得到的任务对象传递给线程对象就可以了。 #include iostream
#include thread
#include future
using namespace std;int main()
{packaged_taskint(int) task([](int x) {return x 100;});thread t1(ref(task), 100);futureint f task.get_future();int value f.get();cout value: value endl;t1.join();return 0;
}在上面的示例代码中通过packaged_task类包装了一个匿名函数作为子线程的任务函数最终的得到的这个任务对象需要通过引用的方式传递到子线程内部这样才能在主线程的最后通过任务对象得到future对象再通过这个future对象取出子线程通过返回值传递出的数据。 6.4 std::async std::async函数比前面提到的std::promise和packaged_task更高级一些 因为通过这函数可以直接启动一个子线程并在这个子线程中执行对应的任务函数异步任务执行完成返回的结果也是存储到一个future对象中 当需要获取异步任务的结果时只需要调用future类的get()方法即可 如果不关注异步任务的结果只是简单地等待任务完成的话可以调用future类的wait()或者wait_for()方法。 // 函数原型
// 定义于头文件 future
// 1
template class Function, class... Args
std::futurestd::result_of_tstd::decay_tFunction(std::decay_tArgs...)async( Function f, Args... args );// 2
template class Function, class... Args
std::futurestd::result_of_tstd::decay_tFunction(std::decay_tArgs...)async( std::launch policy, Function f, Args... args );可以看到这是一个模板函数在C11中这个函数有两种调用方式 函数①直接调用传递到函数体内部的可调用对象返回一个future对象 函数②通过指定的策略调用传递到函数内部的可调用对象返回一个future对象 函数参数: f可调用对象这个对象在子线程中被作为任务函数使用 Args传递给 f 的参数实参 policy可调用对象·f的执行策略
策略说明std::launch::async调用async函数时创建新的线程执行任务函数std::launch::deferred调用async函数时不执行任务函数直到调用了future的get()或者wait()时才执行任务这种方式不会创建新的线程
关于std::async()函数的使用对应的示例代码如下
6.4.1 方式1
调用async()函数直接创建线程执行任务
#include iostream
#include thread
#include future
using namespace std;int main()
{cout 主线程ID: this_thread::get_id() endl;// 调用函数直接创建线程执行任务futureint f async([](int x) {cout 子线程ID: this_thread::get_id() endl;this_thread::sleep_for(chrono::seconds(5));return x 100;}, 100);future_status status;do {status f.wait_for(chrono::seconds(1));if (status future_status::deferred){cout 线程还没有执行... endl;f.wait();}else if (status future_status::ready){cout 子线程返回值: f.get() endl;}else if (status future_status::timeout){cout 任务还未执行完毕, 继续等待... endl;}} while (status ! future_status::ready);return 0;
}示例程序输出的结果为
主线程ID: 8904
子线程ID: 25036
任务还未执行完毕, 继续等待...
任务还未执行完毕, 继续等待...
任务还未执行完毕, 继续等待...
任务还未执行完毕, 继续等待...
任务还未执行完毕, 继续等待...
子线程返回值: 200调用async()函数时不指定策略就是直接创建线程并执行任务,示例代码的主线程中做了如下操作 status f.wait_for(chrono::seconds(1)); 其实直接调用f.get()就能得到子线程的返回值。 为了演示wait_for()的使用所以写的复杂了些。 6.4.2 方式2
调用async()函数不创建线程执行任务
#include iostream
#include thread
#include future
using namespace std;int main()
{cout 主线程ID: this_thread::get_id() endl;// 调用函数直接创建线程执行任务futureint f async(launch::deferred, [](int x) {cout 子线程ID: this_thread::get_id() endl;return x 100;}, 100);this_thread::sleep_for(chrono::seconds(5));cout f.get();return 0;
}示例程序输出的结果
主线程ID: 24760
主线程开始休眠5秒...
子线程ID: 24760
200由于指定了launch::deferred 策略因此调用async()函数并不会创建新的线程执行任务 当使用future类对象调用了get()或者wait()方法后才开始执行任务此处一定要注意调用wait_for()函数是不行的。
通过测试程序输出的结果可以看到两次输出的线程ID是相同的任务函数是在主线程中被延迟主线程休眠了5秒调用了。
最终总结
使用async()函数是多线程操作中最简单的一种方式不需要自己创建线程对象并且可以得到子线程函数的返回值。使用std::promise类在子线程中可以传出返回值也可以传出其他数据并且可选择在什么时机将数据从子线程中传递出来使用起来更灵活。使用std::packaged_task类可将子线程的任务函数进行包装并可得到子线程返回值。