C++多线程编程(上)

简介: C++多线程编程

多线程学习笔记

前言:这周学习学习了多线程并发的相关知识,写一个读书笔记以作记录。学习的教程是网易云课堂的:https://study.163.com/course/courseMain.htm?courseId=1006067356&trace_c_p_k2=217aa888da5741698cfb97e1e70009cd


更新:在最近的项目中使用到了多线程技术,发现有些知识遗漏或是不准确,已对下文进行更正和重新排版。2020/11/3


一、进程、线程、并发

  • 进程:简而言之就是一个运行的程序,如点开一个exe文件就打开了一个进程;

线程:进程中的不同执行路径,即在一个进程中运行了多个功能;(每一个进程都至少包含有一个线程,即主线程,主线程与进程的关系是相互依存)


并发:分为多线程和多进程,顾名思义就是多进程同时运行和多线程同时运行,如同时打开两个QQ客户端就是多进程,VStudio中多个窗口线程就是多线程并发;

二、thread库

以前由于系统环境的不同,如windows、linux,就需要选择不同的线程库进行代码编写,可移植性不高。

C++11之后有了标准的线程库:std::thread

除此之外,C++还有另外三个库支持多线程编程,分别是**,,和**,之后我会对其一一介绍,现在先来看看Thread库。

看看thread类,它是thread库多线程实现的基础。

构造函数

从构造函数可以窥见“多线程”的一些实现思想。

thread()默认构造函数,创建一个空的 std::thread 执行对象(在线程池的实现中就需要提前创建一定数量的线程对象);

thread(Fn&& fn, Args&&…)初始化构造函数,创建一个 std::thread 对象,该 std::thread 对象可被 joinable,新产生的线程会调用 fn 函数(即可调用对象),该函数的参数由 args 给出。


thread(const thread&) = delete拷贝构造函数(被禁用),意味着 std::thread 对象不可拷贝构造,这也可以理解,“线程”这个概念在同一时刻仅能由一个线程对象运行,所以不存在拷贝赋值之类的;

**thread(thread&& x)**转移/移动构造函数,,调用成功之后 x 不代表任何 std::thread 执行对象,相当于将“线程”的所有权转给了另外的线程对象。

下面这个例子就展示了如何创建线程:

//参考并稍加修改自博客:  https://blog.csdn.net/coolwriter/article/details/79883253
//example 1_1
#include <iostream>       // std::cout  
#include <thread>         // std::thread  
void func1()  
{  
    for (int i = 0; i != 10; ++i)  
    {  
        std::cout << "thread 1 print " << i << std::endl;  
    }  
}  

void func2(int n)  
{  
    std::cout << "thread 2 print " << n << std::endl;  
}  

int main()  
{  
    std::thread t1(func1);     
    std::thread t2(func2, 123);   
    std::cout << "main, foo and bar now execute concurrently...\n";  
    // synchronize threads:  
    t1.join();                // pauses until first finishes  
    t2.join();               // pauses until second finishes  
    std::cout << "thread 1 and htread 2 completed.\n";  
    system("pause");
    return 0;  
}  

结果:

861693d5a5b3c9e88e3435cd72456759.png

上述代码中,使用两个重要成员函数。

重要函数

join,它可以阻塞主线程直到子线程执行完毕,即必须等待A.join()执行完毕,才会接着执行之后的代码;

detach,表示该线程和主线程分离,该线程被运行时库给接管,若是在linux环境下运行,会发现即便Ctrl+C退出了主线程,但是子线程依旧还在运行。

一旦线程执行完毕,它所分配的资源将会被释放。

另外,调用 detach 函数之后this不再代表任何的线程执行实例。

PS:值得一提的是,多线程的调度机制可能会造成先创建的线程还未执行,而后面的线程就已经开始执行了,比如上述例子在代码中的顺序应该是thread1 thread2 而后打印出main,但实际效果却并非如此,因此在多线程程序的编写要注意变量的互斥性,以及代码的鲁棒性。

至于其他的成员函数:

get_id:获取线程 ID,返回一个类型为 std::thread::id 的对象。


0f54de0ec5ef6159cf8fe763714f6aaa.png

joinable():检查线程是否可被 join。检查当前的线程对象是否表示了一个活动的执行线程由默认构造函数创建的线程是不能被 join 的

swap():Swap 线程,交换两个线程对象所代表的底层句柄即ID等资源,见下:

#include <iostream>
#include <thread>
#include <chrono>

void foo()
{
  std::this_thread::sleep_for(std::chrono::seconds(1));
}

void bar()
{
  std::this_thread::sleep_for(std::chrono::seconds(1));
}

int main()
{
  std::thread t1(foo);
  std::thread t2(bar);

  std::cout << "thread 1 id: " << t1.get_id() << std::endl;
  std::cout << "thread 2 id: " << t2.get_id() << std::endl;

  std::swap(t1, t2);

  std::cout << "after std::swap(t1, t2):" << std::endl;
  std::cout << "thread 1 id: " << t1.get_id() << std::endl;
  std::cout << "thread 2 id: " << t2.get_id() << std::endl;

  t1.swap(t2);

  std::cout << "after t1.swap(t2):" << std::endl;
  std::cout << "thread 1 id: " << t1.get_id() << std::endl;
  std::cout << "thread 2 id: " << t2.get_id() << std::endl;

  t1.join();
  t2.join();
}

结果如下:


0c2acc2d1c1d98a916518a1efdd813d0.png

yield: 当前线程放弃执行,操作系统调度另一线程继续执行。

sleep_until: 线程休眠至某个指定的时刻(time point),该线程才被重新唤醒,详见连接

sleep_for: 线程休眠某个指定的时间片(time span),该线程才被重新唤醒,不过由于线程调度等原因,实际休眠时间可能比 sleep_duration 所表示的时间片更长。

#include <iostream>
#include <chrono>
#include <thread>

int main()
{
  std::cout << "Hello waiter" << std::endl;
  std::chrono::milliseconds dura( 2000 );
  std::this_thread::sleep_for( dura );
  std::cout << "Waited 2000 ms\n";
}

三、mutex库

互斥

在介绍mutex之间,先简要介绍一下何为互斥。

互斥是指散布在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。

最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

mutex主要是对临界区进行互斥操作,防止多线程同时访问该资源,造成系统错误。

常用函数

该库中所要使用的类及函数有以下这些:

mutex类4种
     std::mutex,最基本的 Mutex 类。 
     std::recursive_mutex,支持递归的 Mutex类。 
     std::time_mutex,定时 Mutex 类。 
     std::recursive_timed_mutex,定时递归Mutex 类。 

Lock 类(两种) 
     std::lock_guard,方便线程对互斥量上锁;
       std::unique_lock,比unique更灵活;
       std::try_to_lock_t ,尝试同时对多个互斥量上锁。
       std::lock,可以同时对多个互斥量上锁。 
       std::call_once,如果多个线程需要同时调用某个函数,可以保证多个线程对该函数只调用一次。

在介绍这些函数之前,先提一个概念——“锁”。

什么是锁?

——简单的理解就是,谁拿到了锁,谁就资源去拿屋子的东西!

体现在多线程之中则是:只有某个线程拿到锁时,它才能访问和修改锁对应的资源,其他的任何线程都只有等待!

mutex类

std::mutex 是最基本的互斥量,不支持递归地对 std::mutex 对象上锁,而 std::recursive_lock 则可以。

mutxe的成员函数:

1、构造函数,std::mutex不允许拷贝构造,不允许 move 拷贝,开始产生的 mutex 对象是处于 unlocked 状态的

2、lock(),该线程将锁住互斥量。会发生下面 3 种情况:


  • 如果该互斥量不被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁。

PS:什么是死锁?

多个线程各占据了一部分资源的锁,但是需要所有资源才可以继续运行,但是大家都不放所,如此相持的局面。——就好比江湖人士各有一片神功秘籍残片,但是人人都不愿意交给其他人,所有人都死盯着对方放手。

3、unlock(), 解锁,释放对互斥量的所有权。

4、try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,**则当前线程也不会被阻塞。**线程调用该函数也会出现下面 3 种情况:

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁。

std::recursive_mutex 与 std::mutex 一样,也是一种可以被上锁的对象,但是和 std::mutex 不同的是,recursive_mutex 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的**多层所有权,**std::recursive_mutex 释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。


std::time_mutex 比 std::mutex 多了两个成员函数, try_lock_for(),try_lock_until()。


try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

C++多线程编程(下):https://developer.aliyun.com/article/1508312

相关文章
|
11天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
8天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
11天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
18天前
|
安全 程序员 API
|
11天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
40 1
|
15天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
16天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
42 4
|
18天前
|
缓存 安全 C++
C++无锁队列:解锁多线程编程新境界
【10月更文挑战第27天】
33 7
|
16天前
|
消息中间件 供应链 Java
掌握Java多线程编程的艺术
【10月更文挑战第29天】 在当今软件开发领域,多线程编程已成为提升应用性能和响应速度的关键手段之一。本文旨在深入探讨Java多线程编程的核心技术、常见问题以及最佳实践,通过实际案例分析,帮助读者理解并掌握如何在Java应用中高效地使用多线程。不同于常规的技术总结,本文将结合作者多年的实践经验,以故事化的方式讲述多线程编程的魅力与挑战,旨在为读者提供一种全新的学习视角。
44 3
|
18天前
|
消息中间件 存储 安全