Posts Modern-C++_并发
Post
Cancel

Modern-C++_并发

并发编程

通俗地说,“并发”是指在一个时间段里有多个操作在同时进行,而“多线程”是实现并发的一种方式。

多线程

线程概念

C++而言,线程就是一个能够独立运行的函数:

1
2
3
4
5
6
auto f = []()  // 定义一个lambda表达式
{
  cout << "thread id:" << this_thread::get_id() << endl;
};

thread t(f);  // 启动一个线程:运行函数f

多线程开发

基于基础的thread线程类的简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mutex out_lock;

void func(const char* name)
{
  thid_thread::sleep_for(100ms);
  lock_guard<mutex> guard{
    out_lock;
  };
  cout << "Thread: " << name << endl;
}

int main(){
  thread t1{func, "A"};
  thread t2{func, "B"};
  t1.join();
  t2.join();
}

可以看到几个细节:

  1. thread要求析构之前需要join,要么detach(放弃线程管理),否则程序会异常退出;
  2. 使用互斥量(mutex)锁定cout,否则输出会交织在一起;互斥量特性:单个互斥量只能被一个线程锁定。mutex只可默认构造,不可拷贝(或移动),不可赋值,提供的方法:
    1. lock:锁定,当锁被其他线程获得时则堵塞执行;
    2. try_lock:尝试锁定,获得锁则返回true,否则返回false
    3. unlock:解除锁定(获取锁的时候调用)。

上述例子没有返回数据,如果需要在某个线程执行后取回结果,则需要使用信号量/条件变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void work(condition_variable& cv,
         	mutex& cv_mut,
         	bool& result_ready,
         	int& result)
{
  this_thread::sleep_for(2s)
  result = 66;
  {
    unique_lock lock{cv_mut};
  	result_ready = true;
  }
  cv.notify_one();
}

int main()
{
  condition_variable cv;  // 条件变量
  mutex cv_mut; // 互斥量
  bool result_ready = false; // 结果状态变量
  int result; // 结果变量
  
  thread t1{work, ref(cv), ref(cv_mut), 
           	ref(result_ready), ref(result)}; // ref声明引用输入
  
  cout << "Waiting for something." << endl;
  unique_lock lock{cv_mut}; // 单一锁
  cv.wait(lock, [&] {
    return result_ready;
  });
  cout << "Answer: " << result << endl;
  t1.join();
}

可以看到,为了返回函数结果,需要额外定义:条件变量单一锁结果变量状态结果变量,相对使用复杂,可以用async来简化(后文重点介绍)。

编程技巧

  1. C++多线程编程读取const变量是安全的:多用const关键字,“读而不写”就不会有数据竞争

  2. 保持“仅调用一次”:防止初始化函数多次运行,可以通过声明once_flag类型变量,最好是静态、全局的(所以线程可见),作为初始化的标志:

  3. 线程局部存储(thread local storage):通过thread_local实现,标记的变量在每个线程中都会有一个独立的副本,即“线程独占”:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    thread_local int n = 0; // 线程局部存储变量
    auto f = [&](int x) // 线程函数,捕获引用变量
    {
      n += x;
      cout << n;
    }
       
    int main(){
      thread t1{f, 10};
      thread t2{f, 20}; // 最终输出为10/20,互不干扰
    }
    
  4. 原子变量:原子变量在多线程的含义是:“不可分”,即操作要么完成,要么不可完成,不可被其他操作打断,所以不存在竞争读写的问题。但是不是所有的操作都可以原子化的,只存在一些基本的类型原子化,如:atomic_int、atomic_long等:

    1
    2
    3
    
    using atomic_bool = std::atomic<bool>;
    using atomic_int = std::atomic<int>;
    using atomic_long = std::atomic<long>;
    

    原子变量本身是通过模板类包装了原始类型,接口都是一致的,但是禁用了拷贝构造函数,即不可用“=”赋值,只能用圆括号/花括号

    原子变量的最基本用法是:作为线程安全的全局计数器/标志位:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    static atomic_flag flag {false};
    static atomic_int n;
       
    auto f = [&](){
      auto value = flag.test_and_set(); // TAS检查原子标志量
         
      if (value) 
        cout << "flag has been set." << endl;
      else
        cout << "set flag by" << this_thread::get_id() << endl;
      n += 100; // 原子变量加法运算
      this_thread::sleep_for(n.load() * 10ms); // 使用时间字面量
    };
       
    thread t1(f);
    thread t2(f);
       
    t1.join(); // 等待线程结束
    t2.join();
    

Async

直接调用thread是相对“原始”的使用方式,使用也更加复杂,可以使用async()函数,如上文例子:

1
2
3
4
5
6
7
8
9
10
11
int work()
{
  this_thread::sleep_for(2s);
  return 66;
}

int main(){
  auto fut = async(launch::async, work); // launch调用运行策略:新线程调用
  cout << "Waiting for something." << endl;
  cout << "Answer: " << fut.get() << endl; //get方法只能调用一次,采用移动方法
}

注意:如果你不显式获取 async()的返回值(即future对象),它就会同步阻塞直至任务完成(由于临时对象的析构函数),于是async就变成了sync。如果不关心返回值,则可以用auto来避免:

1
2
std::async(task, ...); // 没有显式获取future,被同步阻塞
auto f = std::async(task, ...); // 只有上一个任务完成后才能被执行

Promise

Promise是另一种使用方式,称之为“承诺量”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void work(promise<int> prom)
{
  this_thread::sleep_for(2s);
  prom.set_value(66);
}

int main()
{
  promise<int> prom;
  auto fut = prom.get_future();
  thread t1{work, move(prom)};
  cout << "Waiting for something." << endl;
  cout << "Answer: " << fut.get() << endl;
}

promisefuture需要成对出现:相当于promise负责数据在线程的移动,future负责数据获取,不需要考虑返回数据的生命周期管理。

一组promisefuture只能使用一次,既不能重复设,也不能重复取。

This post is licensed under CC BY 4.0 by the author.

LLMs位置编码

Modern-C++_工程实践