对于单处理器系统,处理器在一个单元时间内只能执行一个进程,操作系统系统以极快的速度在多个进程之间进行切换,营造了一种多个进程同时运行的假象。

1. 一些基本概念:

c++中的静态库与动态库:
1. 静态库:*.lib 是指一些已经编译过的代码,在程序运行之前,静态库在编译的时候被放入到可执行文件中。

静态库在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中,对应的链接方式称为静态链接。静态库与汇编生成的目标文件(.o文件)一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的归档集合,即很多目标文件经过压缩打包后形成的一个文件。

静态库有两个重大缺点:

1)空间浪费

2)静态链接对程序的更新、部署和发布会带来很多麻烦。一旦程序中有任何模块更新,整个程序就要重新链接,发布给用户。

2. 动态库:*.dll 与静态库不同的是,动态库在程序开始执行后才开始进行链接,可以将许多程序都会用到的函数放入到动态库中。在这样就不必在每个程序中都包含这些函数了,只需在运行时链接一个动态库就可以了。

动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。

动态库特点:

1)代码共享,所有引用该动态库的可执行目标文件共享一份相同的代码与数据。

2)程序升级方便,应用程序不需要重新链接新版本的动态库来升级,理论上只要简单地将旧的目标文件覆盖掉。

3)在运行时可以动态地选择加载各种应用程序模块

进程和线程的概念:
在多任务系统中,CPU以极快的速度在不同进程之间切换,每个进程只运行几毫秒,从严格意义上来说,cpu在任何时刻只运行一个进程,只不过它的快速切换营造了并行处理的假象。每个进程都有自己的虚拟地址空间和控制线程。线程是操作系统调度器分配处理器时间的基础单元,可以将系统看作运行在准并行环境中的进程集合,在进程间快速反复的切换叫做多任务处理。通常,在一个进程的地址空间中要执行多个线程。

感觉这篇博客对于线程和进程的解释很清晰:进程和线程的主要区别(C++多线程编程实战这本书完全连这个概念都没有讲清楚):具体解释如下

区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

线程:

c++中通过std中的thread方法创建函数:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <thread>using std::cout;
using std::endl;
using std::thread;void func()
{// do something
}int main()
{thread t(func);t.join();      // join()函数会阻塞线程,直到线程执行完毕return 0;
}

关于线程的阻塞:
在某一时刻某一个线程在运行一段代码的时候,这时候另一个线程也需要运行,但是在运行过程中的那个线程执行完成之前,另一个线程是无法获取到CPU执行权的(调用sleep方法是进入到睡眠暂停状态,但是CPU执行权并没有交出去),这个时候就会造成线程阻塞。

出现线程阻塞的原因:(参考:https://blog.csdn.net/sunshine_2211468152/article/details/87299708)
1. 睡眠状态

当一个线程执行代码的时候调用了sleep方法,线程处于睡眠状态,此时有其他线程需要执行时就会造成线程阻塞,而且sleep方法被调用之后,线程不会释放锁对象,也就是说锁还在该线程手里,CPU执行权还在自己手里,等睡眠时间一过,该线程就会进入就绪状态。

2. 礼让状态:

当一个线程正在运行时,调用了yield方法之后,该线程会将执行权礼让给同等级的线程或者比它高一级的线程优先执行,此时该线程有可能只执行了一部分而此时把执行权礼让给了其他线程,这个时候也会进入阻塞状态,但是该线程会随时可能又被分配到执行权,比较讲究谦让;

3.等待状态:
当一个线程正在运行时,调用了wait方法,此时该线程需要交出CPU执行权,也就是将锁释放出去,交给另一个线程,该线程进入等待状态,但与睡眠状态不一样的是,进入等待状态的线程不需要设置睡眠时间,但是需要执行notify方法或者notifyall方法来对其唤醒,自己是不会主动醒来的,等被唤醒之后,该线程也会进入就绪状态,但是进入仅需状态的该线程手里是没有执行权的,也就是没有锁,而睡眠状态的线程一旦苏醒,进入就绪状态时是自己还拿着锁的

4. 自闭状态:

当一个线程正在运行时,调用了一个join方法,此时该线程会进入阻塞状态,另一个线程会运行,直到运行结束后,原线程才会进入就绪状态。这个比较像是”走后门“,本来该先把你的事情解决完了再解决后边的人的事情,但是这时候有走后门的人,那就会停止给你解决,而优先把走后门的人事情解决了;

------------------------------------------------------------------------------------------------------------------------

如果不希望线程被阻塞,可以调用detach方法,将线程和线程对象分离:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <thread>using std::cout;
using std::endl;
using std::thread;void func()
{// do something
}int main()
{thread t(func);t.detach();      // join()函数会阻塞线程,直到线程执行完毕// do other thing return 0;
}

调用detach方法后,线程和线程对象就分离了,让线程作为后台线程去执行,当前线程也不会阻塞,但是datech之后就无法再和线程发生联系了。例如,detach之后线程无法再进行join(),线程何时执行完我们也无法控制。

注:

std::thread出了作用域之后将会析构。这时如果线程函数没有执行完毕将会发生错误,因此应该保证线程函数的生命周期在线程变量std::thread的生命周期之内。

void func()
{// do something
}int main()
{thread t(func);return 0;
}

例如,上面的程序可能会报错,因为线程对象可能会先于线程函数结束。所以应该用join()阻塞线程或者用detach让线程在后台运行。

此外,可以将线程对象保存到一个容器中,以保证线程对象的生命周期:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <vector>using std::cout;
using std::endl;
using std::thread;
using std::vector;vector<thread> g_list;
vector<std::shared_ptr<thread>> g_list2;void func()
{// do something
}void CreateThread()
{thread t(func);g_list.push_back(std::move(t));    // 将线程对象保存到容器中g_list2.push_back(std::make_shared<thread>(func));
}int main()
{CreateThread();for (auto& thread : g_list){thread.join;}for (auto& thread : g_list2){thread->join();}return 0;
}

线程不支持复制,但是可以将线程移动:

void func()
{// do something
}int main()
{thread t(func);thread t1(std::move(t));   // 移动语义return 0;
}

线程被移动之后,线程对象t将不再代表任何线程了。

线程的基本使用方法:

1. 获取线程ID,获取CPU核数

void func()
{//do something
}int main()
{thread t(func);t.get_id();    // 获取线程IDcout << thread::hardware_concurrency() << endl;   // 获取CPU核心数return 0;
}

2,线程休眠:

void func()
{std::this_thread::sleep_for(std::chrono::seconds(3));   // 线程休眠cout << "finish sleep" << endl;
}int main()
{thread t(func);t.join();return 0;
}

多线程中的互斥量:

互斥量是一种同步语句,是一种线程同步的手段,用来保护多线程同时访问的共享数据:
C++中提供了四种互斥量:

std::mutext:   独占的互斥量,不能递归使用

std::timed_mutex:   带超时的互斥量,不能递归使用。

std::recursive_mutex:  递归互斥量,不带超时功能。

std::recursive_timed_mutex:  带超时的递归互斥量

1. std::mutext:   独占的互斥量,不能递归使用

互斥量的基本接口很相似,一般都是通过lock()方法来阻塞线程,直到获得互斥量的所有权为止,在线程获得互斥量并完成任务之后,使用unlock来解除对互斥量的占用。lock(),unlock()应成对出现。try_lock()尝试锁定互斥量,成功返回true,否则为false,所以它是非阻塞的。

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>using namespace std;mutex g_lock;   // 独占互斥量
void func()
{g_lock.lock();   // 锁cout << "Enter thread " << std::this_thread::get_id() << endl;std::this_thread::sleep_for(std::chrono::seconds(2));cout << "Leaving thread " << std::this_thread::get_id() << endl;g_lock.unlock();
}int main()
{thread t1(func);thread t2(func);thread t3(func);t1.join();t2.join();t3.join();return 0;
}

结果如下图所示:

一般推荐使用lock_guard()来替代lock/unlock,因为更加安全。lock_guard在构造时会自动锁定互斥量,在退出作用后进行析构时就会自动解锁互斥量,避免忘记unlock操作。

lock_guard用到了RAII技术:
使用局部对象管理资源的技术通常称为“资源获取就是初始化”,即Resource Acquisition Is Initialization 机制。这一机制是Bjarne Stroustrup首先提出的,要解决的是这样一个问题:

在C++中,如果在这个程序段结束时需要完成一些资源释放的工作,那么正常情况下自然是没有什么问题,但是当一个异常抛出时,释放资源的语句就不会被执行。于是Bjarne Stroustrup就想到确保 能运行资源释放代码的地方就是在这个程序段(栈帧)中放置的对象的析构函数了,因为stack winding会保证它们的析构函数都会被执行。将初始化和资源释放都放到一个包装类中的好处:

a. 保证了资源的正常释放
         b. 省去了在异常处理中冗长而重复甚至有些还不一定执行到的清理逻辑,进而确保了代码的异常安全。
         c. 简化代码体积。

所在lock_guard在类的构造函数中和会分配资源,在析构函数中释放资源,保证资源在出了作用域之后就释放,上面的例子使用lock_guard后会更加简洁:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>using namespace std;mutex g_lock;   // 独占互斥量
void func()
{std::lock_guard<std::mutex> locker(g_lock);cout << "Enter thread " << std::this_thread::get_id() << endl;std::this_thread::sleep_for(std::chrono::seconds(2));cout << "Leaving thread " << std::this_thread::get_id() << endl;
}int main()
{thread t1(func);thread t2(func);thread t3(func);t1.join();t2.join();t3.join();return 0;
}

2. std::recursive_mutex:  递归互斥量,不带超时功能。

递归锁允许同一线程多次获得该互斥锁,可以解决同一线程需要多次获取互斥量时的死锁的问题,一个线程多次获取同一互斥量时会发生死锁问题:

例如:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>using namespace std;struct Complex
{std::mutex mutex;int i;Complex() : i(2) {};  // 构造函数void mul(int x){std::lock_guard<std::mutex> lock(mutex);   // 获取互斥量i *= x;}void div(int y){std::lock_guard<std::mutex> lock(mutex);   // 获取互斥量i /= y;}void both(int x, int y){std::lock_guard<std::mutex> lock(mutex);   // 获取互斥量mul(x);div(y);}};int main()
{Complex cmp;cmp.both(21, 2);cout << cmp.i << endl;return 0;
}

在主线程中多次获取互斥量,就会发生死锁。因为互斥量已被当前线程获取,无法释放,就会导致这样的问题。

递归锁可以解决这种问题:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>using namespace std;struct Complex
{std::recursive_mutex mutex;int i;Complex() : i(2) {};  // 构造函数void mul(int x){std::lock_guard<std::recursive_mutex> lock(mutex);   // 获取互斥量i *= x;}void div(int y){std::lock_guard<std::recursive_mutex> lock(mutex);   // 获取互斥量i /= y;}void both(int x, int y){std::lock_guard<std::recursive_mutex> lock(mutex);   // 获取互斥量mul(x);div(y);}};int main()
{Complex cmp;cmp.both(21, 2);cout << cmp.i << endl;return 0;
}

但是递归锁会存在如下的缺点:
a. 用到递归锁的多线程互斥处理本身是可以简化的。递归互斥量很容易产生复杂的逻辑,会导致线程同步引起的晦涩的问题。

b. 与非递归锁相比,递归锁的效率会更低。

c. 递归锁没有说明一个线程最多可以重复获得几次互斥量,一旦超过一定的次数,再调用lock就会抛出std::system的错误。

3. std::timed_mutex:   带超时的互斥量,不能递归使用。

timed_mutex在获取锁时增加超时等待功能,因为有时候不知道获取锁需要等待多久,为了不至于一直在等待获取互斥量,可以设置一个超时时间,在超时后还可以做其他的事情。多出的两个接口为:try_lock_for, try_lock_until.

例如:

// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>using namespace std;void work()
{std::timed_mutex mutex;// 定义函数std::chrono::milliseconds timeout(100);  // 100mswhile (true){if (mutex.try_lock_for(timeout))  // 获取到互斥量{cout << "Thread " << this_thread::get_id() << " do work in mutex" << endl;std::chrono::milliseconds sleep_time(250);this_thread::sleep_for(sleep_time);mutex.unlock();    // 释放互斥量this_thread::sleep_for(sleep_time);}else   // 未获取到互斥量  处理其他事务{cout << "Thread " << this_thread::get_id() << " do work not in mutex" << endl;std::chrono::milliseconds sleep_time(100);this_thread::sleep_for(sleep_time);}}
}int main()
{thread t1(work);thread t2(work);t1.join();t2.join();return 0;
}

------------------------------------------------------------分割线------------------------------------------------------------------

C++多线程编程 (1)相关推荐

  1. C 多线程编程之在类中使用多线程(thread)的方法

    一.thread的基本用法 参见C++使用thread类多线程编程 . 二.类外使用多线程,访问类的成员 这几种方式,新建线程都是在类外,然后通过把友元函数或者成员函数作为thread参数. #inc ...

  2. Linux多线程管理: 多线程编程

    多线程编程 mind-Mapping保存有一下导图的xmind文件,可直接获取 互斥变量 互斥对象 ptrhead相关接口 条件变量 future异步访问类 async类 promise类 packa ...

  3. [转]Linux 的多线程编程的高效开发经验

    Linux 平台上的多线程程序开发相对应其他平台(比如 Windows)的多线程 API 有一些细微和隐晦的差别.不注意这些 Linux 上的一些开发陷阱,常常会导致程序问题不穷,死锁不断.本文中我们 ...

  4. Java多线程编程实战:模拟大量数据同步

    背景 最近对于 Java 多线程做了一段时间的学习,笔者一直认为,学习东西就是要应用到实际的业务需求中的.否则要么无法深入理解,要么硬生生地套用技术只是达到炫技的效果. 不过笔者仍旧认为自己对于多线程 ...

  5. 20180829-Java多线程编程

    Java 多线程编程 Java给多线程编程提供了内置的支持.一个多线程程序包含两个或多个能并发运行的部分. 程序的每一部分都称作一个线程,并且每个线程定义了一个独立的执行路径. 多线程是多任务的一种特 ...

  6. java多线程编程01---------基本概念

    一. java多线程编程基本概念--------基本概念 java多线程可以说是java基础中相对较难的部分,尤其是对于小白,次一系列文章的将会对多线程编程及其原理进行介绍,希望对正在多线程中碰壁的小 ...

  7. 如何实现C++中的多线程编程

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 本文整理自:知乎用户yikang知乎专栏 Part 1. 基本概念 ...

  8. 多线程编程有什么用途_C++ 多线程编程 (一)

    What is thread? In computer science, a thread of execution is the smallest sequence of programmed in ...

  9. 多线程编程1-NSThread

    前言 每个iOS应用程序都有个专门用来更新显示UI界面.处理用户触摸事件的主线程,因此不能将其他太耗时的操作放在主线程中执行,不然会造成主线程堵塞(出现卡机现象),带来极坏的用户体验.一般的解决方案就 ...

  10. Linux下不使用qt自带sleep,Linux下Qt多线程编程

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 作者:武汉华嵌嵌入式培训中心 技术部 以下和大家分享Linux平台下Qt两种多线程编程的方式: 1.使用Linux平台下的线程函数.以下是给出的代码片段: ...

最新文章

  1. 【控制】蚁群算法(ACO,Ant Colony Optimization)及 Matlab 实现
  2. 雷军 1994 年写的代码,你见过吗?厉害了!
  3. 人工智能 AI技术学习路线图 初阶+中阶+高阶
  4. 通过CN3口直接控制台达伺服电机A2-M(三)
  5. 计算机硬盘磁道损坏有什么症状,硬盘0磁道(0磁道)损坏的维修方法
  6. 微信公众号全局返回码
  7. FFmpeg使用滤镜链为视频插入多张图片
  8. 从事java的年龄_请教前辈们:JAVA的职业有年龄限制吗
  9. 修改mysql中自增列的起始值
  10. 立委科普:问答系统的前生今世
  11. 99物联金手指模组AFW127PI
  12. 河南联通新款创维WIFI6 gpon光猫 SK D748
  13. 计算机网络知识(看这篇就够啦)
  14. 如何自制圆环_如何自己动手制作环形变压器?
  15. 【C++之GDB调试】GDB调试从入门到精通
  16. n的阶乘c语言输出为负数,为什么 n 为20 阶乘为负数
  17. 测试员工职责--研发环节视角
  18. 蓝桥杯单片机备考必看内容,学习一周,保底省三!
  19. Oracle数据库基础(二)
  20. 将闲置的Ipad作为Windows的副屏(Twomon SE)

热门文章

  1. Sentinel初始化监控_分布式系统集群限流_线程数隔离_削峰填谷_流量控制_速率控制_服务熔断_服务降级---微服务升级_SpringCloud Alibaba工作笔记0031
  2. STM32工作笔记0068---SPI同步通信Flash读写实验
  3. springcloud工作笔记101---强大的代码自动提示工具_aiXcoder智能编程_以及开源java工具大全_提升开发效率
  4. SpringCloud工作笔记087---SpringBoot启动报错:IDEA 错误: 找不到或无法加载主类 解决方法_connected to the target VM, address:
  5. Android学习笔记---23_网络通信之网络图片查看器
  6. How can I force Python's file.write() to use the same newline format in Windows as in Linux (“\r\n”
  7. CRF++中的unigram和bigram features指定是uni/bigrams的输出标签
  8. 杭电1874畅通工程续
  9. c++ release和debug语句分离
  10. 判断一个图是否有环(有向图,无向图)