C++ 多线程知识汇总

https://zhuanlan.zhihu.com/p/194198073 (防链接失效)

程序使用并发的原因有两种:

  1. 为了关注点分离(程序中不同的功能,使用不同的线程去执行),当为了分离关注点而使用多线程时,设计线程的数量的依据,不再是依赖于 CPU 中的可用内核的数量,而是依据概念上的设计(依据功能的划分);
  2. 为了提高性能, 此时线程数量可以依据CPU的逻辑核心数目,这样可以使得每个线程能在不同的CPU核心上同时并发执行;

知道何时不使用并发与知道何时使用它一样重要。

不使用并发的唯一原因就是收益(性能的增幅)比不上成本(代码开发的脑力成本、时间成本,代码维护相关的额外成本)。运行越多的线程,操作系统需要为每个线程分配独立的栈空间,需要越多的上下文切换,这会消耗很多操作系统资源,如果在线程上的任务完成得很快,那么实际执行任务的时间要比启动线程的时间小很多,所以在某些时候,增加一个额外的线程实际上会降低,而非提高应用程序的整体性能,此时收益就比不上成本。而且多线程代码如果编写不当,运行中会出现很多问题,诸如执行结果不符合预期、程序崩溃等问题。

 

产生死锁的四个必要条件(面试考点):

  1. 互斥(资源同一时刻只能被一个进程使用)
  2. 请求并保持(进程在请资源时,不释放自己已经占有的资源)
  3. 不剥夺(进程已经获得的资源,在进程使用完前,不能强制剥夺)
  4. 循环等待(进程间形成环状的资源循环等待关系)

临界区速度最快,但只能作用于同一进程下不同线程,不能作用于不同进程;临界区可确保某一代码段同一时刻只被一个线程执行;

信号量多个线程同一时刻访问共享资源,进行线程的计数,确保同时访问资源的线程数目不超过上限,当访问数超过上限后,不发出信号量;

std::unique_lock 类似于 lock_guard,只是 std::unique_lock 用法更加丰富,同时支持 std::lock_guard() 的原有功能。 使用 std::lock_guard 后不能手动 lock() 与手动 unlock(),使用 std::unique_lock 后可以手动 lock() 与手动 unlock(); std::unique_lock 的第二个参数,除了可以是 adopt_lock,还可以是 try_to_lock 与 defer_lock;

try_to_lock: 尝试去锁定,得保证锁处于 unlock 的状态,然后尝试现在能不能获得锁;尝试用 mutx 的 lock() 去锁定这个 mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里,并继续往下执行;

defer_lock: 初始化了一个没有加锁的 mutex;

std::condition_variable 是 C++ 中用于线程间同步的一个重要工具,可以让一个线程在某个条件不满足时等待,直到另一个线程通知它。

复制代码
#include <iostream>
#include <thread>
#include <condition_variable>
#include <queue>
#include <atomic>

std::mutex mtx; // 互斥量
std::condition_variable cv; // 条件变量
std::queue<int> dataQueue; // 共享数据队列
std::atomic<bool> done(false); // 指示生产者是否完成

void producer() {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产延迟
        {
            std::lock_guard<std::mutex> lock(mtx); // 锁定互斥量
            dataQueue.push(i); // 生产数据
            std::cout << "Produced: " << i << std::endl;
        }
        cv.notify_one(); // 通知一个等待的线程
    }
    done = true; // 标记生产结束
    cv.notify_all(); // 通知所有等待的线程
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx); // 锁定互斥量
        cv.wait(lock, [] { return !dataQueue.empty() || done; }); // 等待条件变量
        
        if (dataQueue.empty() && done) {
            break; // 如果没有数据且生产已完成,退出循环
        }
        
        int data = dataQueue.front(); // 访问共享资源
        dataQueue.pop();
        std::cout << "Consumed: " << data << std::endl;
    }
}

int main() {
    std::thread prod(producer); // 启动生产者线程
    std::thread cons(consumer); // 启动消费者线程

    prod.join(); // 等待生产者线程结束
    cons.join(); // 等待消费者线程结束

    return 0;
}
复制代码

 

为了减少创建与销毁线程所带来的时间消耗与资源消耗,因此采用线程池的策略:

程序启动后,预先创建一定数量的线程放入空闲队列中,这些线程都是处于阻塞状态,基本不消耗 CPU,只占用较小的内存空间。

接收到任务后,任务被挂在任务队列,线程池选择一个空闲线程来执行此任务。

任务执行完毕后,不销毁线程,线程继续保持在池中等待下一次的任务。

线程池所解决的问题:

(1) 需要频繁创建与销毁大量线程的情况下,由于线程预先就创建好了,接到任务就能马上从线程池中调用线程来处理任务,减少了创建与销毁线程带来的时间开销和CPU资源占用。

(2) 需要并发的任务很多时候,无法为每个任务指定一个线程(线程不够分),使用线程池可以将提交的任务挂在任务队列上,等到池中有空闲线程时就可以为该任务指定线程。

线程池代码示例:

示例包含三个文件,分别是 main.cpp,thread_safe_queue.hpp,join_threads.hpp

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// main.cpp<br>#include <Windows.h>
#include <functional>
#include <iostream>
#include <thread>
#include <vector>
 
#include "join_threads.hpp"
#include "thread_safe_queue.hpp"
 
class ThreadPool {
  std::atomic<bool> done;
  tp::thread_safe_queue<std::function<void()>> work_queue;
  std::vector<std::thread> threads;
  tp::join_threads joiner;
 
  void work_thread() {
    while (!done) {
      std::function<void()> task;
      if (work_queue.try_pop(task)) {
        task();
      } else {
        std::this_thread::yield();
      }
    }
  }
 
public:
  ThreadPool() : done(false), joiner(threads) {
    int max_thread_count = std::thread::hardware_concurrency();
 
    try {
      for (int i = 0; i < max_thread_count; i++) {
        threads.push_back(std::thread(&ThreadPool::work_thread, this));
      }
    } catch (...) {
      done = true;
      throw;
    }
  }
 
  ~ThreadPool() {
    while (!work_queue.empty()) {
      std::this_thread::yield();
    }
 
    done = true;
  }
 
  template <typename FunctionType>
  void Submit(FunctionType f) {
    work_queue.push(std::function<void()>(f));
  }
};
 
void Task1() {
  Sleep(3000);
  printf("Task1 finished\n");
}
 
void Task2() {
  Sleep(3000);
  printf("Task2 finished\n");
}
 
void Task3() {
  Sleep(3000);
  printf("Task3 finished\n");
}
 
int main() {
  ThreadPool pool;
  pool.Submit(Task1);
  pool.Submit(Task2);
  pool.Submit(Task3);
 
  return 0;
}

  

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// thread_safe_queue.hpp
#pragma once
#include <memory>
#include <mutex>
 
namespace tp {
template <typename T>
class thread_safe_queue {
 public:
  thread_safe_queue() : head(new node), tail(head.get()) {}
  thread_safe_queue(const thread_safe_queue& other) = delete;
  thread_safe_queue& operator=(const thread_safe_queue& other) = delete;
 
  std::shared_ptr<T> try_pop() {
    std::unique_ptr<node> old_head = try_pop_head();
    return old_head != nullptr ? old_head->data : nullptr;
  }
 
  bool try_pop(T& value) {
    std::unique_ptr<node> old_head = try_pop_head(value);
    return old_head != nullptr;
  }
 
  std::shared_ptr<T> wait_and_pop() {
    std::unique_ptr<node> const old_head = wait_pop_head();
    return old_head->data;
  }
 
  std::shared_ptr<T> wait_and_pop(T& value) {
    std::unique_ptr<node> const old_head = wait_pop_head(value);
    return old_head->data;
  }
 
  template <typename T>
  void push(T new_value) {
    std::shared_ptr<T> new_data(std::make_shared<T>(std::move(new_value)));
    std::unique_ptr<node> p(new node);
    {
      std::lock_guard<std::mutex> tail_lock(tail_mutex);
      tail->data = new_data;
      node* const new_tail = p.get();
      tail->next = std::move(p);
      tail = new_tail;
    }
    data_cond.notify_one();
  }
 
  bool empty() {
    std::lock_guard<std::mutex> head_lock(head_mutex);
    return (head.get() == get_tail());
  }
 
 private:
  struct node {
    std::shared_ptr<T> data;
    std::unique_ptr<node> next;
  };
 
  std::mutex head_mutex;
  std::unique_ptr<node> head;
  std::mutex tail_mutex;
  node* tail;
  std::condition_variable data_cond;
 
 private:
  node* get_tail() {
    std::lock_guard<std::mutex> tail_lock(tail_mutex);
    return tail;
  }
 
  std::unique_ptr<node> pop_head()  // 1
  {
    std::unique_ptr<node> old_head = std::move(head);
    head = std::move(old_head->next);
    return old_head;
  }
 
  std::unique_lock<std::mutex> wait_for_data()  // 2
  {
    std::unique_lock<std::mutex> head_lock(head_mutex);
    data_cond.wait(head_lock, [&] { return head.get() != get_tail(); });
    return std::move(head_lock);  // 3
  }
 
  std::unique_ptr<node> wait_pop_head() {
    std::unique_lock<std::mutex> head_lock(wait_for_data());  // 4
    return pop_head();
  }
 
  std::unique_ptr<node> wait_pop_head(T& value) {
    std::unique_lock<std::mutex> head_lock(wait_for_data());  // 5
    value = std::move(*head->data);
    return pop_head();
  }
 
  std::unique_ptr<node> try_pop_head() {
    std::lock_guard<std::mutex> head_lock(head_mutex);
    if (head.get() == get_tail()) {
      return nullptr;
    }
    return pop_head();
  }
 
  std::unique_ptr<node> try_pop_head(T& value) {
    std::lock_guard<std::mutex> head_lock(head_mutex);
    if (head.get() == get_tail()) {
      return nullptr;
    }
    value = std::move(*head->data);
    return pop_head();
  }
};
// namespace tp

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// join_threads.hpp
#pragma once
#include <thread>
#include <vector>
 
namespace tp {
class join_threads {
 public:
  explicit join_threads(std::vector<std::thread>& threads_)
      : threads(threads_) {}
 
  ~join_threads() {
    for (auto& thread : threads) {
      if (thread.joinable()) thread.join();
    }
  }
 
 private:
  std::vector<std::thread>& threads;
};
// namespace tp

  

未完待续.。。。

 

posted @   strive-sun  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示