目录

  • 线程
    • 怎么创建启动一个线程
      • 线程如何区分
    • 线程如何结束
    • 主线程如何处理子线程
  • 多线程编程
    • CAS原子操作
    • lock_guard和unique_lock
    • 线程通信——生产者消费者模型
    • 线程中只调用一次(补于2021.12.23)
    • 线程局部存储(补于2021.12.23)
    • 异步编程async(补于2021.12.23)
  • 参考文献

线程

线程是操作系统能够进行运算调度的最小单位。被包含在进程之中,是进程的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程可以并发执行多个线程,每个线程会执行不同的任务。对应在现实生活中,进程是组长,线程是小组成员。

怎么创建启动一个线程

语言级别,一般调用std名称空间的thread类来启动一个线程。
其对应操作系统层次的一下系统调用:

windows: createThread
linux:pthread_create

以下是thread类的一个构造函数:

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

我们可以看到,其需要一个线程函数(也可以是类对象和lambda表达式)以及这个函数所需要传入的参数
所以我们便可以这样来创建线程:

void threadHandle1(int time)
{// 让子线程睡眠time秒std::this_thread::sleep_for(std::chrono::seconds(time));cout << "hello thread1!" << endl;
}
void threadHandle2(int time)
{// 让子线程睡眠time秒std::this_thread::sleep_for(std::chrono::seconds(time));cout << "hello thread2!" << endl;
}
// 创建了一个线程对象,传入一个线程函数,新线程就开始运行了std::thread t1(threadHandle1, 2);std::thread t2(threadHandle2, 3);

这样,t1就会执行threadHandle1函数,t2执行threadHandle2函数。

线程如何区分

线程除了站在我们角度上的以名字区分,它还有一个属于自己的id!
通过std::thread::get_id()便可以获取到该成员对象线程的id。

std::cout << "t1 thread :: ID = " << t1.get_id() << std::endl;

而在线程函数中通过std::this_thread::get_id()获取线程id。

std::cout << "inside thread :: ID = " << std::this_thread::get_id() << std::endl;

线程如何结束

线程结束主要分为以下四种方式:

线程函数返回(推荐)
调用ExitThraed函数,线程自行撤销
同一进程或者另一个进程中调用TerminateThread函数
ExitProcess和TerminateProcess函数也可以用来终止线程进行

除了第一种,其他都不推荐使用,那我就不把它们写进博客了。

主线程如何处理子线程

主要用到的就是join和detach,其他的百度去吧。

t1.join();

这个方法让主函数等待子线程结束,主线程才继续往下继续运行。

t1.detach();

这个方法把子线程设置为分离线程。也就是主线程和子线程断绝父子关系了。

在一般情况下,如果主线程结束,就代表整个进程结束,如果这时有子线程还未结束就会出现运行错误。
当设置子线程为分离线程,主线程结束,子线程也自动结束。

多线程编程

总结了线程的基本知识,我们现在就来看一下多线程编程。
假设有车站的三个买票窗口来卖100张票。

int ticketCount = 100; // 车站有100张车票,由三个窗口一起卖票
int main()
{list<std::thread> tlist;for (int i = 1; i <= 3; ++i){tlist.push_back(std::thread(sellTicket, i));}for (std::thread &t : tlist){t.join();}cout << "所有窗口卖票结束!" << endl;return 0;
}

现在我们看看具体是怎样卖票的:
我们知道对车票(count)进行卖(--操作)时,会分为3步:

mov eax, count
sub eax,1
mov count,eax

这不是原子性的!
CPU可能刚执行完sub操作的时候,该线程(t1)时间片到了执行到其他线程(t2),这样其他卖票窗口拿到的count也是100,然后这个线程(t2)执行完count = 99,CPU又回去执行t1,这时你就会白给一张票。

所以就必须引入锁操作。

在多线程程序,需满足竞态条件:多线程程序执行的结果是一致的,不会随着CPU对线程不同的调用顺序,而产生不同的结果。
所以我们就需要定义一把

std::mutex mtx; // 全局的一把互斥锁

有了这把锁,我们就可以实现一个没什么大错的卖票程序了:

// 模拟卖票的线程函数  lock_guard unique_lock
void sellTicket(int index)
{while (ticketCount > 0) // ticketCount=1  锁+双重判断{// 保证所有线程都能释放锁,防止死锁问题的发生 scoped_ptrlock_guard<std::mutex> lock(mtx); if (ticketCount > 0){// 临界区代码段  =》  原子操作 =》 线程间互斥操作了 =》 mutexcout << "窗口:" << index << "卖出第:" << ticketCount << "张票!" << endl;//cout << ticketCount << endl;ticketCount--;}std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}

这里用了lock_guard<std::mutex> lock(mtx); 把锁包装成了一个类,保证能出函数一定会释放锁。

CAS原子操作

因为锁的操作是比较重,而且在临界区代码做的事情比较复杂,比较多。所以引入了CAS来保证上面的--操作原子特性。同时这也是无锁操作。
首先定义一下原子的类型:

volatile std::atomic_bool isReady = false;
volatile std::atomic_int mycount = 0;

这里的volatile保证了每次数据都是从内存拿,而不是有一定安全性风险的寄存器。

void task()
{while (!isReady){std::this_thread::yield(); // 线程出让当前的CPU时间片,等待下一次调度}for (int i = 0; i < 100; ++i){mycount++;}
}
int main()
{list<std::thread> tlist;for (int i = 0; i < 10; ++i){tlist.push_back(std::thread(task));}std::this_thread::sleep_for(std::chrono::seconds(3));isReady = true;for (std::thread &t : tlist){t.join();}cout << "mycount:" << mycount << endl;return 0;
}

这明显就比锁轻便了很多!

lock_guard和unique_lock

这两个其实可以类比智能指针来记:
lock_gurad类比于scoped_ptr,它的拷贝构造和复制构造都被删除了,不可用在函数参数传递或者返回过程中,只能用在简单的临界区代码段的互斥操作中。

lock_ guard(const lock_ guard&)=delete;
lock_ guard& operator= (const lock_ guard&)=delete;

而unique_lock可以类比于unique_ptr,它不仅可以用在简单的临界代码段的互斥操作中,还能用在函数调用过程中。
总的来说,建议使用unique_lock.

线程通信——生产者消费者模型

现在就用一个比较常用的模型来认识一下线程通信。
首先先定义一下互斥锁mtx和条件变量cv:

std::mutex mtx; // 定义互斥锁,做线程间的互斥操作
std::condition_variable cv; // 定义条件变量,做线程间的同步通信操作

然后定义出生产者和消费者的类queue:

// 生产者生产一个物品,通知消费者消费一个;消费完了,消费者再通知生产者继续生产物品
class Queue
{public:void put(int val) // 生产物品{//lock_guard<std::mutex> guard(mtx); // scoped_ptrunique_lock<std::mutex> lck(mtx); // unique_ptrwhile (!que.empty()){// que不为空,生产者应该通知消费者去消费,消费完了,再继续生产// 生产者线程进入#1等待状态,并且#2把mtx互斥锁释放掉cv.wait(lck);  // lck.lock()  lck.unlock}que.push(val);/* notify_one:通知另外的一个线程的notify_all:通知其它所有线程的通知其它所有的线程,我生产了一个物品,你们赶紧消费吧其它线程得到该通知,就会从等待状态 =》 阻塞状态 =》 获取互斥锁才能继续执行*/cv.notify_all(); cout << "生产者 生产:" << val << "号物品" << endl;}int get() // 消费物品{//lock_guard<std::mutex> guard(mtx); // scoped_ptrunique_lock<std::mutex> lck(mtx); // unique_ptrwhile (que.empty()){// 消费者线程发现que是空的,通知生产者线程先生产物品// #1 进入等待状态 # 把互斥锁mutex释放cv.wait(lck);}int val = que.front();que.pop();cv.notify_all(); // 通知其它线程我消费完了,赶紧生产吧cout << "消费者 消费:" << val << "号物品" << endl;return val;}
private:queue<int> que;
};

定义生产者和消费者的线程函数:

void producer(Queue *que) // 生产者线程
{for (int i = 1; i <= 10; ++i){que->put(i);std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}
void consumer(Queue *que) // 消费者线程
{for (int i = 1; i <= 10; ++i){que->get();std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}

创建两个线程:

int main()
{Queue que; // 两个线程共享的队列std::thread t1(producer, &que);std::thread t2(consumer, &que);t1.join();t2.join();return 0;
}

线程中只调用一次(补于2021.12.23)

程序免不了要初始化数据,上面的知识告诉我们要加锁,但是C++其实还提供了仅调用一次的功能,其需要我们先声明一个全局可见的标记作为初始化的标志。

#include <mutex>    //for once_flag
static std::once_flag flag;

然后我们调用专门的call_once函数,传递我们刚刚的标记和只调用一次的函数。这样C++就会保证即使多个函数重入call_once,也只能有一个线程会成功运行此函数。

#include <thread>
#include <iostream>
#include <mutex> //for once_flagstatic std::once_flag flag;int main()
{auto function = [](){std::call_once(flag, [](){ std::cout << "this sentence will be print once" << std::endl; });};std::thread t1(function);std::thread t2(function);t1.join();t2.join();
}

上面的代码会保证语句只会被打印一次,其最常见的用法其实就是我们构造单例模式保证对象只会被初始化一次。

[ik@localhost test]$ g++ -lpthread -o test test.cpp
[ik@localhost test]$ ./test
this sentence will be print once

线程局部存储(补于2021.12.23)

我们都知道,读写全局变量会导致数据竞争,因为共享数据,多线程操作时就会导致状态不一致。但是有的时候全局变量的作用不一定是共享数据,而是传递数据。这个时候就可以使用线程局部存储了,其关键字为thread_local

下面的代码定义了一个线程局部存储变量n,其会在t1和t2两个线程中分别创建一个变量n,也就是说两个线程的n不是一个n,这样我们对其进行相加运算不会叠加。

#include <thread>
#include <iostream>thread_local int n = 0;
int main()
{auto function = [](int input){n += input;std::cout << "thread id: " << std::this_thread::get_id() << "   "<< "n: " << n << std::endl;};std::thread t1(function, 3);std::thread t2(function, 4);t1.join();t2.join();
}
[ik@localhost test]$ ./test
thread id: 140672148616960   n: 3
thread id: 140672140224256   n: 4

异步编程async(补于2021.12.23)

大多数thread做的事情也可以用async来实现,但不会看到明显的线程。

#include <future> //for asyncstd::async(std::forward<_Fn>(__fn), std::forward<_Args>(__args)...);

异步的本质还是调用一个线程去执行我们传入的任务,不过是让底层自动管理线程。其会返回一个future变量,其是函数执行返回的结果。如果有返回值,我们就可以用成员函数get获得结果。

#include <thread>
#include <iostream>
#include <future>
#include <unistd.h> //for sleepint main()
{auto function = [](){sleep(2);return std::this_thread::get_id();};auto f1 = std::async(function);auto f2 = std::async(function);std::cout << "excute thread: " << f1.get() << std::endl;std::cout << "excute thread: " << f2.get() << std::endl;
}
[ik@localhost test]$ g++ -lpthread -o test test.cpp
[ik@localhost test]$ ./test
excute thread: 140098122565376
excute thread: 140098114172672


get只能调用一次,多次调用会触发async异常

下面还有非常重要的一点:如果不显示获取async的返回值future,它就会同步阻塞直至任务完成,于是异步就会变成同步。下面是个很好的例子:

#include <thread>
#include <iostream>
#include <future>
#include <unistd.h> //for sleepint main()
{auto function = [](int time){std::cout << "thread: " << std::this_thread::get_id() << "   sleep for " << time << "(s)" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(time));return std::this_thread::get_id();};std::async(function,2);std::cout << "this is main" << std::endl;
}

这种情况就会导致同步发生:

[ik@localhost test]$ ./test
thread: 140604791146240   sleep for 2(s)
this is main

下面是获取future的代码:

#include <thread>
#include <iostream>
#include <future>
#include <unistd.h> //for sleepint main()
{auto function = [](int time){std::cout << "thread: " << std::this_thread::get_id() << "   sleep for " << time << "(s)" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(time));return std::this_thread::get_id();};auto f1 = std::async(function,2);std::cout << "this is main" << std::endl;
}

执行结果明显就是先输出 this is main ,在输出异步执行的输出:

[ik@localhost test]$ g++ -lpthread -o test test.cpp
[ik@localhost test]$ ./test
this is main
thread: 139629252716288   sleep for 2(s)

参考文献

[1] 施磊.腾讯课堂——C++高级.图论科技,2020.7.
[2] DoubleLi.如何终止线程运行.博客园,2012.8.15.
[3] 罗剑锋.罗剑锋的C++实战笔记.极客时间

C++高级——多线程编程相关推荐

  1. java高级-多线程编程

    2019独角兽企业重金招聘Python工程师标准>>> 一.进程和线程 在java语言中最大的特点就是支持多线程的开发(也是为数不多支持多线程开发的语言),如果对多线程没有一个全面而 ...

  2. java自动化测试语言高级之多线程编程

    java自动化测试语言高级之多线程编程 Java 多线程编程 Java 给多线程编程提供了内置的支持. 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务 ...

  3. 廖雪峰Java11多线程编程-3高级concurrent包-4Concurrent集合

    Concurrent 用ReentrantLock+Condition实现Blocking Queue. Blocking Queue:当一个线程调用getTask()时,该方法内部可能让给线程进入等 ...

  4. java多线程编程—高级主题_Java day20 高级编程【第一章】Java多线程编程

    [第一章]Java多线程编程 一.进程与线程 多个时间段会有多个程序依次执行,但是同一时间点只有一个进程执行 线程是在进程基础之上划分的更小的程序单元 ,线程是在进程基础上创建并且使用的,所以线程依赖 ...

  5. .NET多线程编程入门

    在.NET多线程编程这个系列我们讲一起来探讨多线程编程的各个方面.首先我将在本篇文章的开始向大家介绍多线程的有关概念以及多线程编程的基础知识;在接下来的文章中,我将逐一讲述.NET平台上多线程编程的知 ...

  6. [PYTHON] 核心编程笔记(18.多线程编程)

    18.1 引言/动机 18.2 线程和进程 18.2.1 什么是进程(重量级进程)? 计算机程序只不过是磁盘中可执行的,二进制(或其他类型)的数据,他们只有在被读取到内存中,被操作系统调用时才开始他们 ...

  7. linux 多线程编程笔记

    一, 线程基础知识 1,线程的概念 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行 中必不可少的资源(如程序计 ...

  8. python之多线程编程(一):基本介绍

    Python提供了多个模块来支持多线程编程,包括thread,threading和Queue模块等.编写的程序可以使用thread和threading模块来创建与管理线程. thread模块提供了最基 ...

  9. 详解Java多线程编程中LockSupport类的线程阻塞用法

    转载自  详解Java多线程编程中LockSupport类的线程阻塞用法 LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了基本的线程同步原语.LockSupport实际 ...

最新文章

  1. 第六周项目一-分数类的雏形(1)
  2. python培训好学吗-人工智能“速成班”Python好学吗 小心别被忽悠了
  3. mac电脑sublime text3安装pretty json插件
  4. windows下利用IIS搭建web和ftp服务以及防火墙配置
  5. 查询SQL中某表里有多少列包含某字段
  6. 图学java基础篇之IO
  7. 第二章:图像处理基础
  8. url带多个参数_动态URL和静态URL做seo优化不必二选一
  9. 数据结构中La表的数据合并到Lb表中
  10. 业余长跑爱好者最后膝盖都怎样了?
  11. 【ASP.NET】HTTP中的 get 和 post 请求
  12. SQL对某个字段分组并加序号
  13. fighting小银考呀考不过四级
  14. document.getElementsByName , document.getElementsByTagName ,document.createElement
  15. 卡巴斯基安全部队2013 卡巴斯基正式版 送一年使用序列号
  16. 基于开源软件打造企业网络安全
  17. idea创建SpringBoot工程
  18. Android 图片毛玻璃的实现方法
  19. A-KAZE论文研读
  20. linux查找以c开头的的文件夹,文件查找命令find详解

热门文章

  1. FAT16和FAT32文件定位
  2. CC1101和CC1310的参数配置的问题整理
  3. 免费的Web3在线技术学习平台:Moonbuilders Academy入门指南
  4. [一日一教学](15)设置文件属性:attrib
  5. 电气设备常用基本文字符号
  6. Robust Initialization of Monocular Visual-Inertial Estimation on Aerial Robots
  7. 评分模板html,小程序模板-评分星星
  8. FPGA-verilog-写数字钟
  9. 学计算机pr学的好有用吗,别人用手机剪映剪辑出来的视频,比我用电脑pr剪辑的还要好?瞬间被打击了,我学习了pr还有什么优势吗?_科技数码通...
  10. 串行网络、环形网络、星型网络