[学习][记录] 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…: 函数实参

相关文章
|
5天前
|
编译器 C++ 开发者
C++一分钟之-C++20新特性:模块化编程
【6月更文挑战第27天】C++20引入模块化编程,缓解`#include`带来的编译时间长和头文件管理难题。模块由接口(`.cppm`)和实现(`.cpp`)组成,使用`import`导入。常见问题包括兼容性、设计不当、暴露私有细节和编译器支持。避免这些问题需分阶段迁移、合理设计、明确接口和关注编译器更新。示例展示了模块定义和使用,提升代码组织和维护性。随着编译器支持加强,模块化将成为C++标准的关键特性。
21 3
|
6天前
|
安全 JavaScript 前端开发
C++一分钟之-C++17特性:结构化绑定
【6月更文挑战第26天】C++17引入了结构化绑定,简化了从聚合类型如`std::tuple`、`std::array`和自定义结构体中解构数据。它允许直接将复合数据类型的元素绑定到单独变量,提高代码可读性。例如,可以从`std::tuple`中直接解构并绑定到变量,无需`std::get`。结构化绑定适用于处理`std::tuple`、`std::pair`,自定义结构体,甚至在范围for循环中解构容器元素。注意,绑定顺序必须与元素顺序匹配,考虑是否使用`const`和`&`,以及谨慎处理匿名类型。通过实例展示了如何解构嵌套结构体和元组,结构化绑定提升了代码的简洁性和效率。
19 5
|
6天前
|
存储 安全 编译器
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
33 5
|
6天前
|
算法 安全 编译器
【C++航海王:追寻罗杰的编程之路】C++11(四)
【C++航海王:追寻罗杰的编程之路】C++11(四)
13 0
|
6天前
|
存储 安全 程序员
【C++航海王:追寻罗杰的编程之路】C++11(一)
【C++航海王:追寻罗杰的编程之路】C++11(一)
13 0
【C++航海王:追寻罗杰的编程之路】C++11(一)
|
4天前
|
程序员 编译器 C++
探索C++语言宝库:解锁基础知识与实用技能(类型变量+条件循环+函数模块+OOP+异常处理)
探索C++语言宝库:解锁基础知识与实用技能(类型变量+条件循环+函数模块+OOP+异常处理)
8 0
|
6天前
|
编译器 C++ 容器
【C++航海王:追寻罗杰的编程之路】C++11(三)
【C++航海王:追寻罗杰的编程之路】C++11(三)
6 0
|
6天前
|
存储 编译器 C++
【C++航海王:追寻罗杰的编程之路】C++11(二)
【C++航海王:追寻罗杰的编程之路】C++11(二)
10 0
|
6天前
|
Unix 编译器 C语言
【C++航海王:追寻罗杰的编程之路】关键字、命名空间、输入输出、缺省、重载汇总
【C++航海王:追寻罗杰的编程之路】关键字、命名空间、输入输出、缺省、重载汇总
8 0
|
7天前
|
存储 安全 编译器
【C++】:函数重载,引用,内联函数,auto关键字,基于范围的for循环,nullptr关键字
【C++】:函数重载,引用,内联函数,auto关键字,基于范围的for循环,nullptr关键字
10 0