12 官方库
约 16089 字大约 54 分钟
2025-06-22
协程是一种可以被挂起和恢复的函数,它提供了一种创建异步代码的方法。 C++中协程的概念早在2017年已经被提出,并且作为技术规范加入C++扩展中。
线程
命名空间this_thread
配合多线程使用的 提供了四个公共的成员函数,通过这些成员函数就可以对当前线程进行相关的操作了。
get_id()
得到当前线程的线程ID,函数原型如下:
thread::id get_id() noexcept;
thread::id 就是一个被封装过的整形数
void func(){cout << "子线程: " << this_thread::get_id() << endl;}
int main(){
cout << "主线程: " << this_thread::get_id() << endl;
thread t(func);
t.join();
}
程序启动,开始执行main()
函数,此时只有一个线程也就是主线程。当创建了子线程对象t
之后,指定的函数func()
会在子线程中执行,这时通过调用this_thread::get_id()
就可以得到当前(子)线程的线程ID了。
sleep_for()
线程被创建后有五种状态:
- 创建态:这个状态是非常短的,只要线程被创建出来后就不是创建态了,只有线程在创建期间才是创建态
- 就绪态:线程拿到除cpu外所有资源的情况
- 运行态:进程抢到了cpu的时间片在cpu中运行
- 阻塞态(挂起态):拿不到(不包括cpu)所需要的资源
- 退出态(终止态) 休眠函数sleep_for(),调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长,因为阻塞态的线程已经让出了CPU资源,代码也不会被执行,所以线程休眠过程中对CPU来说没有任何负担。 函数原型:
template <class Rep, class Period>
void sleep_for (const chrono::duration<Rep,Period>& rel_time);
参数是指定一个休眠时长,也就是一个时间段
程序休眠完成之后,会从阻塞态重新变成就绪态,就绪态的线程需要再次争抢CPU时间片,抢到之后才会变成运行态,这时候程序才会继续向下运行。
sleep_until()
另一个休眠函数sleep_until(),和sleep_for()不同的是它的参数类型不一样:
- sleep_until():指定线程阻塞到某一个指定的时间点time_point类型,之后解除阻塞
- sleep_for():指定线程阻塞一定的时间长度duration 类型,之后解除阻塞 函数原型:
template <class Clock, class Duration>
void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
sleep_until()和sleep_for()函数的功能是一样的,只不过前者是基于时间点去阻塞线程,后者是基于时间段去阻塞线程,项目开发过程中根据实际情况选择最优的解决方案即可。
yield()
其目的是避免一个线程长时间占用CPU资源,从而导致多线程处理性能下降 在线程中调用这个函数之后,处于运行态的线程会主动让出自己已经抢到的CPU时间片,最终变为就绪态,这样其它的线程就有更大的概率能够抢到CPU时间片了。 需要注意一点,线程调用了yield()之后会主动放弃CPU资源,但是这个变为就绪态的线程会马上参与到下一轮CPU的抢夺战中,不排除它能继续抢到CPU时间片的情况,这是概率问题。
void yield() noexcept;
此函数没有参数,当程序运行到此函数时会自动放弃对cpu的争抢,可以放在线程函数里面使用
线程类
在不创建线程的情况下程序的进程也就是一个线程,当在程序中创建了一个线程,原进程就退化成了主线程,主线程和子线程共同构成了进程
C++11中提供的线程类叫做std::thread,基于这个类创建一个新的线程非常的简单,只需要提供线程函数或者函数对象即可,并且可以同时指定线程函数的参数。
构造函数
//默认构造,构造一个线程对象,在这个线程中不执行任何处理动作
thread() noexcept;
//移动构造函数,将 other 的线程所有权转移给新的thread 对象。之后 other 不再表示执行线程。
thread( thread&& other ) noexcept;
//创建线程对象,并在该线程中执行函数f中的业务逻辑,args是要传递给函数f的参数
//任务函数f的可选类型有很多,具体如下:
//普通函数,类成员函数,匿名函数,仿函数(这些都是可调用对象类型)
//可以是可调用对象包装器类型,也可以是使用绑定器绑定之后得到的类型(仿函数)
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
//使用=delete显示删除拷贝构造, 不允许线程对象之间的拷贝
thread( const thread& ) = delete;
示例:
void func1(){
cout<<"func1:"<<this_thread::get_id()<<endl;
}
int func2(string name,int age){
cout<<name<<age<<"func2:"<<this_thread::get_id()<<endl;
return 0;
}
int main() {
cout<<"main_thread:"<<this_thread::get_id()<<endl;//输出主线程id
thread t1;//空线程
thread t2(func1);
thread t3(func2,"呆虫",21);
thread t4([=](int a){
cout<<a<<"lambda:"<<this_thread::get_id()<<endl;
},1001);
thread t5=std::move(t4);
return 0;
}
这样执行程序会报错,原因是创建了子线程后main函数也就是主线程,其他子线程属于此主线程,因此刚开始的时候主线程拥有时间片,它在执行完自己函数内的的程序后就退出了。如果主线程退出了,那么其里面程序的虚拟内存空间就不存在了,因此子线程访问的资源也就不存在了。 解决方法就是让主线程在等待其内部的子线程全部执行完毕后再退出
线程函数一般返回值为void类型,因为子线程在调用这个函数的时候不会处理其返回值 当启动了一个线程(创建了一个thread对象)之后,在这个线程结束的时候(std::terminate()),我们如何去回收线程所使用的资源呢?thread库给我们两种选择:
- 加入式(join())
- 分离式(detach())
另外,我们必须要在线程对象销毁之前在二者之间作出选择,否则程序运行期间就会有bug产生。
公共成员函数
get_id()
std::thread::id get_id() const noexcept;
这个get_id和this_thread里面的get_id作用是一样的,区别在与this_thread里面的get_id只能在线程函数里面使用用来获取本线程函数的id 而成员函数的get_id就可以在线程函数外面获取线程类的id了
join()
join()字面意思是连接一个线程,意味着主动地等待线程的终止(线程阻塞)。在某个线程中通过子线程对象调用join()函数,调用这个函数的线程(主线程)被阻塞,但是子线程对象中的任务函数会继续执行,当任务执行完毕之后join()会清理当前子线程中的相关资源然后返回(停止阻塞),同时,调用该函数的线程解除阻塞继续向下执行。
void join();
如果要阻塞主线程的执行,只需要在主线程中通过子线程对象调用这个方法即可,当调用这个方法的子线程对象中的任务函数执行完毕之后,主线程的阻塞也就随之解除了。 这样可以保证子线程访问的资源不被回收 这样在上述程序中只需要在main函数(主线程)中添加以下代码即可解决报错:
t2.join();
t3.join();
t5.join();
不调用t1的join是因为它没有任务,不是一个可用的线程对象 不调用t4的join是因为它的资源被转移给了t5
创建了子线程后子线程的运行顺序是不确定的,因为它们可能抢到时间片的顺序不同 为了让子线程按照特定顺序执行就需要使用sleep_for或sleep_until函数控制不同线程睡眠的时长让它们按照特定顺序拿到时间片,以此保证特定顺序的执行
[!important] 典型应用:多线程下载 多线程下载即让要下载的目标文件分为好几个部分,不同的线程分别下载对应部分的数据。在下载目标文件之前需要得出目标文件的大小,然后在本地开辟出一个同等大小的存储空间在里面先填充垃圾数据用以占位。然后各线程分别按顺序下载对应部分的数据将对应部分的数据替换占位的垃圾数据,当所有线程都下载好后再将文件进行合并
detach() 进行线程分离,分离主线程和创建出的子线程。在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。
void detach();
线程分离后主线程就不用负责子线程的资源释放了
joinable() 判断主线程和子线程是否处理关联(连接)状态,一般情况下,二者之间的关系处于关联状态,该函数返回一个布尔类型:
- 返回值为true:主线程和子线程之间有关联(连接)关系
- 返回值为false:主线程和子线程之间没有关联(连接)关系
bool joinable() const noexcept;
- 在创建的子线程对象的时候,如果没有指定任务函数,那么子线程不会启动,主线程和这个子线程也不会进行连接
- 在创建的子线程对象的时候,如果指定了任务函数,子线程启动并执行任务,主线程和这个子线程自动连接成功
- 子线程调用了detach()函数之后,父子线程分离,同时二者的连接断开,调用joinable()返回false
- 在子线程调用了join()函数,子线程中的任务函数继续执行,直到任务处理完毕,这时join()会清理(回收)当前子线程的相关资源,所以这个子线程和主线程的连接也就断开了,因此,调用join()之后再调用joinable()会返回false。
operator= 线程中的资源是不能被复制的,因此通过=操作符进行赋值操作最终并不会得到两个完全相同的对象。 其重载的是移动构造函数
// move (1)
thread& operator= (thread&& other) noexcept;
// copy [deleted] (2)
thread& operator= (const other&) = delete;
静态函数 thread线程类还提供了一个静态方法,用于获取当前计算机的CPU核心数,根据这个结果在程序中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的。
static unsigned hardware_concurrency() noexcept;
类的成员函数作为子线程的任务函数
线程任务函数必须是传入的函数地址
class A{
public:
void func(string name,int age){
cout<<"func"<<name<<age<<endl;
}
static void print(){
cout<<"static func"<<endl;
}
};
int main() {
A a;
//类的静态函数
thread t1(&A::print);
//thread t8(A::print);//error
//类的成员函数
thread t2(&A::func,a,"呆虫",20);
thread t3(&A::func,&a,"八嘎",12);
thread t4(&A::func,A(),"阿冷",26);
//可调用对象包装器
thread t5(bind(&A::func,a,"呆虫",20));
thread t6(bind(&A::func,a,"八嘎",12));
thread t7(bind(&A::func,a,"阿冷",26));
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
t6.join();
t7.join();
return 0;
}
call_once函数
在头文件<mutex>中 被call_once()函数修饰的函数只能调用一次 在某些特定情况下,某些函数只能在多线程环境下调用一次,比如:要初始化某个对象,而这个对象只能被初始化一次,就可以使用std::call_once()来保证函数在多线程环境下只能被调用一次。使用call_once()的时候,需要一个once_flag作为call_once()的传入参数,该函数的原型如下:
template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
- flag:once_flag类型的对象,要保证这个对象能够被多个线程同时访问到
- f:回调函数,可以传递一个有名函数地址,也可以指定一个匿名函数
- args:作为实参传递给回调函数
线程同步
多线程虽然效率上有极大的提高,但也带来了一个问题,就是多个线程同时读或写同一块内存,或者说一个线程在对一块内存进行操作时时间片用完了,而这个内存被另一个线程使用,当此线程再次拿到时间片后这块内存上的数据就不一定和原本一样了 线程同步并不是让它们同时运行,而是让它们按照先后顺序有顺序地对内存进行操作
互斥锁
解决线程混乱的方案就是线程同步,最常用的就是互斥锁 C++11一共提供了四种互斥锁:
- std::mutex:独占的互斥锁,不能递归使用
- std::timed_mutex:带超时的独占互斥锁,不能递归使用
- std::recursive_mutex:递归互斥锁,不带超时功能
- std::recursive_timed_mutex:带超时的递归互斥锁 其中递归互斥锁就是可以多次使用的(在前面线程上锁后依旧可以多次加锁),不递归的互斥锁只能锁一次 递归互斥锁加锁和解锁操作是针对多个线程的。 普通的互斥锁是针对单个线程的,当一个线程上锁后只有这个线程才可以解锁,且只有解锁后其他线程才可以再对互斥锁加锁或上锁
在使用互斥锁时锁住的不是单个变量而是和这个变量相关的代码块,因为对这个变量的操作有可能是一个逻辑。找到变量相关的上下文代码块后在代码块的上面加锁。当一个线程抢到这个互斥锁后就上锁,其他线程就会进入阻塞态。当这个代码块执行完要使用unlock()解锁操作,这样其他线程才可以继续执行
mutex
成员函数 lock()函数用于给临界区加锁,并且只能有一个线程获得锁的所有权,它有阻塞线程的作用,函数原型如下:
void lock();
独占互斥锁对象有两种状态:锁定和未锁定。如果互斥锁是打开的,调用lock()函数的线程会得到互斥锁的所有权,并将其上锁,其它线程再调用该函数的时候由于得不到互斥锁的所有权,就会被lock()函数阻塞。当拥有互斥锁所有权的线程将互斥锁解锁,此时被lock()阻塞的线程解除阻塞,抢到互斥锁所有权的线程加锁并继续运行,没抢到互斥锁所有权的线程继续阻塞。
除了使用lock()还可以使用try_lock()获取互斥锁的所有权并对互斥锁加锁,函数原型如下:
bool try_lock();
二者的区别在于try_lock()不会阻塞线程,lock()会阻塞线程:
- 如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回true
- 如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回false 使用try_lock()可以对没有线程进行处理,在抢不到互斥锁的情况下可以根据处理具体执行而不是进入阻塞
当互斥锁被锁定之后可以通过unlock()进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。该函数的函数原型如下:
void unlock();
使用互斥锁进行线程同步的大致思路主要分为以下几步:
- 找到多个线程操作的共享资源(全局变量、堆内存、类成员变量等),也可以称之为临界资源
- 找到和共享资源有关的上下文代码,也就是临界区
- 在临界区的上边调用互斥锁类的lock()方法
- 在临界区的下边调用互斥锁的unlock()方法
[!warning] Title 当线程对互斥锁对象加锁,并且执行完临界区代码之后,一定要使用这个线程对互斥锁解锁,否则最终会造成线程的死锁。死锁之后当前应用程序中的所有线程都会被阻塞,并且阻塞无法解除,应用程序也无法继续运行。
lock_guard
lock_guard是C++11新增的一个模板类,使用这个类,可以简化互斥锁lock()和unlock()的写法,同时也更安全。 这个模板类的定义和常用的构造函数原型如下:
// 类的定义,定义于头文件 <mutex>
template< class Mutex >
class lock_guard;
// 常用构造函数
explicit lock_guard( mutex_type& m );
lock_guard在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记unlock()操作而导致线程死锁。(只要构造出这个对象就可上锁,不需要也没有lock函数) lock_guard使用了RAII技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。
但使用lock_guard也会有一个问题,就是这个类会在它生命周期结束时解锁,这会导致不如使用lock和unlock灵活,它上锁和解锁作用到的代码块就是它本身创建出来开始到作用域结束 解决方法就是单独给要上锁的代码块和lock_guard放在同一作用域
//不需要上锁的代码块
{
lock_guard<mutex> guard(mx);//mutex为互斥锁的类型,mx是mutex对象要与模板类形参里面的mutex同类
//上锁的代码块
}
//不需要上锁的代码块
recorsive_mutex
递归互斥锁std::recursive_mutex允许同一线程多次获得互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题 虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题,但是还是建议少用,主要原因如下:
- 使用递归互斥锁的场景往往都是可以简化的,使用递归互斥锁很容易放纵复杂逻辑的产生,从而导致bug的产生
- 递归互斥锁比非递归互斥锁效率要低一些。
- 递归互斥锁虽然允许同一个线程多次获得同一个互斥锁的所有权,但最大次数并未具体说明,一旦超过一定的次数,就会抛出std::system错误。
超时互斥锁timed_mutex和recorsive_timed_mutex
stdtimed_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 Period>
bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);
template <class Clock, class Duration>
bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
当得到互斥锁的所有权之后,函数会马上解除阻塞,返回true,如果阻塞的时长用完或者到达指定的时间点之后,函数也会解除阻塞,返回false
和sleep_for、sleep_until类似: try_lock_for函数是当线程获取不到互斥锁资源的时候,让线程阻塞一定的时间长度 try_lock_until函数是当线程获取不到互斥锁资源的时候,让线程阻塞到某一个指定的时间点
关于递归超时互斥锁stdtimed_mutex是一样的,只不过它可以允许一个线程多次获得互斥锁所有权,而stdrecursive_timed_mutex也拥有和std::recursive_mutex一样的弊端,不建议频繁使用。
条件变量
条件变量是C++11提供的另外一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时时,才会唤醒当前阻塞的线程。
- 条件变量可以放行多个,阻塞多个线程
- 互斥锁可以放行一个线程,阻塞多个线程
条件变量需要和互斥量配合起来使用,C++11提供了两种条件变量:
condition_variable:需要配合std::unique_lockstd::mutex进行wait操作,也就是阻塞线程的操作。
condition_variable_any:可以和任意带有lock()、unlock()语义的mutex搭配使用,也就是说前面的四种互斥锁都可以使用
条件变量通常用于生产者和消费者模型,大致使用过程如下: 拥有条件变量的线程获取互斥量 循环检查某个条件,如果条件不满足阻塞当前线程,否则线程继续向下执行 产品的数量达到上限,生产者阻塞,否则生产者一直生产。。。 产品的数量为零,消费者阻塞,否则消费者一直消费。。。 条件满足之后,可以调用notify_one()或者notify_all()唤醒一个或者所有被阻塞的线程 由消费者唤醒被阻塞的生产者,生产者解除阻塞继续生产。。。 由生产者唤醒被阻塞的消费者,消费者解除阻塞继续消费。。。
condition_variable
成员函数
condition_variable的成员函数主要分为两部分:线程等待(阻塞)函数 和线程通知(唤醒)函数,这些函数被定义于头文件 <condition_variable>。
阻塞函数
调用wait()函数的线程会被阻塞 如果线程被该函数阻塞,这个线程会释放占有的互斥锁的所有权,当阻塞解除之后这个线程会重新得到互斥锁的所有权,继续向下执行
//调用该函数的线程直接被阻塞
void wait (unique_lock<mutex>& lck);
//函数的第二个参数是一个判断条件,是一个返回值为布尔类型的函数
//该参数可以传递一个有名函数的地址,也可以直接指定一个匿名函数
//表达式返回false当前线程被阻塞,表达式返回true当前线程不会被阻塞,继续向下执行
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);
独占的互斥锁对象不能直接传递给wait()函数,需要通过模板类unique_lock进行二次处理,通过得到的对象仍然可以对独占的互斥锁对象做如下操作,使用起来更灵活。
其公共成员函数和timed_mutex是相同的,作用也相同 都有lock、try_lock、try_lock_for、try_lock_until、unlock
wait_for() 和wait一样,但多了阻塞时长
template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time);
template <class Rep, class Period, class Predicate>
bool wait_for(unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time, Predicate pred);
wait_until() 和wait一样,但多了阻塞时间点
template <class Clock, class Duration>
cv_status wait_until (unique_lock<mutex>& lck,
const chrono::time_point<Clock,Duration>& abs_time);
template <class Clock, class Duration, class Predicate>
bool wait_until (unique_lock<mutex>& lck,
const chrono::time_point<Clock,Duration>& abs_time, Predicate pred);
通知函数
void notify_one() noexcept;
void notify_all() noexcept;
- notify_one():唤醒一个被当前条件变量阻塞的线程
- notify_all():唤醒全部被当前条件变量阻塞的线程
condition_variable_any
condition_variable_any的成员函数也是分为两部分:线程等待(阻塞)函数 和线程通知(唤醒)函数,这些函数被定义于头文件 <condition_variable>。
成员函数
等待函数
// 调用该函数的线程直接被阻塞
template <class Lock> void wait (Lock& lck);
// 该函数的第二个参数是一个判断条件,是一个返回值为布尔类型的函数
//该参数可以传递一个有名函数的地址,也可以直接指定一个匿名函数
//表达式返回false当前线程被阻塞,表达式返回true当前线程不会被阻塞,继续向下执行
template <class Lock, class Predicate>
void wait (Lock& lck, Predicate pred);
可以直接传递给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::duration<Rep,Period>& rel_time);
template <class Lock, class Rep, class Period, class Predicate>
bool wait_for (Lock& lck, const chrono::duration<Rep,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_point<Clock,Duration>& abs_time);
template <class Lock, class Clock, class Duration, class Predicate>
bool wait_until (Lock& lck,
const chrono::time_point<Clock,Duration>& abs_time,
Predicate pred);
通知函数
void notify_one() noexcept;
void notify_all() noexcept;
notify_one():唤醒一个被当前条件变量阻塞的线程 notify_all():唤醒全部被当前条件变量阻塞的线程
原子变量
C++11提供了一个原子类型std::atomic<T>,通过这个原子类型管理的内部变量就可以称之为原子变量,我们可以给原子类型指定bool、char、int、long、指针等类型作为模板参数(不支持浮点类型和复合类型也就是自定义类型)。 但指针可以指向复合数据类型,但也不能通过指针保护复合类型,它只能保证对这个指针的算术操作是原子操作(如++、--)
原子指的是一系列不可被CPU上下文交换的机器指令,这些指令组合在一起就形成了原子操作。在多核CPU下,当某个CPU核心开始运行原子操作时,会先暂停其它CPU内核对内存的操作,以保证原子操作不会被其它CPU内核所干扰。 由于原子操作是通过指令提供的支持,因此它的性能相比锁和消息传递会好很多。相比较于锁而言,原子类型不需要开发者处理加锁和释放锁的问题,同时支持修改,读取等操作,还具备较高的并发性能,几乎所有的语言都支持原子类型。 原子类型是无锁类型,但是无锁不代表无需等待,因为原子类型内部使用了CAS循环,当大量的冲突发生时,该等待还是得等待!但是总归比锁要好。
在多线程操作中,使用原子变量之后就不需要再使用互斥量来保护该变量了,用起来更简洁。因为对原子变量进行的操作只能是一个原子操作(atomic operation),原子操作指的是不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何的上下文切换。多线程同时访问共享资源造成数据混乱的原因就是因为CPU的上下文切换导致的,使用原子变量解决了这个问题,因此互斥锁的使用也就不再需要了。
[!important] Title CAS全称是Compare and swap, 它通过一条指令读取指定的内存地址,然后判断其中的值是否等于给定的前置值,如果相等,则将其修改为新的值
atomic类成员
类定义
template< class T >
struct atomic;
在使用这个模板类的时候,一定要指定模板类型。
构造函数
// 默认无参构造函数
atomic() noexcept = default;
// 使用 desired 初始化原子变量的值。
constexpr atomic( T desired ) noexcept;
// 使用=delete显示删除拷贝构造函数, 不允许进行对象之间的拷贝
atomic( const atomic& ) = delete;
公共成员函数
原子类型在类内部重载了=操作符,并且不允许在类的外部使用 =进行对象的拷贝。
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;
[!tip] Title 使用volatile关键字修饰的函数是线程安全的函数
特化成员函数
复合运算符重载
//模板类型T为整形
T operator+= (T val) volatile noexcept;
T operator+= (T val) noexcept;
T operator-= (T val) volatile noexcept;
T operator-= (T val) noexcept;
T operator&= (T val) volatile noexcept;
T operator&= (T val) noexcept;
T operator|= (T val) volatile noexcept;
T operator|= (T val) noexcept;
T operator^= (T val) volatile noexcept;
T operator^= (T val) noexcept;
//模板类型T为指针
T operator+= (ptrdiff_t val) volatile noexcept;
T operator+= (ptrdiff_t val) noexcept;
T operator-= (ptrdiff_t val) volatile noexcept;
T operator-= (ptrdiff_t val) noexcept;
以上各个 operator 都会有对应的 fetch_* 操作(按位操作)
操作符 | 操作符重载函数 | 等级的成员函数 | 整形 | 指针 | 其他 |
---|---|---|---|---|---|
+ | atomic::operator+= | atomic::fetch_add | 是 | 是 | 否 |
- | atomic::operator-= | atomic::fetch_sub | 是 | 是 | 否 |
& | atomic::operator&= | atomic::fetch_and | 是 | 否 | 否 |
| | atomic::operator|= | atomic::fetch_or | 是 | 否 | 否 |
^ | atomic::operator^= | atomic::fetch_xor | 是 | 否 | 否 |
其他函数
//可以直接使用等号给原子变量赋值,也可以使用atomic_init()函数初始化,第一个参数是原子变量的引用,第二个参数是要赋得值
void atomic_init(atomic<T>& a,T value)
//更改原子变量的值,可以使用等号直接赋值,也可以调用store方法和exchange方法,这两个都是类成员函数
a.store(value);//将a的值更新为value
a.exchange(value);//将a的值更新为value,这个函数有返回值,返回值是a原来的数值
内存顺序约束
在C/C++代码进行汇编时由于编译器的优化以及多线程每次执行的不同导致汇编成的二进制文件中的操作码可能会有些许不同,这样可能导致原子操作不太符合预期,这种情况可以对编译器的优化进行不同程度的限制来控制操作码顺序重排的度。当然对优化限制越多生成的二进制文件效率也会越低。
在调用 atomic类提供的 API 函数的时候,需要指定原子顺序,在C++11给我们提供的 API 中使用枚举用作执行原子操作的函数的实参,以指定如何同步不同线程上的其他操作。
这些内存顺序约束的API被放到一个枚举类型中
typedef enum memory_order {
memory_order_relaxed, // relaxed
memory_order_consume, // consume
memory_order_acquire, // acquire
memory_order_release, // release
memory_order_acq_rel, // acquire/release
memory_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_acquire:若有两个线程,一个线程负责写数据,另一个线程负责对同一块内存进行读数据,如果在写数据的时候对这个内存的数据进行了多次更新,那么另一个进行读的数据也能读到所有更新的数据,但对于这个线程它只需要最后那个有效的数据,之前读到的更新数据是没有用的(也叫脏数据),因此会带来不必要的内存开销,而使用memory_order_consume能够减少这种情况。(只能减少不能杜绝)
- 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 操作前。 一般来说是不需要对内存顺序约束进行修改的,在跨平台开发可能需要,因为不同平台的操作码可能是不同的
C++20新增成员
公共成员函数 | 说明 |
---|---|
wait(C++20) | 阻塞线程直至被提醒且原子值更改 |
notify_one(C++20) | 通知(唤醒)至少一个在原子对象上阻塞的线程 |
notify_all(C++20) | 通知(唤醒)所有在原子对象上阻塞的线程 |
别名 | 原始类型定义 |
---|---|
atomic_bool(C++11) | std::atomic<bool> |
atomic_char(C++11) | std::atomic<char> |
atomic_schar(C++11) | std::atomic<signed char> |
atomic_uchar(C++11) | std::atomic<unsigned char> |
atomic_short(C++11) | std::atomic<short> |
后续新增原子变量类型别名与此规律相同 |
线程异步
需要获取线程返回的结果,就不能通过join()得到结果,只能通过一些额外手段获得,比如:定义一个全局变量,在子线程中赋值,在主线程中读这个变量的值,整个过程比较繁琐。C++提供的线程库中提供了一些类用于访问异步操作的结果。
- 顾客(主线程)发起一个任务(子线程磨咖啡),磨咖啡的过程中顾客去做别的事情了,有两条时间线(异步)
- 顾客(主线程)发起一个任务(子线程磨咖啡),磨咖啡的过程中顾客没去做别的事情而是死等,这时就只有一条时间线(同步),此时效率相对较低。
std::future
因此多线程程序中的任务大都是异步的,主线程和子线程分别执行不同的任务,如果想要在主线中得到某个子线程任务函数返回的结果可以使用C++11提供的std:future类,这个类需要和其他类或函数搭配使用 这个类是一个模板类,可以存储任意指定类型的数据。
类定义
template< class T > class future;
template< class T > class future<T&>;
template<> class future<void>;
构造函数
// 默认无参构造函数
future() noexcept;
// 移动构造函数,转移资源的所有权
future( future&& other ) noexcept;
// 使用=delete显示删除拷贝构造函数, 不允许进行对象之间的拷贝
future( const future& other ) = delete;
常用成员函数(public)
一般情况下使用=进行赋值操作就进行对象的拷贝,但是future对象不可用复制,因此会根据实际情况进行处理:
- 如果other是右值,那么转移资源的所有权
- 如果other是非右值,不允许进行对象之间的拷贝(该函数被显示删除禁止使用)
future& operator=( future&& other ) noexcept;
future& operator=( const future& other ) = delete;
取出future对象内部保存的数据,其中void get()是为future<void>准备的,此时对象内部类型就是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::duration<Rep,Period>& timeout_duration ) const;
template< class Clock, class Duration >
std::future_status wait_until( const std::chrono::time_point<Clock,Duration>& timeout_time ) const;
其返回值是子线程的状态 当wait_until()和wait_for()函数返回之后,并不能确定子线程当前的状态,因此我们需要判断函数的返回值,这样就能知道子线程当前的状态了:
常量 | 解释 |
---|---|
future_status::deferred | 子线程中的任务函仍未启动 |
future_status::ready | 子线程中的任务已经执行完毕,结果已就绪 |
future_status::timeout | 子线程中的任务正在执行中,指定等待时长已用完 |
std::promise
std::promise是一个协助线程赋值的类,它能够将数据和future对象绑定起来,为获取线程函数中的某个值提供便利。
类成员函数
类定义
这也是一个模板类,我们要在线程中传递什么类型的数据,模板参数就指定为什么类型。
template< class R > class promise;
template< class R > class promise<R&>;
template<> class promise<void>;
构造函数
// 默认构造函数,得到一个空对象
promise();
// 移动构造函数
promise( promise&& other ) noexcept;
// 使用=delete显示删除拷贝构造函数, 不允许进行对象之间的拷贝
promise( const promise& other ) = delete;
公共成员函数
在std::promise类内部管理着一个future类对象,调用get_future()就可以得到这个future对象了
std::future<T> get_future();
存储要传出的 value 值,并立即让状态就绪,这样数据被传出其它线程就可以得到这个数据了。重载的第四个函数是为promise<void>类型的对象准备的。
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();
promise的使用
通过promise传递数据的过程一共分为5步:
- 在主线程中创建std::promise对象
- 将这个std::promise对象通过引用的方式传递给子线程的任务函数
- 在子线程任务函数中给std::promise对象赋值
- 在主线程中通过std::promise对象取出绑定的future实例对象
- 通过得到的future对象取出子线程任务函数中返回的值。
在实例化子线程对象的时候,如果任务函数的参数是引用类型,那么实参一定要放到std::ref()函数中,表示要传递这个实参的引用到任务函数中。 如果不使用ref这个函数是无法将引用传递过去的
std::packaged_task
stdpromise有点类似,stdpackaged_task保存的是一个函数。
类成员函数
类的定义
通过类的定义可以看到这也是一个模板类,模板类型和要在线程中传出的数据类型是一致的。
template< class > class packaged_task;
template< class R, class ...Args >
class packaged_task<R(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::future<R> get_future();
packaged_task其实就是对子线程要执行的任务函数进行了包装,和可调用对象包装器的使用方法相同,包装完毕之后直接将包装得到的任务对象传递给线程对象就可以了
std::async
stdpromise和packaged_task更高级一些,因为通过这函数可以直接启动一个子线程并在这个子线程中执行对应的任务函数,异步任务执行完成返回的结果也是存储到一个future对象中,当需要获取异步任务的结果时,只需要调用future 类的get()方法即可,如果不关注异步任务的结果,只是简单地等待任务完成的话,可以调用future 类的wait()或者wait_for()方法。该函数的函数原型如下:
// 直接调用传递到函数体内部的可调用对象,返回一个future对象
template< class Function, class... Args>
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
async( Function&& f, Args&&... args );
// 通过指定的策略调用传递到函数内部的可调用对象,返回一个future对象
template< class Function, class... Args >
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
async( std::launch policy, Function&& f, Args&&... args );
函数参数:
- f:可调用对象,这个对象在子线程中被作为任务函数使用
- Args:传递给 f 的参数(实参)
- policy:可调用对象·f的执行策略
策略 | 说明 |
---|---|
stdasync | 调用async函数时创建新的线程执行任务函数 |
stddeferred | 调用async函数时不执行任务函数,直到调用了future的get()或者wait()时才执行任务(这种方式不会创建新的线程) |
使用方式
方式1
调用async()函数直接创建线程执行任务
#include <iostream>
#include <thread>
#include <future>
using namespace std;
int main()
{
cout << "主线程ID: " << this_thread::get_id() << endl;
// 调用函数直接创建线程执行任务
future<int> 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;
}
调用async()函数时不指定策略就是直接创建线程并执行任务,示例代码的主线程中做了如下操作status = f.wait_for(chrono::seconds(1));其实直接调用f.get()就能得到子线程的返回值。这里为了演示wait_for()的使用,所以写的复杂了些。
方式2
调用async()函数不创建线程执行任务
#include <iostream>
#include <thread>
#include <future>
using namespace std;
int main(){
cout << "主线程ID: " << this_thread::get_id() << endl;
// 调用函数直接创建线程执行任务
future<int> 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;
}
由于指定了launch::deferred 策略,因此调用async()函数并不会创建新的线程执行任务,当使用future类对象调用了get()或者wait()方法后才开始执行任务(此处一定要注意调用wait_for()函数是不行的)。
- 使用async()函数,是多线程操作中最简单的一种方式,不需要自己创建线程对象,并且可以得到子线程函数的返回值。
- 使用std::promise类,在子线程中可以传出返回值也可以传出其他数据,并且可选择在什么时机将数据从子线程中传递出来,使用起来更灵活。
- 使用std::packaged_task类,可以将子线程的任务函数进行包装,并且可以得到子线程的返回值。
时间库
C++11中提供了日期和时间相关的库chrono,通过chrono库可以很方便地处理日期和时间,为程序的开发提供了便利。chrono库主要包含三种类型的类:时间间隔duration、时钟clocks、时间点time_point。 其中这些函数都是声明在chrono命名空间中的,使用时需要打开命名空间
duration时间间隔
用来记录时间长度,可以表示几秒、几分钟、几个小时的时间间隔。 函数原型:
template<
class Rep,
class Period = std::ratio<1>
> class duration;
Rep:时间周期的次数是一个数值类型,表示时钟数(周期)的类型(默认为整形)。若 Rep 是浮点数,则 duration 能使用小数描述时钟周期的数目。
Period:表示时钟的周期,它的原型如下:
template<
std::intmax_t Num,
std::intmax_t Denom = 1
> class ratio;
ratio类表示每个时钟周期的秒数,其中第一个模板参数Num代表分子,Denom代表分母,该分母值默认为1,因此,ratio代表的是一个分子除以分母的数值,比如:
ratio<2>代表一个时钟周期是2秒
ratio<60>代表一分钟
ratio<60*60>代表一个小时
ratio<60*60*24>代表一天
ratio<1,1000>代表的是1/1000秒,也就是1毫秒
ratio<1,1000000>代表一微秒
ratio<1,1000000000>代表一纳秒。
为了方便使用,在标准库中定义了一些常用的时间间隔,比如:时、分、秒、毫秒、微秒、纳秒,它们都位于chrono命名空间下,定义如下:
类型 | 定义 |
---|---|
纳秒:stdnanoseconds | duration<Rep*/*至少 64 位的有符号整数类型*/*, std::nano> |
微秒:stdmicroseconds | duration<Rep*/*至少 55 位的有符号整数类型*/*, std::micro> |
毫秒:stdmilliseconds | duration<Rep*/*至少 45 位的有符号整数类型*/*, std::milli> |
秒:stdseconds | duration<Rep*/*至少 35 位的有符号整数类型*/*> |
分钟:stdminutes | duration<Rep*/*至少 29 位的有符号整数类型*/*, std::ratio<60>> |
小时:stdhours | duration<Rep*/*至少 23 位的有符号整数类型*/*, std::ratio<3600>> |
[!warning] Title 到 hours 为止的每个预定义时长类型至少涵盖 ±292 年的范围
duartion中的成员函数
构造函数
// 1. 拷贝构造函数
duration( const duration& ) = default;
// 2. 通过指定时钟周期的类型来构造对象
template< class Rep2 >
constexpr explicit duration( const Rep2& r );
// 3. 通过指定时钟周期类型,和时钟周期长度来构造对象
template< class Rep2, class Period2 >
constexpr duration( const duration<Rep2,Period2>& d );
示例:
//hour
duration<int,ratio<3600>> hour(1);
chrono::hours h(1);
//min 如果后面的变量rep改为60,min(60)也相当于一小时
duration<int,ratio<60>> min(1);
chrono::minutes m(1);
//sec
duration<int> sec(1);
chrono::seconds sec1(1);
//msec
duration<int,ratio<1,1000>> msec(1);
chrono::microseconds msec1(2);
// micro
duration<int, ratio<1, 1000000>> micro(1);
chrono::microseconds micsec(3);
//nsec
duration<int,ratio<1,1000000000>> nsec(1);
chrono::nanoseconds n(5);
操作符重载
//赋值内容 (公开成员函数)
operator=
//实现一元 + 和一元 - (公开成员函数
operator+
operator-)
//递增或递减周期计数 (公开成员函数)
operator++
operator++(int)
operator–
operator–(int)
//实现二个时长间的复合赋值 (公开成员函数)
operator+=
operator-=
operator*=
operator/=
operator%=
示例:
chrono::minutes m1(9);
chrono::minutes m2(8);
m1++;
m2--;
chrono::minutes m3=m1-m2;//10-7
duration<int ,ratio<60,1> > min(10);
duration<int ,ratio<1,1> > sec(60);
auto t= min-sec;//周期类型是秒
由于两个时钟周期一个是秒,一个是分钟,不知道两者运算后的类型,所以用auto
[!warning] Title duration的加减运算有一定的规则,当两个duration时钟周期不相同的时候,会先统一成一种时钟,然后再进行算术运算,统一的规则如下:假设有ratio<x1,y1> 和 ratio<x2,y2>两个时钟周期,首先需要求出x1,x2的最大公约数X,然后求出y1,y2的最小公倍数Y,统一之后的时钟周期ratio为ratio<X,Y>。
其他函数 获取时间间隔的时钟周期数:
constexpr rep count() const;
time_point时间点
函数原型如下:
// 定义于头文件 <chrono>
template<
class Clock,
class Duration = typename Clock::duration
> class time_point;
Clock:此时间点在此时钟上计量 Duration:用于计量从纪元起时间的 stdduration 类型
构造函数
// 1. 构造一个以新纪元(epoch,即:1970.1.1)作为值的对象,需要和时钟类一起使用,不能单独使用该无参构造函数
time_point();
// 2. 构造一个对象,表示一个时间点,其中d的持续时间从epoch开始,需要和时钟类一起使用,不能单独使用该构造函数
explicit time_point( const duration& d );
// 3. 拷贝构造函数,构造与t相同时间点的对象,使用的时候需要指定模板参数
template< class Duration2 >
time_point( const time_point<Clock,Duration2>& t );
它不能独自构造出来使用,必须配合clocks时钟类使用,clocks里面有一个time_point时间点成员类,可以调用里面的time_point类来使用
其他函数 在这个类中除了构造函数还提供了另外一个time_since_epoch()函数,用来获得1970年1月1日到time_point对象中记录的时间经过的时间间隔(duration),函数原型如下:
duration time_since_epoch() const;
时间点time_point对象和时间段对象duration之间还支持直接进行算术运算(即加减运算),时间点对象之间可以进行逻辑运算 其中 tp 和 tp2 是time_point 类型的对象, dtn 是duration类型的对象。
描述 | 操作 | 返回值类型 |
---|---|---|
成员函数 operator+= | tp += dtn | *this该时间点加上时间段后新的时间点 |
成员函数 operator-= | tp -= dtn | *this该事件点减去一个时间段后以前的时间点 |
非成员函数 operator+ | tp + dtn | time_point和+=类似 |
非成员函数 operator+ | dtn + tp | atime_point两个+相同,不过是左右交换位置 |
非成员函数 operator- | tp - dtn | time_point和-=类似 |
非成员函数 operator- | tp - tp2 | aduration两个时间点间的时间间隔 |
非成员函数 operator== | tp == tp2 | bool |
非成员函数 operator!= | tp != tp2 | bool |
非成员函数 operator< | tp < tp2 | bool |
非成员函数 operator> | tp > tp2 | bool |
非成员函数 operator>= | tp >= tp2 | bool |
非成员函数 operator<= | tp <= tp2 | bool |
clocks时钟
chrono库中提供了获取当前的系统时间的时钟类,包含的时钟一共有三种:
- system_clock:系统的时钟,系统的时钟可以修改,甚至可以网络对时,因此使用系统时间计算时间差可能不准。 假设8.00开始计时到8.02停止,若在计时期间(如:8.01)将系统时间调整到10.01这样计时结束后得到的时间间隔就是2小时2分钟。
- steady_clock:是固定的时钟,相当于秒表。开始计时后,时间只会增长并且不能修改,适合用于记录程序耗时
- high_resolution_clock:和时钟类 steady_clock 是等价的(是它的别名)。
在这些时钟类的内部有time_point、duration、Rep、Period等信息,基于这些信息来获取当前时间,以及实现time_t和time_point之间的相互转换。
时钟类成员类型 | 描述 |
---|---|
rep | 表示时钟周期次数 |
period | 表示时钟计次周期的 std::ratio 类型 |
duration | 时间间隔,可以表示负时长 |
time_point | 表示在当前时钟里边记录的时间点 |
system_clock
时钟类system_clock是一个系统范围的实时时钟。system_clock提供了对当前时间点time_point的访问,将得到时间点转换为time_t类型的时间对象,就可以基于这个时间对象获取到当前的时间信息了。
struct system_clock { // wraps GetSystemTimePreciseAsFileTime/GetSystemTimeAsFileTime
using rep = long long;
using period = ratio<1, 10'000'000>;//100 nanoseconds
using duration = chrono::duration<rep, period>;
using time_point = chrono::time_point<system_clock>;
static constexpr bool is_steady = false;//该时钟是否是一个稳定的时钟
//返回当前时间点
_NODISCARD static time_point now() noexcept{
return time_point(duration(_Xtime_get_ticks()));
}
//将时间点转换成一个时间长度(从1970年1月1日到指定时间点总共经历多少秒)
_NODISCARD static __time64_t to_time_t(const time_point& _Time)noexcept {
return duration_cast<seconds>(_Time.time_since_epoch()).count();
}
//将时间长度转换成时间点
_NODISCARD static time_point from_time_t(__time64_t _Tm) noexcept{ //
return time_point{seconds{_Tm}};
}
};
rep:时钟周期次数是通过整形来记录的long long period:一个时钟周期是100纳秒ratio<1, 10'000'000> duration:时间间隔为rep*period纳秒chronotime_point<system_clock>,里面记录了新纪元时间点
关于此三个静态函数,在外部提供的api函数和内部定义的不太一样
// 返回表示当前时间的时间点。
static std::chrono::time_point<std::chrono::system_clock> now() noexcept;
// 将 time_point 时间点类型转换为 std::time_t 类型
static std::time_t to_time_t( const time_point& t ) noexcept;
// 将 std::time_t 类型转换为 time_point 时间点类型
static std::chrono::system_clock::time_point from_time_t( std::time_t t ) noexcept;
示例:
system_clock::time_point epoch;
duration<int,ratio<60*60*24> > hours(10);//时间段为10天
system_clock::time_point epoch1=epoch+hours;
system_clock::time_point epoch2(epoch+hours);
cout << "Epoch: " << system_clock::to_time_t(epoch) << endl;
cout << "Epoch1: " << system_clock::to_time_t(epoch1) << endl;
cout << "Epoch2: " << system_clock::to_time_t(epoch2) << endl;
system_clock::time_point now=system_clock::now();
time_t allsec=system_clock::to_time_t(now);
cout << "Now: " << allsec << endl;
cout << "Now: " << ctime(&allsec) << endl;
steady_clock
steady_clock相当于秒表,只要启动就会进行时间的累加,并且不能被修改,非常适合于进行耗时的统计。 steady_clock时钟类在底层源码中的定义如下:
struct steady_clock { // wraps QueryPerformanceCounter
using rep = long long;
using period = nano;
using duration = nanoseconds;
using time_point = chrono::time_point<steady_clock>;
static constexpr bool is_steady = true;//时钟是稳定的
// get current time
_NODISCARD static time_point now() noexcept
{
// doesn't change after system boot
const long long _Freq = _Query_perf_frequency();
const long long _Ctr = _Query_perf_counter();
static_assert(period::num == 1, "This assumes period::num == 1.");
const long long _Whole = (_Ctr / _Freq) * period::den;
const long long _Part = (_Ctr % _Freq) * period::den / _Freq;
return time_point(duration(_Whole + _Part));
}
};
- rep:时钟周期次数是通过整形来记录的long long
- period:一个时钟周期是1纳秒nano
- duration:时间间隔为1纳秒nanoseconds
- time_point:时间点通过系统时钟做了初始化chrono::time_point<steady_clock>
在这个类中也提供了一个静态的now()方法,用于得到当前的时间点,函数原型如下:
static std::chrono::time_point<std::chrono::steady_clock> now() noexcept;
使用示例:
steady_clock::time_point p1= steady_clock::now();
for (int i = 0; i < 1000; i++) {
cout<<"八嘎小姐我喜欢你"<<endl;
}
steady_clock::time_point p2= steady_clock::now();
duration<int,ratio<1,1000000000> > d= p2-p1;
cout<<"耗时"<<d.count()<<"纳秒"<<endl;
high_resolution_clock
high_resolution_clock提供的时钟精度比system_clock要高,它也是不可以修改的。在底层源码中,这个类其实是steady_clock类的别名。
using high_resolution_clock = steady_clock;
且内部只提供了一个now()方法 因此high_resolution_clock的使用方式和steady_clock是一样的,在此就不再过多进行赘述了。
转换函数
这些转换函数虽然都是那些类加上cast但并不属于那个类,只是一个转换函数
duration_cast
通过这个函数可以对duration类对象内部的时钟周期Period,和周期次数的类型Rep进行修改,该函数原型如下:
template <class ToDuration, class Rep, class Period>
constexpr ToDuration duration_cast (const duration<Rep,Period>& dtn);
duration_cast<要转换成的类型>()
- 如果是对时钟周期进行转换:源时钟周期必须能够整除目的时钟周期(比如:小时到分钟)。
- 如果是对时钟周期次数的类型进行转换:低等类型默认可以向高等类型进行转换(比如:int 转 double)。但double不能转换成int,高精度转低精度会损失数据,对数据不安全
- 如果时钟周期和时钟周期次数类型都变了,根据第二点进行推导(也就是看时间周期次数类型)。
- 以上条件都不满足,那么就需要使用 duration_cast 进行显示转换。
//分钟->小时
chrono::hours h=duration_cast<hours>( chrono::minutes(60));
//浮点->整形,但会丢失精度
chrono::seconds s = duration_cast<seconds>(duration<double>(2.5));
time_point_cast
函数的作用是对时间点进行转换,因为不同的时间点对象内部的时钟周期Period,和周期次数的类型Rep可能也是不同的,一般情况下它们之间可以进行隐式类型转换,也可以通过该函数显示的进行转换,函数原型如下:
template <class ToDuration, class Clock, class Duration>
time_point<Clock, ToDuration> time_point_cast(const time_point<Clock, Duration> &t);
示例:
time_point<high_resolution_clock,seconds> time_point_sec(seconds(6));
time_point<high_resolution_clock ,milliseconds> time_point_ms(time_point_sec);
time_point_sec= time_point_cast<seconds>(time_point_ms);
线程局部存储
操作系统和编译器对线程局部存储的支持
线程局部存储是指对象内存在线程开始后分配,线程结束时回收且每个线程有该对象自己的 实例,简单地说,线程局部存储的对象都是独立于各个线程的。实际上,这并不是一个新鲜的概 念,虽然C++一直没有在语言层面支持它,但是很早之前操作系统就有办法支持线程局部存储 了。
由于线程本身是操作系统中的概念,因此线程局部存储这个功能是离不开操作系统支持的。 而不同的操作系统对线程局部存储的实现也不同,以至于使用的系统API也有区别,这里主要以 Windows和Linux为例介绍它们使用线程局部存储的方法。
在Windows中可以通过调用API函数TlsAlloc来分配一个未使用的线程局部存储槽索引(TLS slot index),这个索引实际上是Windows内部线程环境块(TEB)中线程局部存储数组的索引。 通过API函数TlsGetValue与TlsSetValue可以获取和设置线程局部存储数组对应于索引元素的值。 API函数TlsFree用于释放线程局部存储槽索引。
Linux使用了pthreads(POSIX threads)作为线程接口,在pthreads中我们可以调用 pthread_key_create与pthread_key_delete创建与删除一个类型为pthread_key_t的键。利用这个键可 以使用pthread_setspecific函数设置线程相关的内存数据,当然,我们随后还能够通过 pthread_getspecific函数获取之前设置的内存数据。
在C++11标准确定之前,各个编译器也用了自定义的方法支持线程局部存储。比如gcc和 clang添加了关键字__thread来声明线程局部存储变量,而Visual Studio C++则是使用 __declspec(thread) 。虽然它们都有各自的方法声明线程局部存储变量,但是其使用范围和规则却 存在一些区别,这种情况增加了C++的学习成本,也是C++标准委员会不愿意看到的。于是在 C++11标准中正式添加了新的thread_local说明符来声明线程局部存储变量。
thread_local说明符
thread_local说明符可以用来声明线程生命周期的对象,它能与static或extern结合,分别指 定内部或外部链接,不过额外的static并不影响对象的生命周期。换句话说,static并不影响其线 程局部存储的属性
struct X {
thread_local static int i;
};
thread_local X a;
int main() {
thread_local X b;
}
声明一个线程局部存储变量相当简单,只需要在普通变量声明上添 加thread_local说明符。被thread_local声明的变量在行为上非常像静态变量,只不过多了线程属 性,当然这也是线程局部存储能出现在我们的视野中的一个关键原因,它能够解决全局变量或者 静态变量在多线程操作中存在的问题,一个典型的例子就是errno。
errno通常用于存储程序当中上一次发生的错误,早期它是一个静态变量,由于当时大多数程 序是单线程的,因此没有任何问题。但是到了多线程时代,这种errno就不能满足需求了。设想一下,一个多线程程序的线程A在某个时刻刚刚调用过一个函数,正准备获取其错误码,也正是这个 时刻,另外一个线程B在执行了某个函数后修改了这个错误码,那么线程A接下来获取的错误码自 然不会是它真正想要的那个。这种线程间的竞争关系破坏了errno的准确性,导致不可确定的结 果。为了规避由此产生的不确定性,POSIX将errno重新定义为线程独立的变量,为了实现这个定 义就需要用到线程局部存储,直到C++11之前,errno都是一个静态变量,而从C++11开始errno 被修改为一个线程局部存储变量。
在了解了线程局部存储的意义之后,让我们回头仔细阅读其定义,会发现线程局部存储只是 定义了对象的生命周期,而没有定义可访问性。也就是说,我们可以获取线程局部存储变量的地 址并将其传递给其他线程,并且其他线程可以在其生命周期内自由使用变量。不过这样做除了用 于诊断功能以外没有实际意义,而且其危险性过大,一旦没有掌握好目标线程的声明周期,就很 可能导致内存访问异常,造成未定义的程序行为,通常情况下是程序崩溃。
使用取地址运算符&取到的线程局部存储变量的地址是运行时被计算出来的, 它不是一个常量,也就是说无法和constexpr结合
线程局部存储对象的初始化和销毁
在同一个线程中,一个线程局部存储对 象只会初始化一次,即使在某个函数中被多次调用。这一点和单线程程序中的静态对象非常相 似。相对应的,对象的销毁也只会发生一次,通常发生在线程退出的时刻。
贡献者
版权所有
版权归属:PinkDopeyBug