IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 如何理解 Linux 下的多线程、锁以及原子操作 -> 正文阅读

[系统运维]如何理解 Linux 下的多线程、锁以及原子操作

如何理解 Linux 下的多线程、锁以及原子操作

引言

对于一个操作系统来说,提高资源利用率和系统吞吐量是其主要目的,而提高资源利用率和系统吞吐量的主要手段,就是提高操作系统并发执行的能力,什么是并发呢,简单来说,就是操作系统在同一个时间段内执行多个任务。或许还会经常听到一个名词——并行,二者听起来相似,但是差别很大。并行是指操作系统在同一个时刻执行多个任务,而一个 CPU 在一个时刻显然只能执行一个任务,因此,只有多 CPU 的环境下才可能实现并行,所以,我们在此主要讨论并发相关的问题。那么,在实际情况中,系统如何来实现并发呢?下面我们先从理论角度出发,讨论这个问题。

进程、线程、进程同步和死锁

进程

对于一个程序来说,最有可能的一个逻辑结构,便是获取输入,处理数据,输出结果。假设我们要获取的输入在万级以上,而 CPU 速度较慢。如果没有获取到用户的输入,那么程序就没有办法处理数据,如果程序没有处理数据,那么程序就没有办法输出。那么,假如我们要执行多次这个程序,我们该如何提升它的执行速度呢?

如果对计算机组成原理熟悉的话,计算机组成原理中有一个概念便是流水,通过将一个流程分为若干个小的步骤,从而在一个时钟周期内并发执行。如同车间的流水线一样,每一道工序其实并没有必要等待前一个完成之后再开始执行,而是只要达到其可以执行的前提条件,就可以开始执行了。

不妨类比一下,程序在本质上就是一个对数据加工的过程。我们也没有必要等待输入全部完成之后再处理数据,也没有必要等待数据全部处理完之后再输出。但是如果仅仅只有一个程序的话,显然不能实现如此的效果。不过,我们也可以将程序分为若干个部分,产生的结果就是进程。

我们将输入的指令分至进程 1,处理数据的指令分至进程 2,输出的指令分至进程 3。如此,我们便可以每隔一段时间(时间片)分别调用这些进程,从宏观上来说,它们是同时执行的。每个进程只要满足可执行条件就可以执行,从而减少时间的浪费,提高程序的运行效率,提高系统的吞吐量。以下是我认为比较好的一个进程的定义:

进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个单位

线程

大家或许还听到过一个概念叫做线程,那么,线程和进程的区别是什么呢?

我们先来探讨一下进程的执行过程。任何程序都要在 CPU 中运行,但是在执行的之前,系统必须进行一些配置(寄存器中保存什么值、从哪一个指令开始等)。当一个进程执行完毕,或分配给它的执行时间(时间片)用完之后,我们需要执行下一个进程,此时就需要进行进程调度,把之前进程(只是时间片用完,但并没有执行完毕)的运行配置保存下来,把下一个进程的运行配置加载至 CPU,开始执行。显然此过程也是时间的消耗,那么我们能否避免,或者减少这样时间的消耗呢?

我们的线程就应允而生了。线程,本质上,也是对程序的一次分解,只不过分解的方式有所不同。在将程序分解为进程时,我们就相当于把程序完全切开了,彼此除了逻辑上之外,基本互不相关,每个进程都有它自身的程序和数据等相关信息,也正因如此,我们在调度进程时,需要耗费大量的时间。如果将程序分解为线程,其实并没有对程序 “动手”,而是从逻辑上把它分为了若干个部分,每个线程只是保存它要执行的那部分指令在程序中从哪里开始,到哪里结束等信息。

综上所述,线程相较于进程,显然是非常轻的(因为进程还保存了它的程序等资源,而线程并没有)。因此当调度线程时,并不需要大刀阔斧,只需快速的将信息保存至 CPU 即可,从而加快了CPU调度时的时间。但线程也存在一些问题,那就是被分割程序的资源是所有线程共享的,如果使用不当,那就可能会出现错误。

关于进程和线程,我们可以使用下图来简单理解:

在这里插入图片描述

进程同步

以上我们讨论的都是理想的情况,但是在大多数情况下,都会出一些问题。

  1. 例如,我们将程序的处理(计算)部分,分解为若干个进程或线程,那么当我们的程序在调度时,它们的顺序是并不确定的。大家知道,程序的三大执行结构:顺序、循环以及分支,其中顺序结构显然是最常见的,我们的程序,理论上应该是从上到下依次执行的。那么如果调度顺序不确定,那么显然就破坏了程序的执行结构,那么最直观的就是会导致结果的不正确。

  2. 另外一个方面是,每个进程可能都会访问同一个变量,当进程在离开CPU之前(并没有执行完成),这个变量运行良好,但当这一进程再次运行,访问这个变量时,却发现早已 “物是人非” 了。因为,在它离开的这段日子里,此变量被其它的进程 “糟蹋了一番”,这种情形即使是人类,也会迷茫吧。如果就在这样的情况继续工作,那么只会是一错再错,这显然不是我们想要的,这里带大家简单体验一下:

我们使用 10 个线程来计数,每个线程从 1 到 100000,因此最终结果应当显示为 1000000,但结果并不如愿,由于大家都访问同一个变量 count,导致每一个线程在执行时,count 与原来的内容均不一致。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define THREAD_COUNT    10		// 线程数量

void* thread_callback(void *arg) {		// 回调函数
    int *pcount = (int *)arg;			// 强制类型转换

    int i = 0;
    while (i++ < 100000) {

        ++(*pcount);					// 计数器加一

        usleep(1);						// 休眠

    }

}

int main() {

    pthread_t threadid[THREAD_COUNT] = {0};		// 声明 10 个线程ID(初始化)

    int i = 0, count = 0;

    for (i = 0; i < THREAD_COUNT; ++i) {
        pthread_create(&threadid[i], NULL, thread_callback, &count);		// 创建线程
    }

    for (i = 0; i < 100; ++i) {

        printf("count: %d\n", count);		// 每个一段时间打印计数器
        sleep(1);

    }

    return 0;

}

执行结果如下:

如何理解 Linux 下的多线程、锁以及原子操作

那么我们如何解决这个问题呢?

进程同步的主要目的,其实就是通过一定的手段来控制各个进程的执行顺序,确保大家访问的资源(临界资源)在进程离开期间不要被其它进程“糟蹋”,如果使用,也要等之前访问这个资源的进程使用完毕之后再使用,如此才能保证程序的正确执行。

那么我们如何实现进程同步呢?

  • 硬件
    • 关中断
    • Test-and-Set 指令
    • Swap 指令
  • 信号量
    • 整型信号量
    • 记录型信号量
    • AND 型信号量
    • 信号量集
    • 管程机制

由于进程同步的方法较多,且比较不好理解,因此不打算在这里详细讲解,如果有需要,之后将会再写一篇博客,来说明这些内容

死锁

如果进程同步使用不当的话,那将产生严重的问题——死锁。

死锁,其实本质上与锁并没有关系,其含义是程序没有办法自动解开死循环。何处来的死循环呢?其实就是每个进程所需要的资源被其它资源占用,从而导致每个进程都没有办法执行,从而使系统的吞吐量直线下降。那么我们如何解决死锁呢?

简单来说,我们可以将它扼杀在摇篮之中,也可以对死锁进行预防。既然程序没有办法自动解开死循环,那么,我们可以实时检查是否有死锁的出现,然后终止某些进程,释放资源,让其它进程可以运行,从而解除死锁。由于与死锁相关的内容也很多,因此也不在此详细说明。

Linux 下的进程与线程

以上均属于理论部分,下面我们来看一看 Linux 环境下的进程与线程的内容。在 Linux 下,一个程序的运行就是一个进程的运行,我们在编写 C 语言程序时,系统提供了两个函数来创建进程与线程,分别是 fork()pthread_create()

  • fork 这个单词的意思是克隆,也就是将当前的程序再克隆一份出来,从而实现进程的创建,当执行完 fork 之后,系统中会有两个完全一样的进程在运行,调用 fork 函数的为父进程,fork 产生出来的是子进程,它们具有相同的运行代码,那么如何将他们分开呢?根据 fork 的返回值,对于子进程,fork 函数会返回 0,对于父进程,fork 函数会返回子进程的进程 id。所以 fork 函数是调用一次(父进程),返回两次(父进程和子进程)。那么程序就可以通过返回值来判断,如果是子进程就执行某一代码块,如果是父进程则执行另一个代码块。

  • pthread_create 显然就是线程创建的意思,调用它,我们需要指定回调函数,也就是线程开始执行的内容,调用后,会返回一个线程id,它是我们控制线程的主要手段,上述实例中我们使用的就是线程。

不论是 fork 还是 pthread,它们都有可能会产生死锁问题,在 fork 中还有可重入与不可重入的概念,但在此只是简要介绍一下,下面我们主要讨论 Linux 下的锁以及原子操作的内容,均与 pthread 相关。

Linux 下的锁、原子操作

Linux 下调用线程是,正常的情况是,每一个线程执行完操作之后,再让下一个线程执行相应的操作。而不正常的情况是,在一个线程的所有操作还没有执行完成时,就切换到下一个线程执行相应的操作,完成之后再执行上一个线程还没有执行完的操作,从而在结果上产生了错乱。对此,我们可以采用为临界资源加锁与原子操作的方式来解决。

在 Linux 中,锁主要有两类,分别为互斥锁和自旋锁:

  • 互斥锁:mutex,在执行操作之前加锁,执行完成操作之后解锁,在上锁期间,其它的线程是不能访问临界资源的,此时,试图访问临界资源的线程会引起线程切换,适用于锁的内容较多的情况
  • 自旋锁:spinlock,在使用上类似于 mutex ,差别是,未能成功访问临界资源的线程会不停的尝试去访问该临界资源,直到获取到该临界资源为止,适用于锁的内容较少的情况(减少线程切换的代价)

原子操作,本质上,是使用一条 cpu 指令,实现若干条 cpu 指令,从而使若干操作不可能被分割,只能同时执行

下面我们来看一下如何使用互斥锁来解决上述程序的问题:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define THREAD_COUNT    10		// 线程数量

pthread_mutex_t mutex;		// 互斥锁(全局)

void* thread_callback(void *arg) {		// 回调函数
    int *pcount = (int *)arg;			// 强制类型转换

    int i = 0;
    while (i++ < 100000) {

        pthread_mutex_lock(&mutex);		// 上锁
        ++(*pcount);					// 计数器加一
        pthread_mutex_unlock(&mutex);	// 解锁

        usleep(1);						// 休眠

    }

}

int main() {

    pthread_t threadid[THREAD_COUNT] = {0};		// 声明 10 个线程 ID(初始化)

    pthread_mutex_init(&mutex, NULL);			// 初始化互斥锁

    int i = 0, count = 0;

    for (i = 0; i < THREAD_COUNT; ++i) {
        pthread_create(&threadid[i], NULL, thread_callback, &count);		// 创建线程
    }

    for (i = 0; i < 100; ++i) {

        printf("count: %d\n", count);		// 每个一段时间打印一次计数器
        sleep(1);

    }

    return 0;

}

编译指令如下:

$ gcc -o lock lock.c -lpthread # 文件名为 lock.c

执行结果如下:

如何理解 Linux 下的多线程、锁以及原子操作

总结

本篇文章主要讨论了操作系统中进程与线程的相关概念,也讨论了在 Linux 系统中的进程与线程,演示了如何在程序中创建一个线程,并使用锁来解决线程执行中出现的问题。上述描述并不一定正确,主要目的是帮助大家更好的理解操作系统中进程与线程的概念。

下面是一些有关 Linux 下线程相关函数的总结:

函数功能
pthread_t线程 ID
pthread_create创建线程
pthread_mutex_t互斥锁类型
pthread_mutex_init初始化互斥锁
pthread_spinlock_t自旋锁类型
pthread_spin_init初始化自旋锁

后记

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习:

此文章主要用来作为课程学习过程中的总结,不仅有具体的实战代码,还会联系大学中的计算机知识体系,大家如果有什么问题也可以在评论区留言,我会尽力帮大家解决问题,如果觉得对大家有帮助的话,希望多多点赞、转发,谢谢!

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-07-04 23:22:23  更:2022-07-04 23:24:39 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/29 8:56:25-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计