C++一些新属性以及简单的使用例子

发布时间 2023-12-20 14:12:31作者: 蔡头一枚

(1)线程
执行处理器调度的基本单位。程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。
(2)进程 资源分配的基本单位,也可能作为调度运行的单位。进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。


C++11 新特性
C++11新标准多线程支持库
< thread > : 提供线程创建及管理的函数或类接口;
< mutex > : 为线程提供获得独占式资源访问能力的互斥算法,保证多个线程对共享资源的同步访问;
< condition_variable > : 允许一定量的线程等待(可以定时)被另一线程唤醒,然后再继续执行;
< future > : 提供了一些工具来获取异步任务(即在单独的线程中启动的函数)的返回值,并捕捉其所抛出的异常;
< atomic > : 为细粒度的原子操作(不能被处理器拆分处理的操作)提供组件,允许无锁并发编程。


#############线程 (编译时要带 -lpthread)
(1). std::thread
std::thread(方法, ...) // 后面匹配的带参跟线程函数的参数个数以及类型一致
例如:
void print(){}
std::thread(print); // 创建一个线程,线程的运行方法是print, 无参数

int print(int num, void *arg){ return 0; }
int num = 8;
std::thread(print, num, &num); // 创建一个线程,线程的执行方法是print, thread()的参数跟print的参数的类型要一致


#############互斥锁
(2). std::mutex
lock_guard (在构造函数里加锁,在析构函数里解锁), 不支持手动解锁, 只能等待作用域执行完成后自动解锁, 轻量级锁
unique_lock 自动加锁、解锁, 可支持手动解锁, 锁住的作用域可选择; 重量级锁(支持的功能多)
lock_guard 是不可移动的(moveable),即不能拷贝、赋值、移动,只能通过构造函数初始化和析构函数销毁,unique_lock 是可移动的,可以拷贝、赋值、移动。

例如:
std::mutex mutex;
{
// 上锁,临界区资源操作, {}作用域完成后自动解锁
lock_guard<std::mutex> gLock(mutex);
}

{
// 上锁,临界区资源操作, {}作用域完成前可手动解锁,作用域完成后即使没有解锁也会自动解锁
unique_lock<std::mutex> uniqueLock(mutex);
uniqueLock.unlock();

}


####################条件变量
std::condition_variable
条件变量结合互斥锁使用, 提高多线程的效率, 线程在等待时, 进入休眠状态, 等到唤醒之后再从CPU调度出来
常用方法:
wait(); // 结合unique_lock锁使用, 表示改线程进入休眠等待
notify_all(); //唤醒全部线程
notiyfy_one(); // 唤醒默认等待的线程, 不确定是唤醒哪一个

例如:
std::mutex m_mutex;
std::condition_variable m_cv;
std::atomic<bool> m_atomic(false);

void workThread(int id)
{
std::unique_lock<std::mutex> uqLock;
while ( !m_atomic.load() )
{
m_cv.wait(unlock); // 阻塞等待
}

std::cout <<" Thread " << id << " Run.\n";
}

int main(void)
{
std::thread th[5];
for ( int i=0; i<5; i++ )
{
th[i] = std::thread(workThread, i);
}

std::this_thread::sleep_for(std::chrono::milliseconds(5));
// 唤醒全部线程
m_cv.notify_all();

for ( auto &obj: th )
{
obj.join();
}
return 0;
}

 

#############原子操作(多线程并发操作变量, 数据保护处理, 同步处理, 可避免互斥锁的使用,提高效率)
std::atomic_flag
atomic_flag 一种简单的原子布尔类型,只支持两种操作,test-and-set 和 clear。
如果在初始化时没有明确使用 ATOMIC_FLAG_INIT初始化,那么新创建的 std::atomic_flag 对象的状态是未指定的(unspecified)(既没有被 set 也没有被 clear。)
另外,atomic_flag不能被拷贝,也不能 move 赋值。
ATOMIC_FLAG_INIT: 如果某个 std::atomic_flag 对象使用该宏初始化,那么可以保证该 std::atomic_flag 对象在创建时处于 clear 状态。

std::atomic<T>
大家可以把原子操作理解成一种:不需要用到互斥量加锁(无锁)技术的多线程并发编程方式
原子操作:在多线程中 不会被打断的 程序执行片段;原子操作,比互斥量效率上更胜一筹。
互斥量的加锁一般是针对一个代码段(几行代码),而原子操作针对的一般是一个变量,而不是一个代码段;
原子操作: 一般都是指"不可分割的操作";也就是说这种操作状态要么是完成的,要么是没完成的,不可能出现半完成状态;

成员函数:
store 用非原子参数替换原子对象的值
load 获取原子对象的值
exchange 交换两个原子对象的值

例如:
// 两个线程交替给全局变量赋值
std::atomic<int> m_atoNum(0);
int thread3(int num)
{
for ( int i=0; i<8; i++ )
{
m_atoNum++;
printf("thread %d - m_atoNum: %d\n", num, m_atoNum.load());
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 0;
}

int main(void)
{
std::thread th1(thread3, 0);
std::thread th2(thread3, 1);
th1.join();
th2.join();
return 0;
}

#########################RALL机制
RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,
最后在对象析构的时候,释放构造时获取的资源
我们在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就别销毁了;当这个变量是类对象时,这个时候,就会自动调用这个类的析构函数,而这一切都是自动发生的,不要程序员显示的去调用完成。这个也太好了,RAII就是这样去完成的。
由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了.

1. 资源创建
2. 资源使用
3. 资源销毁

关于互斥锁和条件变量:
互斥量可以保护共享数据的修改,如果线程正在等待共享数据的某个条件出现,仅用互斥量的话就需要反复对互斥对象锁定解锁,以检查值的变化,这样将频繁查询的效率非常低。
条件变量可以让等待共享数据条件的线程进入休眠,并在条件达成时唤醒等待线程,提供一种更高效的线程同步方式。条件变量一般和互斥锁同时使用,提供一种更高效的线程同步方式。