重庆网站推广免费软件,设计公司网站 唐山,免费crm在线看系统,连云区住房和城乡建设局网站1. thread — 线程篇
所需头文件#xff1a;thread
1.1 构造函数
// 1 默认构造函数
thread() noexcept;
// 2 移动构造函数#xff0c;把other的所有权转移给新的thread对象#xff0c;之后 other 不再表示执行线程。
thread( thread other ) noex…1. thread — 线程篇
所需头文件thread
1.1 构造函数
// 1 默认构造函数
thread() noexcept;
// 2 移动构造函数把other的所有权转移给新的thread对象之后 other 不再表示执行线程。
thread( thread other ) noexcept;
// 3 f参数可选普通函数类成员函数匿名函数仿函数
template class Function, class... Args
explicit thread( Function f, Args... args );
// 4 使用 delete 显示删除拷贝构造, 不允许线程对象之间的拷贝
thread( const thread ) delete;第三种创建 创建线程对象并在该线程中执行函数 f 中的业务逻辑args 是要传递给函数 f 的参数 任务函数 f 的可选类型有很多具体如下
普通函数类成员函数匿名函数仿函数这些都是可调用对象类型可以是可调用对象包装器类型也可以是使用绑定器绑定之后得到的类型仿函数类成员函/变量作参数thread(类函数/成员地址, 类实例对象地址, 类函数参数)参数列表类似于 std::bind()博客最后有实例
f 一般返回值指定为 void因为子线程在调用这个函数的时候不会处理其返回值。
1.2 公共成员函数
get_id()
应用程序启动之后默认只有一个线程这个线程一般称之为主线程或父线程通过线程类创建出的线程一般称之为子线程每个被创建出的线程实例都对应一个线程ID这个ID是唯一的可以通过这个ID来区分和识别各个已经存在的线程实例。
调用命名空间std::this_thread中的get_id()方法可以得到当前线程的线程ID在主函数中调用即可得到主线程ID在线程中调用即可得到子线程ID。
void func(){cout 子线程: this_thread::get_id() endl;
}
// main()
cout 主线程: this_thread::get_id() endl;
thread t(func); // 创建子线程
t.get_id(); // 在主函数中获得子线程ID
t.join();当启动了一个线程创建了一个thread对象之后在这个线程结束的时候std::terminate()我们如何去回收线程所使用的资源呢thread库给我们两种选择
加入式join()分离式detach()
join()
join()字面意思是连接一个线程意味着主动地等待线程的终止线程阻塞。在某个线程中通过子线程对象调用join()函数调用这个函数的线程被阻塞但是子线程对象中的任务函数会继续执行当任务执行完毕之后join()会清理当前子线程中的相关资源然后返回同时调用该函数的线程解除阻塞继续向下执行。join()函数在哪个线程中被执行那么哪个线程就被阻塞。
// 子线程
void func(){cout 子线程 endl;
}
// 主线程
cout 主线程 endl;
thread t(func); // 创建子线程
t.join(); // main线程中调用线程 t 的join函数main线程随之被阻塞不会向下执行
int x 1;调用 join() 有两种情况
如果任务函数func()还没执行完毕主线程阻塞直到子线程执行完毕主线程解除阻塞继续向下运行如果任务函数func()已经执行完毕主线程不会阻塞继续向下运行
总的来说t.join() 语句使得只有在子线程 t 完成执行之后才能执行 int x 1; 语句。
线程执行完毕后join() 会清理回收当前子线程的相关资源
detach()
detach() 函数的作用是进行线程分离分离主线程和创建出的子线程。在线程分离之后主线程退出也会一并销毁创建出的所有子线程在主线程退出之前它可以脱离主线程继续独立的运行任务执行完毕之后这个子线程会自动释放自己占用的系统资源。 注意事项线程分离函数detach()不会阻塞线程子线程和主线程分离之后在主线程中就不能再对这个子线程做任何控制了比如调用get_id()获取子线程的线程ID。
joinable()
joinable() 函数用于判断主线程和子线程是否处理关联连接状态一般情况下二者之间的关系处于关联状态该函数返回一个布尔类型有连接关系返回值为 true否则返回 false。 实例
void foo(){cout thread starts endl;
}thread t; // 在创建的子线程对象的时候如果没有指定任务函数那么子线程不会启动主线程和这个子线程也不会进行连接
cout before starting, joinable: t.joinable() endl; // 0t thread(foo); // 指定了任务函数子线程启动并执行任务主线程和这个子线程自动连接成功
cout after starting, joinable: t.joinable() endl;// 1t.join();// 线程t任务处理完毕后这时join()会清理回收当前子线程的相关资源所以这个子线程和主线程的连接也就断开了
// 因此调用join()之后再调用joinable()会返回false。
cout after joining, joinable: t.joinable() endl;// 0thread t1(foo);// 在创建的子线程对象的时候如果指定了任务函数子线程启动并执行任务主线程和这个子线程自动连接成功
cout after starting, joinable: t1.joinable() endl;// 1
t1.detach();// 子线程调用了detach()函数之后父子线程分离同时二者的连接断开调用joinable()返回false
cout after detaching, joinable: t1.joinable() endl;// 0operator
线程中的资源是不能被复制的 因此通过操作符进行赋值操作最终并不会得到两个完全相同的对象。
// move (1)
thread operator (thread other) noexcept;
// copy [deleted] (2)
thread operator (const other) delete;实例
void foo(){cout thread starts endl;
}thread t1(foo); // 因为下边有资源所有权转移这里的t1线程并不会执行
t1.join(); // error, 同一份资源不能回收两次会抛出异常
thread t2(move(t1)); // 将线程t1的资源所有权的转移给t2
t2.join(); // t2线程执行完进行join() 会进行资源回收1.3 静态函数(获取CPU核心数)
thread 线程类还提供了一个静态方法用于获取当前计算机的CPU核心数根据这个结果在程序中创建出数量相等的线程每个线程独自占有一个CPU核心这些线程就不用分时复用CPU时间片此时程序的并发效率是最高的。
int num thread::hardware_concurrency();
cout CPU number: num endl;1.4 命名空间 - this_thread
get_id()
关于命名空间里的 get_id() 前别已经涉及过这里就不再赘述。 简答来说直接调用可以得到当前线程的线程IDthis_thread::get_id()
sleep_for()
进程创建完成后有 5 种状态同样地线程被创建后也有这五种状态创建态就绪态运行态阻塞态(挂起态)退出态(终止态) 关于状态之间的转换是一样的。
命名空间this_thread中提供了一个休眠函数sleep_for()调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长因为阻塞态的线程已经让出了CPU资源代码也不会被执行所以线程休眠过程中对CPU来说没有任何负担。 参数需要指定一个休眠时长一般配合chrono 库使用时间长度 duration类型
#include chrono
#include thread
void func(){this_thread::sleep_for(chrono::seconds(1)); // 当前线程进入阻塞态1s
}
thread t(func);
t.join();sleep_until()
指定线程到某一个指定的时间点time_point类型之后解除阻塞 作用其实和sleep_for() 差不多
void func(){auto now chrono::system_clock::now();// 获取当前系统时间点chrono::seconds sec(2);// 时间间隔为2sthis_thread::sleep_until(now sec);// 当前时间点之后休眠两秒
}
thread t(func);
t.join();yeild()
调用yeild函数会使处于运行态的线程会主动让出自己已经抢到的CPU时间片最终变为就绪态 使用这个函数的时候需要注意一点线程调用了yield()之后会主动放弃CPU资源但是这个变为就绪态的线程会马上参与到下一轮CPU的抢夺战中不排除它能继续抢到CPU时间片的情况这是概率问题。 结论
std::this_thread::yield() 的目的是避免一个线程长时间占用CPU资源从而导致多线程处理性能下降std::this_thread::yield() 是让当前线程主动放弃了当前自己抢到的CPU资源但是在下一轮还会继续抢
2. 线程同步篇
2.1 call_once
在某些特定情况下某些函数只能在多线程环境下调用一次比如要初始化某个对象而这个对象只能被初始化一次就可以使用std::call_once()来保证函数在多线程环境下只能被调用一次。使用call_once()的时候需要一个once_flag作为call_once()的传入参数。
once_flag g_flag; // 全局定义
std::call_once(once_flag flag, 回调函数回调函数的参数);多线程操作过程中std::call_once() 内部的回调函数只会被执行一次
#include mutexonce_flag g_flag;
void do_once(int a){cout age: a endl;
}
void do_something(int age){static int num 1;call_once(g_flag, do_once, 19);cout do_something() function num num endl;
}thread t1(do_something, 20);
thread t2(do_something, 19);
t1.join();
t2.join();虽然运行的两个线程中都执行了任务函数do_something()但是call_once()中指定的回调函数只被执行了一次。
2.1 利用互斥锁mutex进行线程同步
进行多线程编程如果多个线程需要对同一块内存进行操作比如同时读、同时写、同时读写对于后两种情况来说如果不做任何的人为干涉就会出现各种各样的错误数据。这是因为线程在运行的时候需要先得到CPU时间片时间片用完之后需要放弃已获得的CPU资源这种并发执行是 “执行–简短–执行” 的间断性活动并且由于多线程可以共享变量这样失去了封闭性导致运行的结果具有不可再现性。 解决多线程数据混乱的方案就是进行线程同步最常用的就是互斥锁在C11中一共提供了四种互斥锁
std::mutex独占的互斥锁不能递归使用std::timed_mutex带超时的独占互斥锁不能递归使用std::recursive_mutex递归互斥锁不带超时功能std::recursive_timed_mutex带超时的递归互斥锁
互斥锁在有些资料中也被称之为互斥量二者是一个东西。
线程同步大概思路
使用互斥锁进行线程同步的主要分为以下几步
1找到多个线程操作的共享资源全局变量、堆内存、类成员变量等也可以称之为临界资源2找到和共享资源有关的上下文代码也就是临界区下图中的黄色代码部分3在临界区的上边调用互斥锁类的lock()方法4在临界区的下边调用互斥锁的unlock()方法5线程同步的目的是让多线程按照顺序依次执行临界区代码这样做线程对共享资源的访问就从并行访问变为了线性访问访问效率降低了但是保证了数据的正确性。
std::mutex普通互斥锁
lock()
lock()函数用于给临界区加锁并且只能有一个线程获得锁的所有权它有阻塞线程的作用。
独占互斥锁对象有两种状态锁定和未锁定。如果互斥锁是打开的调用lock()函数的线程会得到互斥锁的所有权并将其上锁其它线程再调用该函数的时候由于得不到互斥锁的所有权就会被lock()函数阻塞。当拥有互斥锁所有权的线程将互斥锁解锁此时被lock()阻塞的线程解除阻塞抢到互斥锁所有权的线程加锁并继续运行没抢到互斥锁所有权的线程继续阻塞。
try_lock()
除了使用lock()还可以使用try_lock()获取互斥锁的所有权并对互斥锁加锁二者的区别在于try_lock() 不会阻塞线程lock() 会阻塞线程
如果互斥锁是未锁定状态得到了互斥锁所有权并加锁成功函数返回 true如果互斥锁是锁定状态无法得到互斥锁所有权加锁失败函数返回 false
try_lock() 如果被调用时没有获得锁则直接返回 false一个尝试动作返回 false 不会阻塞线程。
unlock()
当互斥锁被锁定之后可以通过unlock()进行解锁但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁其它线程是没有权限做这件事情的。
当线程对互斥锁对象加锁并且执行完临界区代码之后一定要使用这个线程对互斥锁解锁否则最终会造成线程的死锁。死锁之后当前应用程序中的所有线程都会被阻塞并且阻塞无法解除应用程序也无法继续运行。
线程同步实例
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));}
}thread t1(slow_increment, 0);
thread t2(slow_increment, 1);
t1.join();
t2.join();另外需要注意一点
在所有线程的任务函数执行完毕之前互斥锁对象是不能被析构的一定要在程序中保证这个对象的可用性。互斥锁的个数和共享资源的个数相等也就是说每一个共享资源都应该对应一个互斥锁对象。互斥锁对象的个数和线程的个数没有关系。
std::lock_guard 模板类简化互斥锁写法
lock_guard 是C11新增的一个模板类使用这个类可以简化互斥锁lock()和unlock()的写法同时也更安全。 lock_guard 工作原理在lock_guard的构造函数里调用了mutex的lock()成员函数在lock_guard的析构函数里调用了mutex的unlock()成员函数。 也就是lock_guard在使用上面提供的这个构造函数构造对象时会自动锁定互斥量而在退出作用域后进行析构时就会自动解锁从而保证了互斥量的正确操作避免忘记unlock()操作而导致线程死锁。 lock_guard 使用了RAII技术就是在类构造函数中分配资源在析构函数中释放资源保证资源出了作用域就释放。
lock_guardmutex lock(g_num_mutex); // 用已经定义好的锁初始化类模板int g_num 0; // 为 g_num_mutex 所保护
mutex g_num_mutex;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));}
}thread t1(slow_increment, 0);
thread t2(slow_increment, 1);
t1.join();
t2.join();这种方式看起来方便但是也有弊端在上面的示例程序中整个for循环的体都被当做了临界区多个线程是线性的执行临界区代码的因此临界区越大程序效率越低。把一次for循环的所有资源当成临界区而mutex可以只锁定一部分为临界区。
std::unique_lock 比 lock_guard 更灵活
unique_lock 是一个类模板它与lock_guard 一样在用互斥锁初始化完之后自动进行加锁解锁但是比lock_guard 更灵活。lock_guard 里边并没有封装功能函数但是 unique_lock 里边封装了很多函数。
// 无第二参数
unique_lockmutex u_mutex(g_num_mutex);// 如果拿不到锁就一直卡在这执行流程不往下走unique_lockmutex u_mutex(g_num_mutex第二参数);这里的g_num_mutex 是 mutex 类型的变量名
unique_lock 的第二参数有三个
std::adopt_lock作用暂时没弄懂std::try_to_lock初始化时尝试加锁如果没有成功则立刻返回不进行阻塞std::defer_lock初始化完并不加锁可自行决定加锁时机 int g_num 0; // 为 g_num_mutex 所保护
mutex g_num_mutex;
void test() {unique_lockmutex u_mutex(g_num_mutex, std::defer_lock); // 初始化完并没有加锁u_mutex.lock(); // 可以自行加锁g_num;u_mutex.unlock();// 可删除的语句unique_lock能够自动解锁也可以手动解锁
}std::recursive_mutex允许互斥锁递归
递归互斥锁std::recursive_mutex允许同一线程多次获得互斥锁可以用来解决同一线程需要多次获取互斥量时死锁的问题在下面的例子中使用独占非递归互斥量会发生死锁
struct Calculate{Calculate() : m_i(6) {}void mul(int x){//lock_guardmutex locker(m_mutex);lock_guardrecursive_mutex locker(m_mutex);m_i * x;}void div(int x){//lock_guardmutex locker(m_mutex);lock_guardrecursive_mutex locker(m_mutex);m_i / x;}void both(int x, int y){//lock_guardmutex locker(m_mutex);lock_guardrecursive_mutex locker(m_mutex);mul(x);div(y);}int m_i;//mutex m_mutex;recursive_mutex m_mutex;
};Calculate cal;
cal.both(6, 3); // 调用后就会发生死锁在both()中已经对互斥锁加锁了继续调用mult()函数已经得到互斥锁所有权的线程再次获取这个互斥锁的所有权就会造成死锁在C中程序会异常退出使用C库函数会导致这个互斥锁永远无法被解锁最终阻塞所有的线程。要解决这个死锁的问题一个简单的办法就是使用递归互斥锁std::recursive_mutex它允许一个线程多次获得互斥锁的所有权。
虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题但是还是建议少用主要原因如下
使用递归互斥锁的场景往往都是可以简化的使用递归互斥锁很容易放纵复杂逻辑的产生从而导致bug的产生递归互斥锁比非递归互斥锁效率要低一些。递归互斥锁虽然允许同一个线程多次获得同一个互斥锁的所有权但最大次数并未具体说明一旦超过一定的次数就会抛出std::system错误。
std::timed_mutex 不像lock一样一直阻塞
std::timed_mutex 是超时独占互斥锁主要是在获取互斥锁资源时增加了超时等待功能因为不知道获取锁资源需要等待多长时间为了保证不一直等待下去设置了一个超时时长超时后线程就可以解除阻塞去做其他事情了。 std::timed_mutex比std::_mutex多了两个成员函数try_lock_for()和try_lock_until()。两个函数是对 lock() 函数的延申返回true 时就代表已经获取到互斥锁资源。
try_lock_for()函数是当线程获取不到互斥锁资源的时候在给定的时间长度内允许线程为尝试获得资源而阻塞。参数对应 sleep_for()try_lock_until()函数是当线程获取不到互斥锁资源的时候在给定的时间节点之前允许线程为尝试获得资源而阻塞。参数对应 sleep_until()
关于两个函数的返回值bool在未达到限定情况时得到互斥锁的所有权之后函数会马上解除阻塞返回 true如果阻塞的时长用完或者到达指定的时间点之后函数也会解除阻塞返回 false。
std::recursive_timed_mutex
关于递归超时互斥锁std::recursive_timed_mutex的使用方式和std::timed_mutex是一样的只不过它可以允许一个线程多次获得互斥锁所有权而std::timed_mutex只允许线程获取一次互斥锁所有权。另外递归超时互斥锁std::recursive_timed_mutex也拥有和std::recursive_mutex一样的弊端不建议频繁使用。
2.3 使用条件变量进行线程同步配合锁才行
通过实例来理解引用条件变量的意义
#include iostream
#include thread
#include mutex
#include list
using namespace std;
// 模拟简化的网络游戏服务器
// 线程1从玩家那里收集发送来的命令数据并把这些数据写到一个队列中
// 线程2从队列中取出命令进行解析然后执行命令对应的动作class A {
public:// 把收到的消息玩家命令放入到一个队列的线程void inMsgRecvQueue() {for (int i 0; i 10; i) {unique_lockmutex sbguard1(my_mutex);cout 向消息队列插入一个元素 i \n;msgRecvQueue.push_back(i);//this_thread::sleep_for(chrono::milliseconds(1300));}}bool outMsgLuLproc(int command) {if (msgRecvQueue.empty()) { // 队列为空没必要去读取return false;}unique_lockmutex sbguard1(my_mutex);if (!msgRecvQueue.empty()) {command msgRecvQueue.front();msgRecvQueue.pop_front();return true;}return false;}void outMsgRecvQueue() {int command 0;for (int i 0; i 200; i) {// 不断尝试去队列中取元素// 队列为空时不断调用outMsgLuLproc函数也是一定的资源浪费bool res outMsgLuLproc(command);if (res) {cout 从消息队列中取出一个元素\n;}else {cout 目前消息队列是空的\n;}}}
private:list int msgRecvQueue; // 命令队列mutex my_mutex;
};int main() {A myobj;thread my_out_thread(A::outMsgRecvQueue, myobj);thread my_in_thread(A::inMsgRecvQueue, myobj);my_out_thread.join();my_in_thread.join();return 0;
}my_in_thread 线程中会有大量的 outMsgLuLproc() 函数调用尝试去取队列中的命令但是大部分的调用都是徒劳的。为了避免不断地判断消息队列是否为空而改为当消息队列不为空的时候做一个通知这样得到通知后再去取数据。这时候就引用了 std::condition_variable 这是一个类一个和条件相关的类用于等待一个条件的达成。
两种条件变量
条件变量是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
condition_variable
所需头文件 condition_variable condition_variable 的成员函数主要分为两部分线程等待阻塞函数 和线程通知唤醒函数
等待函数
调用wait()函数的线程会被阻塞下边给出 wait 函数形式
void wait (unique_lockmutex lck); // 调用该函数的线程直接被阻塞template class Predicate
void wait (unique_lockmutex lck, Predicate pred);
// 关于 pred
// 该函数的第二个参数是一个判断条件是一个返回值为布尔类型的函数
// 表达式返回false当前线程被阻塞表达式返回true当前线程不会被阻塞继续向下执行
// 该参数可以传递一个有名函数的地址也可以直接指定一个匿名函数独占的互斥锁对象不能直接传递给wait()函数需要通过模板类unique_lock进行二次处理
还有两个与 wait() 函数功能相同的函数
wait_for()函数指定阻塞时长wait_until()函数指定让线程阻塞到某一个时间点
假设阻塞期间内的线程没有被其他线程唤醒当到条件后线程就会自动解除阻塞继续向下执行。
通知函数
notify_one()唤醒一个被当前条件变量阻塞的线程如果有多个 wait 线程唤醒哪个其实是不确定的notify_all()唤醒全部被当前条件变量阻塞的线程
condition_variable_any
condition_variable_any用法与condition_variable基本相同其wait()函数可以配合四种类型的互斥量直接作为参数condition_variable对象只能配合unique_lockmutex然后再传入 wait 函数。除此之外它们的用法是相同的。
队列模拟多线程读写数据实例condition_variable
本质上就是生产者消费者模型使用条件变量进行同步
#include iostream
#include thread
#include mutex
#include list
using namespace std;
// 模拟简化的网络游戏服务器
// 线程1从玩家那里收集发送来的命令数据并把这些数据写到一个队列中
// 线程2从队列中取出命令进行解析然后执行命令对应的动作class A {
public:// 把收到的消息玩家命令放入到一个队列的线程void inMsgRecvQueue() {for (int i 0; i 10000; i) {unique_lockmutex u_mutex(my_mutex);cout 向消息队列插入一个元素 i \n;msgRecvQueue.push_back(i);my_cond.notify_one();// 尝试把处于阻塞在wait()的线程唤醒但是根据函数名显然只会唤醒一个数据}}void outMsgRecvQueue() {int command 0;for (; 1; ) {// 不断尝试去队列中取元素unique_lockmutex u_mutex(my_mutex); // condition_variable 需要配合unique_lock使用// 如果wait()第二个参数的lambda表达式返回true那么wait()直接返回// 返回false那么wait()将解锁互斥量并在此句阻塞直至有线程调用notigy_one() 通知为止// 如果不指定第二个参数那么跟lambda表达式返回false的情况一样直接等待其他线程调用notify_one()my_cond.wait(u_mutex, [this] {if (msgRecvQueue.empty()) return false; // 存在虚假唤醒万无一失写法return true;});cout thread id this_thread::get_id() endl;// 现在互斥量是锁着的执行到此队列里一定有数据command msgRecvQueue.front();msgRecvQueue.pop_front();cout 取出元素 command endl;u_mutex.unlock(); // 做完事情可以手动解锁体现出unique_lock的灵活性// 让刚得到锁的线程睡一下两个线程就交替取数据了//this_thread::sleep_for(chrono::milliseconds(200));}}
private:list int msgRecvQueue; // 命令队列mutex my_mutex;condition_variable my_cond;
};int main() {A myobj;// 搞两个取数据的线程运行可以发现 notify_one() 只会唤醒一个thread my_out_thread1(A::outMsgRecvQueue, myobj);thread my_out_thread2(A::outMsgRecvQueue, myobj);thread my_in_thread(A::inMsgRecvQueue, myobj);my_out_thread1.join();my_out_thread2.join();my_in_thread.join();return 0;
}用于生产者和消费者模型的大致过程
条件变量通常用于生产者和消费者模型大致使用过程如下
拥有条件变量的线程获取互斥量消费者循环检查某个条件如果条件不满足阻塞当前线程否则线程继续向下执行 \quad 产品的数量达到上限生产者阻塞否则生产者一直生产 \quad 产品的数量为零消费者阻塞否则消费者一直消费生产者条件满足之后可以调用notify_one()或者notify_all()唤醒一个或者所有被阻塞的线程 \quad 由消费者唤醒被阻塞的生产者生产者解除阻塞继续生产 \quad 由生产者唤醒被阻塞的消费者消费者解除阻塞继续消费
生产者和消费者模型实例condition_variable
无限进行取放的实例加上睡眠可以让取放交替进行
#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(){for (int i 0; ; i) {if (i m_maxSize) i 0;unique_lockmutex locker(m_mutex); // 配合unique_lock使用m_notFull.wait(locker, [this]() {return m_queue.size() ! m_maxSize;});m_queue.push_back(i);cout i 被生产 endl;m_notEmpty.notify_one();locker.unlock();//this_thread::sleep_for(chrono::milliseconds(800));}}void take(){while (1) {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;locker.unlock();//this_thread::sleep_for(chrono::milliseconds(800));}}private:listint m_queue; // 存储队列数据mutex m_mutex; // 互斥锁condition_variable m_notEmpty; // 不为空的条件变量condition_variable m_notFull; // 没有满的条件变量int m_maxSize; // 任务队列的最大任务个数
};int main()
{SyncQueue taskQ(50);thread my_put(SyncQueue::put, taskQ);thread my_take(SyncQueue::take, taskQ);my_put.join();my_take.join();return 0;
}2.4 使用原子变量实现线程同步不需要锁
C11提供了一个原子类型std::atomicT这是一个类模板可以用来封装某类型的值通过这个原子类型管理的内部变量就可以称之为原子变量我们可以给原子类型指定bool、char、int、long、指针等类型作为模板参数不支持浮点类型和复合类型。
原子指的是一系列不可被CPU上下文交换的机器指令这些指令组合在一起就形成了原子操作。在多核CPU下当某个CPU核心开始运行原子操作时会先暂停其它CPU内核对内存的操作以保证原子操作不会被其它CPU内核所干扰。
由于原子操作是通过指令提供的支持因此它的性能相比锁和消息传递会好很多。相比较于锁而言原子类型不需要开发者处理加锁和释放锁的问题同时支持修改读取等操作还具备较高的并发性能几乎所有的语言都支持原子类型。
可以看出原子类型是无锁类型但是无锁不代表无需等待因为原子类型内部使用了CAS循环当大量的冲突发生时该等待还是得等待但是总归比锁要好。CAS全称是Compare and swap, 它通过一条指令读取指定的内存地址然后判断其中的值是否等于给定的前置值如果相等则将其修改为新的值。
C11内置了整形的原子变量这样就可以更方便的使用原子变量了。在多线程操作中使用原子变量之后就不需要再使用互斥量来保护该变量了用起来更简洁。因为对原子变量进行的操作只能是一个原子操作atomic operation原子操作指的是不会被线程调度机制打断的操作这种操作一旦开始就一直运行到结束中间不会有任何的上下文切换。多线程同时访问共享资源造成数据混乱的原因就是因为CPU的上下文切换导致的使用原子变量解决了这个问题因此互斥锁的使用也就不再需要了。
所需头文件atomic
构造函数
atomic() noexcept default; // 默认无参构造函数
constexpr atomic( T desired ) noexcept; // 使用 desired 初始化原子变量的值
atomic( const atomic ) delete; // 使用delete显示删除拷贝构造函数, 不允许进行对象之间的拷贝std::atomicint g_mycount 0;
// 也可以这样写std::atomicint g_mycount(0);
// 封装一个类型为int的值可以像操作int变量一样操作 g_mycount 这个原子变量// 原子操作
g_mycount; g_mycount; g_mycount 1; g_mycount - 1;
g_mycount 1; g_mycount | 1; g_mycount ^ 1;
// 不是原子操作
g_mycount g_mycount 1; 原子操作针对的一个变量互斥量针对的是代码片段
3. 线程异步
C11中增加的线程类使得我们能够非常方便的创建和使用线程但有时会有些不方便比如需要获取线程返回的结果就不能通过join()得到结果只能通过一些额外手段获得比如定义一个全局变量在子线程中赋值在主线程中读这个变量的值整个过程比较繁琐。C提供的线程库中提供了一些类用于访问异步操作的结果。
我们去星巴克买咖啡因为都是现磨的所以需要等待但是我们付完账后不会站在柜台前死等而是去找个座位坐下来玩玩手机打发一下时间当店员把咖啡磨好之后就会通知我们过去取这就叫做异步。
顾客主线程发起一个任务子线程磨咖啡磨咖啡的过程中顾客去做别的事情了有两条时间线异步顾客主线程发起一个任务子线程磨咖啡磨咖啡的过程中顾客没去做别的事情而是死等这时就只有一条时间线同步此时效率相对较低。
因此多线程程序中的任务大都是异步的主线程和子线程分别执行不同的任务如果想要在主线中得到某个子线程任务函数返回的结果可以使用C11提供的std:future类这个类需要和其他类或函数搭配使用。
异步操作的主要目的是让调用方法的主线程不需要同步等待调用函数从而可以让主线程继续执行它下面的代码。因此异步操作无须额外的线程负担使用回调的方式进行处理。在设计良好的情况下处理函数可以不必或者减少使用共享变量减少了死锁的可能。当需要执行I/O操作时使用异步操作比使用线程同步 I/O操作更合适。
异步和多线程并不是一个同等关系异步是目的多线程是实现异步的一个手段。实现异步可以采用多线程或交给另外的进程来处理。
C11中的异步操作主要有std::future、std::async、std::promise、std::packaged_task。
3.1 std:future底层对象
构造函数
std::future 对象是std::async、std::promise、std::packaged_task的底层对象用来传递其他线程中操作的数据结果它是一个模板类可以存储任意指定类型的数据。
future() noexcept; //默认无参构造函数future( future other ) noexcept; // 移动构造函数转移资源的所有权future( const future other ) delete; // 使用delete显示删除拷贝构造函数, 不允许进行对象之间的拷贝通过构造函数可知future 类不允许进行对象之间的拷贝但是由移动构造函数可知传入右值可以转移所有权因此 右边是右值也可以转移资源的所有权。
成员函数
get() 阻塞当前线程子线程的数据就绪后解除阻塞就能得到传出的数值。wait() 阻塞当前线程子线程执行完毕时接触阻塞wait_until()阻塞一定的时长wait_for() 阻塞到某一指定的时间点
当wait_until()和wait_for()函数返回之后并不能确定子线程当前的状态因此我们需要判断函数的返回值这样就能知道子线程当前的状态了。 实例
std::futureint result std::async(work, 1);
std::future_status status result.wait_for(chrono::seconds(1));if (status std::future_status::timeout) {// 超时线程还没执行完cout 超时线程没有执行完毕 endl;
}
else if (status std::future_status::ready) {// 线程返回成功cout 线程成功执行完并返回 endl;
}
else if (status std::future_status::deferred) { // 如果 async 的第一个参数被设置为 std::launch::deferred则本条件成立cout 线程被延迟执行 endl;
}3.2 std::async 创建异步线程
针对需要线程返回一个值的情况返回值是一个 std::future 类型。
std::thread 是直接的创建线程在系统资源紧张的情况下调用 std::thread 可能会导致创建线程失败程序也会随之崩溃。
而std::async 其实是叫创建异步任务它可能创建线程也可能不创建线程。同时 std::async 还有一个优点这个异步任务返回的值程序员可以通过 std::future 对象在将来某个时刻程序执行完直接拿到手。当然如果不关注异步任务的结果只是简单地等待任务完成的话可以调用 std::future 类的wait()或者wait_for()方法这样功能就和 std::thread 差不多了。
另外std::async 不需要 join()
int work(int x) {return x;
}
// 根据返回值确定future模板参数
std::futureint result std::async(work, 1); // 流程并不会卡在这里
cout result.get() endl; // 卡在这里等待线程执行完但是这个 get 只能使用一次如果类的成员作线程入口函数参见本文开头的线程构造函数async(类函数/成员地址, 类实例对象地址, 类函数参数)
async额外参数
std::launch::deferred该参数表示线程入口函数的执行被延迟到 std::future 的 wait 或者 get 函数调用时如果都没有被调用那么这个线程就不执行了std::launch::async强制立即执行
std::futureint result std::async(std::launch::deferred, work, 1); // 流程并不会卡在这里
result.get(); // 线程由此开始执行3.3 std::packaged_task 保存的是一个可调用对象
从字面意思理解std::packaged_task 是打包任务或者说把任务包装起来的意思。 这是一个类模板它的模板参数是各种可调用对象。通过 std::packaged_task 把各种可调用对象包装起来方便将来作为线程入口函数来调用。需要配合 std::ref将包装完的对象转化为引用包装器。
通过调用get_future()方法就可以得到一个std::future对象基于这个对象就可以得到传出的数据了。
实例
int work(int x) {return x;
}
std::packaged_taskint(int) mypt(work); // 1. 把函数work包装起来// 这里的ref作用是将一个对象转换成一个引用包装器对象
std::thread t1(std::ref(mypt), 1); // 2. 以引用的形式作为参数传入线程 第二参数作为线程入口函数的参数
t1.join(); // thread创建的线程需要 join() 一下std::futureint result mypt.get_future(); // 3. future对象里含有线程入口函数的返回结果
// 这里用 result 保持 work() 函数的返回结果
cout result.get() endl; // 4. 输出结果thread 启动的线程记得要 join() 一下
如果把上面代码中的 join 注释掉虽然程序会卡在 result.get() 行一直等待线程返回但是整个程序会报异常。当然把 join 放到 get 之后也是可以的。
join 和 get 谁先出现执行流程就会卡在其所在的行等待线程返回。程序中需要 join 的调用否则执行后程序会报异常。
std::packaged_task 也可以包装一个 lambda 表达式
std::packaged_taskint(int) mypt([] {cout 1 endl;});3.4 std::promise保存一个共享状态的值
这是一个类模板这个类模板的作用是能够在某个线程中为其赋值然后就可以在其他线程中把这个值取出来使用。
std::promise是一个协助线程赋值的类可以通过 std::promise 保存一个值在将来的某个时刻通过把一个std::future 绑到这个 promise 上来得到这个绑定的值。
通过promise传递数据的过程一共分为5步 实例
// 必须以引用参数的形式传递 promise
void work(promiseint tmp_prom, int x) {x * x;tmp_prom.set_value(x); // 3. 在子线程任务函数中给std::promise对象赋值
}std::promiseint myprom; // 1. 在主线程中创建 std::promise 对象
std::thread t1(work, std::ref(myprom), 10); // 2. 将这个std::promise对象通过引用的方式传递给子线程的任务函数
t1.join(); // 记得join
std::futureint ful myprom.get_future(); // 4. 在主线程中通过std::promise对象取出绑定的future实例对象
int res ful.get(); // 5. 通过得到的future对象取出子线程任务函数中返回的值。
cout res endl;可以看出比 std::packaged_task 的方式多了一步在子线程中进行赋值的操作而用std::packaged_task 方式的子线程函数是有返回值的。 子线程的函数需要设置promiseint tmp_prom promise 类型的引用参数。
下边通过两个例子来理解 std::promise 的作用
子线程任务函数执行期间让状态就绪
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(); // 一定要放在最后不然join会阻塞到子线程执行完毕// 执行结果
/*
value: 100
睡醒了....
*/子线程的任务函数指定的是一个匿名函数在这个匿名的任务函数执行期间通过 p.set_value(100) 传出了数据并且激活了状态数据就绪后外部主线程中的 int value f.get() 解除阻塞并将得到的数据打印出来3 秒钟之后子线程休眠结束匿名的任务函数执行完毕。
子线程任务函数执行结束让状态就绪
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();// 执行结果
/*
睡醒了....
value: 100
*/在示例程序中子线程的这个匿名的任务函数中通过 p.set_value_at_thread_exit(100) 在执行完毕并退出之后才会传出数据并激活状态数据就绪后外部主线程中的 int value f.get() 解除阻塞并将得到的数据打印出来因此子线程在休眠5秒钟之后主线程中才能得到传出的数据。