[学习][记录] c++语言:从放弃到入门 <一> c++11新关键字以及引入的新特性(终)

简介: [学习][记录] c++语言:从放弃到入门 <一> c++11新关键字以及引入的新特性

二十四、 Thread框架

24.1 join 与 detach

t.join 和 t.detach 标志着,线程对象和线程的关系。t.join 表识,线程与线程对象 的同步关系。而 t.detach 表识,线程与线程对象的异步关系。

join 是阻塞的。

注意:主线程结束后 detach() 可能会还没运行就销毁了

24.2 传参方式

线程,有自己独立的栈。可以共享全局的变量。在线程启动的时候可以传入启动参数。

1.传值

std::thread threadTest(func,arg1,arg2,…);

2.传引用

std::thread threadTest(func,std::ref(arg1),std::ref(arg2));

24.3 常用函数

join()  //阻塞运行线程
joinable() // 线程是否阻塞的
detach() // 异步运行
sdt::ref() //引用化

24.4 同步之mutex

24.5 volatile

修饰变量,此变量可能被多线程访问和修改,加上此关键字,可以避免编译器优化,不使用存储在寄存器中的值,而是每次都去内存里去读。

24.6 lock(),unlock()

对某作用域加锁,解锁,但如果作用域抛异常可能会导致解锁失败,产生死锁。

可以控制加锁粒度。

24.7 try_lock(),unlock()

尝试加锁,加锁失败会返回false,

try_lock()

1.如果互斥锁当前未被任何线程锁定,则调用线程将其锁定(从此点开始,直到调用其成员解锁,该线程拥有互斥锁)。

2.如果互斥锁当前被另一个线程锁定,则该函数将失败并返回false,而不会阻塞(调用线程继续执行)。

3.如果互斥锁当前被调用此函数的同一线程锁定,则会产生死锁(具有未定义的行为)。 请参阅recursive_mutex以获取允许来自同一线程的多个锁的互斥锁类型。

24.8 std::lock_guard()

自动锁,声明范围内进行自动加锁解锁操作

在 lock_guard 对象构造时,传入的 Mutex 对象(即它所管理的 Mutex 对象) 会被当前线程锁住。在 lock_guard

对象被析构时,它所管理的 Mutex 对象会自动解 锁,由于不需要程序员手动调用 lock 和 unlock 对 Mutex

进行上锁和解锁操作, 因此这也是最简单安全的上锁和解锁方式,尤其是在程序抛出异常后先前已被上锁的 Mutex

对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常 处理代码。

#include <iostream> 
#include <thread> 
#include <mutex> 
using namespace std; 
mutex mtx; 
void printEven(int i) { 
if( i%2 == 0) cout<< i <<" is even"<<endl; 
else
   throw logic_error("not even"); 
}
void printThreadId(int id) { 
  try{
  lock_guard<mutex> lck(mtx); //栈自旋 抛出异常时栈对象自我析构。          
  printEven(id);
//    mtx.lock(); 
    //printEven(id); 
    //mtx.unlock(); 
  }catch(logic_error & ){ 
    cout<<"exception caught"<<endl; 
  } 
}
int main() { 
  thread ths[10]; //spawn 10 threads 
  for(int i=0; i<10; i++) { 
    ths[i] = thread(printThreadId,i+1); 
  }
  for(auto & th: ths) 
    th.join(); 
  return 0; 
}

24.9 死锁

死锁的原因是,container 试图多次去获取锁己获得的锁。

std::recursive_mutex 允 许多次获取相同的 mutex。

C++中 STL 中的容器,是非线程安全的。

24.10 std::recursive_mutex()

递归锁

  1. 调用线程从成功调用 lock 或 try_lock 开始占有recursive_mutex, 期间线程可以进行对 lock 或 try_lock的附加调用,所有权在线程调用 unlock 匹配次数时结束。
  2. 线程占有recursive_mutex时,若其他线程要求recursive_mutex所有权,调用lock将被阻塞,调用try_lock将返回false.
  3. 可锁定recursive_mutex的最大次数未指定的,但到达该数后,对 lock 的调用将抛出 std::system_error 而对 try_lock 的调用返回false;
  4. 若recursive_mutex在仍被线程占有时被销毁,则程序行为未定义。recursive_mutex满足 mutex 和 标准布局类型的所有要求。

24.11 同步之std::condition_variable

条件变量,多线程中对变量操作时对变量进行条件判断 从而当前线程需要是否阻塞或者被唤醒

条件变量(condition variable)是利用线程间共享的全局变量进行同步的一种机制,

主要包括两个动作:

一个线程等待某个条件为真,而将自己挂起;

另一个线程使的条件成立,并通知等待的线程继续。

为了防止竞争,条件变量的使用总是和一个互斥锁 结合在一起

C++11 中引入了条件变量,其相关内容均在<condition_variable>中。

这里主要 介绍 std::condition_variable 类。 条件变量 std::condition_variable 用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程。std::condition_variable 需要与 std::unique_lock 配合使用。 std::condition_variable 效果上相当于包装了 pthread 库中的 pthread_cond_*()系列 的函数。

当 std::condition_variable 对 象 的 某 个 wait 函 数 被 调 用 的 时 候 , 它 使 用 std::unique_lock(通过 std::mutex)来锁住当前线程。当前线程会一直被阻塞,直到另 外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒 当前线程。

24.11.1 成员函数

(1)、构造函数: 仅支持默认构造函数,拷贝、赋值和移动(move)均是被禁用的。

(2)、wait: 当前线程调用 wait()后将被阻塞,直到另外某个线程调用 notify_*唤 醒当前线程;当线程被阻塞时,该函数会自动调用 std::mutex 的 unlock()释放锁,使

得其它被阻塞在锁竞争上的线程得以继续执行。一旦当前线程获得通知(notify,通常 是另外某个线程调用

notify_*唤醒了当前线程),wait()函数也是自动调用 std::mutex 的 lock()。wait

分为无条件被阻塞和带条件的被阻塞两种。 无条件被阻塞:调用该函数前,当前线程应该已经对 unique_lock lck

完成了加锁。所有使用同一个条件变量的线程必须在 wait 函数中使用同一个 unique_lock。该 wait

函数内部会自动调用 lck.unlock()对互斥锁解锁, 使得其他被阻塞在互斥锁上的线程恢复执行。使用本函数被阻塞的当前线程在获得通知

(notified,通过别的线程调用 notify_*系列的函数)而被唤醒后,wait()函数恢复执行 并自动调用

lck.lock()对互斥锁加锁。

带条件的被阻塞: wait 函数设置了谓词(Predicate),只有当 pred 条件为 false 时 调用该 wait 函数才会阻塞当前线程,并且在收到其它线程的通知后只有当 pred 为 true 时才会被解除阻塞。因此,等效于 while

(!pred()) wait(lck)

(3)、wait_for: 与 wait()类似,只是 wait_for 可以指定一个时间段,在当前线程 收到通知或者指定的时间超时之前,该线程都会处于阻塞状态。而一旦超时或者收到了 其它线程的通知,wait_for 返回,剩下的步骤和 wait

类似。

(4)、wait_until: 与 wait_for 类似,只是 wait_until 可以指定一个时间点,在当 前线程收到通知或者指定的时间点超时之前,该线程都会处于阻塞状态。而一旦超时或 者收到了其它线程的通知,wait_until

返回,剩下的处理步骤和 wait 类似。

(5)、notify_all: 唤醒所有的 wait 线程,如果当前没有等待线程,则该函数什么也 不做。

(6)、notify_one: 唤醒某个 wait 线程,如果当前没有等待线程,则该函数什么也 不做;如果同时存在多个等待线程,则唤醒某个线程是不确定的(unspecified)。

24.12 std::unique_lock()

独占锁,当前锁作用域结束之前,其余线程无法使用mutex。

std::unique_lock对象以独占所有权的方式(uniqueowership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁。

unique_lock具有lock_guard的所有功能,而且更为灵活。虽然二者的对象都不能复制,但是unique_lock可以移动(movable),因此用unique_lock管理互斥对象,可以作为函数的返回值,也可以放到STL的容器中。

24.13 std::atomic

c++11提供了原子类型std::atomic,理论上这个T可以是任意类型。

整形有这种原子变量已经足够方便,就不需要使用std::mutex来保护该变量啦。

struct OriginCounter { // 普通的计数器
  int count = 0;
  std::mutex mutex_;
  void add() {
    std::lock_guard<std::mutex> lock(mutex_);
    ++count;
  }
  void sub() {
    std::lock_guard<std::mutex> lock(mutex_);
    --count;
  }
  int get() {
    std::lock_guard<std::mutex> lock(mutex_);
    return count;
  }
};
struct NewCounter { // 使用原子变量的计数器
  std::atomic<int> count = 0;
  void add() {
    ++count;
    // count.store(++count);这种方式也可以
  }
  void sub() {
    --count;
    // count.store(--count);
  }
  int get() {
    return count.load();
  }
};
void main()
{
  NewCounter counter;
  std::thread thread1([&counter]() {
    int nTime = 10;
    while (nTime--)
    {
      counter.add();
      printf("thread1:%d\n", counter.get());
    }
  });
  thread1.detach();
  std::thread thread2([&counter]() {
    int nTime = 10;
    while (nTime--)
    {
      counter.add();
      printf("thread2:%d\n", counter.get());
    }
  });
  thread2.join();
  return;
}
thread1:1
thread1:2
thread1:4
thread1:5
thread1:6
thread1:7
thread1:8
thread1:9
thread1:10
thread1:11
thread2:3
thread2:12
thread2:13
thread2:14
thread2:15
thread2:16
thread2:17
thread2:18
thread2:19
thread2:20

24.14 std::call_once

c++11提供了std::call_once来保证某一函数在多线程环境中只调用一次,它需要配合std::once_flag使用。

类 std::once_flag 是 std::call_once 的辅助类。

传递给多个 std::call_once 调用的 std::once_flag 对象允许那些调用彼此协调,从而只令调用之一实际运行完成。

std::once_flag 既不可复制亦不可移动。

总之避免,某函数被多个对象调用,多个对象调用std::call_once时传入std::once_flag来判断该对象是否能调用目标函数。

#include <thread>
#include <mutex>
#include <iostream>
std::once_flag onceflag;
void CallOnce() {
  std::call_once(onceflag, []() {
    std::cout << "call once" << std::endl;
  });
}
int main() {
  std::thread threads[5];
  for (int i = 0; i < 5; ++i) {
    threads[i] = std::thread(CallOnce);
  }
  for (auto& th : threads) {
    th.join();
  }
  return 0;
}
call once

24.15 volatile相关

volatile修饰过的变量,编译器对访问该变量的代码通常不再进行优化。

24.16 异步相关

std::future

std::future用于访问异步操作的结果,从翻译上来讲就是将来的值,future.get()时会阻塞,如果有值,就立刻返回值;如果没有,则阻塞当前线程当前位置,直到有值,也就是所谓的异步;

std::promise

std::promise内部有个future 类似

class  Promise{
public:
  Future* m_pFuture;
}

std::promise 主要用来传递future包含的信息的 包括信息的set_value()、get_future()

当需要获取线程中的某个值,可以使用std::promise

std::packaged_task

当需要获取线程函数返回值,可以使用std::packaged_task。

std::packaged_task: (1).禁用拷贝赋值。(2).支持移动赋值。

std::promise与std::future配合使用

#include <functional>
#include <future>
#include <iostream>
#include <thread>
#include <windows.h>
int main() {
  std::promise<int> prom;
  std::future<int> fut = prom.get_future();
  std::thread thread1([&fut]() {
    int startCount = GetTickCount();
    printf("thread1 start :%d\n", startCount);
    int x = fut.get();
    std::cout << "value: " << x << std::endl;
    int endCount = GetTickCount();
    printf("thread1 end :%d\n", endCount);
  });
  thread1.detach();
  std::thread thread2([&prom]() {
    int value = 144;
    printf("thread2 set_value :%d\n", value);
    prom.set_value(value);
  });
  thread2.join();
  system("pause");
  return 0;
}
thread1 start :32191656
thread2 set_value :144
value: 144
thread1 end :32191656

留意输出结果会发现 有一个输出顺序,thread1 先等待一会,等待thread2set_value之后thread1才继续输出。

就是get时没有获得到值所以阻塞中的过程。

std::packaged_task与std::future配合使用

#include <functional>
#include <future>
#include <iostream>
#include <thread>
using namespace std;
int main() {
  std::packaged_task<int(int)> task([](int in)->int {
    return in + 1;
  });
  std::future<int> fut = task.get_future();
  std::thread(std::move(task), 5).detach();//move是因为std::packaged_task不支持拷贝构造,支持移动构造。
  cout << "result " << fut.get() << endl;
  return 0;
}
result 6

std::future用于访问异步操作的结果,

而std::promise和std::packaged_task在future高一层,它们内部都有一个future,promise包装的是一个值,packaged_task包装的是一个函数,当需要获取线程中的某个值,可以使用std::promise,当需要获取线程函数返回值,可以使用std::packaged_task。

24.17 std::async

async是比future,packaged_task,promise更高级的东西,

它是基于任务的异步操作,通过async可以直接创建异步的任务,返回的结果会保存在future中,

不需要像packaged_task和promise那么麻烦,关于线程操作应该优先使用async。

#include <functional>
#include <future>
#include <iostream>
#include <thread>
#include <Windows.h>
using namespace std;
int main() {
  auto res = std::async(std::launch::async,[](int in)->int {
    int start = GetTickCount();
    printf("func start %d\n", start);
    Sleep(2000);
    int end = GetTickCount();
    int n = in + 1;
    printf("func value %d\n", n);
    printf("func end %d\n", end);
    return n;
  }, 5);
  // res.wait();
  int beforeTime1 = GetTickCount();
  printf("Main Thread beforeTime %d\n", beforeTime1);
  cout << "Get Value:"<<res.get() << endl; // 阻塞直到函数返回
  int afterTime1 = GetTickCount();
  printf("Main Thread afterTime1 %d\n", afterTime1);
  return 0;
}
Main Thread beforeTime 34517000
func start 34517000
func value 6
func end 34519015
Get Value:6
Main Thread afterTime1 34519015

语法

std::future<T> std::async(std::launch::async | std::launch::deferred, func, args...);

std::future< T >:返回参数

参数1 执行策略:

  • std::launch::async表示任务执行在另一线程
  • std::launch::deferred表示延迟执行任务,调用get或者wait时才会执行,不会创建线程,惰性执行在当前线程。

如果不明确指定创建策略,以上两个都不是async的默认策略,而是未定义,它是一个基于任务的程序设计,内部有一个调度器(线程池),会根据实际情况决定采用哪种策略。

参数2 func:即将执行的函数

参数args…: 函数实参

相关文章
|
2天前
|
存储 机器学习/深度学习 编译器
【C++终极篇】C++11:编程新纪元的神秘力量揭秘
【C++终极篇】C++11:编程新纪元的神秘力量揭秘
|
1月前
|
C++ 开发者
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
58 16
|
2月前
|
算法 网络安全 区块链
2023/11/10学习记录-C/C++对称分组加密DES
本文介绍了对称分组加密的常见算法(如DES、3DES、AES和国密SM4)及其应用场景,包括文件和视频加密、比特币私钥加密、消息和配置项加密及SSL通信加密。文章还详细展示了如何使用异或实现一个简易的对称加密算法,并通过示例代码演示了DES算法在ECB和CBC模式下的加密和解密过程,以及如何封装DES实现CBC和ECB的PKCS7Padding分块填充。
67 4
2023/11/10学习记录-C/C++对称分组加密DES
|
4月前
|
编译器 C语言 C++
配置C++的学习环境
【10月更文挑战第18天】如果想要学习C++语言,那就需要配置必要的环境和相关的软件,才可以帮助自己更好的掌握语法知识。 一、本地环境设置 如果您想要设置 C++ 语言环境,您需要确保电脑上有以下两款可用的软件,文本编辑器和 C++ 编译器。 二、文本编辑器 通过编辑器创建的文件通常称为源文件,源文件包含程序源代码。 C++ 程序的源文件通常使用扩展名 .cpp、.cp 或 .c。 在开始编程之前,请确保您有一个文本编辑器,且有足够的经验来编写一个计算机程序,然后把它保存在一个文件中,编译并执行它。 Visual Studio Code:虽然它是一个通用的文本编辑器,但它有很多插
|
4月前
|
算法 C++
2022年第十三届蓝桥杯大赛C/C++语言B组省赛题解
2022年第十三届蓝桥杯大赛C/C++语言B组省赛题解
99 5
|
4月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
42 0
|
4月前
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
49 0
|
2天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
50 13