Linux系统编程:线程同步及生产与消费者模型

news2025/1/21 5:00:24

目录

一. 线程同步的概念及功能

二. 线程同步的实现方法

2.1 条件变量相关函数

2.2 线程同步demo代码

三. 生成与消费者模型

3.1 生产与消费者模型的概念

3.2 生产与消费者模型实现代码

四. 总结


一. 线程同步的概念及功能

为了了解线程同步的概念及实现的功能,要先明确线程互斥的缺点。

如伪代码1.1所示的情况,在加锁和解锁之中,需要对临界资源是否满足条件进行判断,如果临界资源条件满足,才会执行有效的操作,临界资源条件长时间无法得到满足,那么就会频繁执行 加锁 -> 检测 -> 解锁的操作,在不断加锁、检测、解锁的过程中,消耗的大量的计算机资源,但是并没有做实际的工作,造成了严重的资源浪费。并且,在线程互斥的条件下,如不加以控制,很可能会存在一个线程频繁申请到锁访问临界资源的情况,这样就造成了其它线程的饥饿问题。

代码1.1:线程互斥不断加锁解锁浪费线程资源问题 

void *ThreadRoutine(void *args)
{
    while(true)
    {
        // 只有在检测到临界资源count > 0时才进行有效工作
        // 如果临界资源不满足条件,那么就不断重复 上锁 -> 检测临界资源 -> 解锁 的操作
        pthread_mutex_lock(&mtx);   // 上锁
        if(临界资源条件满足)
        {
            // 。。。
            // 这里为有效代码
        }
        pthread_mutex_unlock(&mtx);
    }
}

总结,单纯的线程互斥存在的缺陷有:

  • 频繁在加锁解锁的中间,检测临界资源是否就绪,对于线程资源是一种浪费。
  • 某一特定的线程频繁申请到锁访问临界资源,造成了其它线程的饥饿问题。

为了解决线程互斥的上述缺陷,线程同步被引入了进来。线程同步的功能,就是为了解决多线程在互斥的条件下,访问临界资源的合理性问题。

线程同步的概念:让多线程按照一定的顺序,访问临界资源

二. 线程同步的实现方法

通过设置条件变量的方法,可以实现线程的同步。

2.1 条件变量相关函数

创建条件变量pthread_cond_t:

  • 在全局或局部,定义pthread_cond_t类型的对象,就完成了条件变量的创建。
  • 创建条件变量的语法为:pthread_cond_t [条件变量名]

条件变量的初始化:

  • 对于定义在全局的条件变量,可以直接使用 PTHREAD_COND_INITIALIZER 来进行初始化,具体的语法为:pthread_cond_t [条件变量名] = PTHREAD_COND_INITIALIZER。
  • 对于定义在局部的条件变量,使用函数 pthread_cond_init 来进行初始化。

pthread_cond_init 函数 -- 初始化条件变量

函数原型:int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr)

函数参数:

  • cond -- 指向被初始化的条件变量的指针 (被初始化条件变量的地址) 。
  • attr -- 初始化属性,一般采用默认属性,在使用的时候传nullptr。

返回值:函数调用成功返回0,失败返回非0错误码。

条件变量的销毁:

  • 一般来说,只有定义在局部的条件变量才需要人工去销毁,定义在全局的条件变量,在程序运行结束的时候,会自动被销毁。
  • 对于局部变量的销毁,可使用 pthread_cond_destroy。

pthread_cond_destroy 函数 -- 局部变量销毁

函数原型:int pthread_cond_destroy(pthread_cond_t *cond);

函数参数:cond -- 被销毁的条件变量的地址。

返回值:成功返回0,失败返回返回非0错误码。

条件变量的等待:

  • 通过函数pthread_cond_wait,可以实现对某种条件变量的等待。
  • 等待条件变量的代码,一定要在加锁和解锁之间,且pthread_cond_wait一定要传当前线程持有的互斥锁的地址。
  • 调用pthread_cond_wait后,线程会在当前位置为阻塞,直到其它线程将其唤醒,才可以从之前被阻塞的位置开始继续执行。

pthread_cond_wait 函数 -- 等待条件变量

函数原型:int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

函数参数:

  • cond -- 设置等待的条件变量的地址。
  • mutex -- 等到条件变量的线程当前持有的互斥锁的地址。

返回值:成功返回0,失败返回非0错误码。

演示代码2.1为 pthread_cond_wait 如何使用的规范版代码。我们希望,如果临界资源条件不满足,当前线程就要被阻塞,直到临界资源条件满足后再由其它线程唤醒。pthread_cond_wait要在加锁和解锁之间执行,且判断临界资源条件是否满足应当通过while循环来判断而不是if条件判断

代码2.1:pthread_cond_wait 的规范使用方法 

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  // 全局互斥锁
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;     // 全局条件变量

void *ThreadRoutine(void *args)
{
    while(true)
    {
        pthread_mutex_lock(&mtx);

        // 这里一定要用while轮巡检测,而不可以用if
        while(临界资源条件不满足)
        {
            // 设置条件变量阻塞等待
            // 直到临界资源就绪被唤醒
            pthread_cond_wait(&cond, &mutex);
        }

        // 对临界资源进行操作的有效代码位于此处
        // ... ...

        pthread_mutex_unlock(&mtx);
    }
}

使用while轮询检测判断临界资源条件是否满足而不采用if判断的原因如下:

  • pthread_cond_wait 函数可能执行失败。
  • 在多线程条件下被伪唤醒,即:虽然pthread_cond_wait被唤醒,但因为各种原因,造成代码执行流从上次中断位置开始执行的时候,其实条件并不满足。
  • while再进行一次判断,相当于二次确认临界资源条件满足要求,就避免了pthread_cond_wait函数执行失败和伪唤醒造成错误。

线程执行流因pthread_cond_wait被阻塞的时候,是拿锁阻塞的,那么,其他线程又为什么可以访问临界资源了呢?这里就要涉及到pthread_cond_wait的第二个参数了,第二个参数传的是互斥锁的地址。

当线程在pthread_cond_wait调用的位置被阻塞时,它会释放它所持有的第二个参数指向的互斥锁。同理,当该线程再次被唤醒时,又会去竞争互斥锁,以保证其获得访问临界资源的权限。如果线程被唤醒,但是互斥锁被其它线程占有,这种情况有可能存在,但不会出现问题,因为此时该线程会继续阻塞等待锁,拿到锁之后才会继续执行。

唤醒条件变量的等待:  

  • 通过函数pthread_cond_signal或pthread_cond_broadcast函数,可以唤醒指定的条件变量。
  • pthread_cond_signal的功能是唤醒一个等待指定条件变量的线程。
  • pthread_cond_broadcast的功能是唤醒全部等待指定条件变量的线程。

pthread_cond_signal -- 唤醒一个等待指定条件变量的线程

函数原型:int pthread_cond_signal(pthread_cond_t *cond)

函数参数:cond -- 被唤醒的条件变量的地址。

返回值:成功返回0,失败返回非0错误码。

pthread_cond_broadcast函数 -- 唤醒全部等待某个条件变量的线程

函数原型:int pthread_cond_broadcast(pthread_cond_t *cond

函数参数:cond -- 被唤醒的条件变量的地址。

返回值:成功返回0,失败返回非0错误码。

2.2 线程同步demo代码

代码2.2创建了4个子线程,通过条件变量,设置阻塞等待,让这四个线程按照先后顺序依次运行。具体的实现方法为:

  • 创建四个子线程,在线程函数中进行加锁和解锁操作,在加锁和解锁之间设置等待条件变量,以此来阻塞线程执行流执行。
  • 在主线程中,每隔1s调用一次pthread_cond_signal函数,唤醒一个子线程。

编译程序,运行代码,观察图2.1所示的运行结果,我们发现,4个线程按照先后顺序被调用了。

代码2.2:通过条件变量让多个线程依次执行

#include <iostream>
#include <string>
#include <cassert>
#include <unistd.h>
#include <pthread.h>

#define g_PTHREAD_NUM 4

struct ThreadData
{
public:
    // 构造函数
    ThreadData(const std::string& name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
        : _name(name), _pmtx(pmtx), _pcond(pcond)
    { }

    std::string _name;      // 线程名
    pthread_mutex_t *_pmtx;  // 互斥锁地址
    pthread_cond_t *_pcond;
};

void *ThreadRoutine(void *args)
{
    ThreadData* pth = (ThreadData*)args;
    while(true)
    {
        // 加锁
        pthread_mutex_lock(pth->_pmtx);

        // 设置等待条件变量
        pthread_cond_wait(pth->_pcond, pth->_pmtx);

        // 执行线程核心代码(输出线程名称)
        std::cout << "Thread Running ..., " << pth->_name << std::endl;

        // 解锁
        pthread_mutex_unlock(pth->_pmtx);
    }

    delete pth;
    return nullptr;
}

int main()
{
    // 创建并初始化互斥锁
    pthread_mutex_t mtx;
    pthread_mutex_init(&mtx, nullptr);

    // 创建并初始化条件变量
    pthread_cond_t cond;
    pthread_cond_init(&cond, nullptr);

    pthread_t tid[g_PTHREAD_NUM];

    // 创建子线程
    for(int i = 0; i < g_PTHREAD_NUM; ++i)
    {
        std::string name = "Thread ";
        name += std::to_string(i + 1);
        ThreadData *pth = new ThreadData(name, &mtx, &cond);

        int n = pthread_create(tid + i, nullptr, ThreadRoutine, (void*)pth);
        assert(n == 0);
    }

    // 主线程轮询唤醒子线程
    while(true)
    {
        pthread_cond_signal(&cond);
        sleep(1);
    }

    // 主线程等待子线程退出
    for(int i = 0; i < g_PTHREAD_NUM; ++i)
    {
        int n = pthread_join(tid[i], nullptr);
        assert(n == 0);
    }

    return 0;
}

三. 生成与消费者模型

3.1 生产与消费者模型的概念

如图3.1为生产与消费者模型的示意图,在该模型中,有如下的要素:

  • 两个角色:生产者和消费者,生产者负责生产数据,消费者读取数据。
  • 备一个交易场所:超市 -- 负责临时存储数据,类似于缓冲区。
  • 三种关系:消费者和消费者之间的竞争关系(类似于生活中买家竞争有限的货源)、生产者与生产者的竞争关系(现实中生成同一物品的不同厂商都希望自己占用更多的市场份额)、消费者与生产者之间的同步关系(生产者生产了产品消费者才能去消费、消费者消耗掉超市中一定量的产品后,生产者才会继续供应产品)。

总结:生产者消费者模型具备 3种关系、2个角色、1个交易场所。

图3.1 生产与消费者模型

用软件工程师的思维来理解生产与消费者模型:

  • 对于生产者和消费者之间同步关系的理解:将生产者与消费者全部视为线程,当缓冲区中(超市)的数据被生产者写满后,生产者写数据的操作就应当被暂时阻塞,类似于线程同步中的等待信号,同时,生产者生产数据之后,应当唤醒消费者线程,通知消费者线程读取数据。同理,当缓冲区中没有数据时,消费者线程读取数据的操作应当被阻塞,直到生产者向缓冲区中写入了数据唤醒消费者线程。
  • 生产者生产数据消费者才能消费,消费者将仓库库存进行一定清理后生产中才能继续生产数据,生产者和消费者所对应的线程访问缓冲区资源的顺序就有了一定的约束,因此生产者和消费者之间的关系为同步。
  • 消费者与消费者之间的互斥关系的理解:缓冲区(超市)为临界资源,为了保证线程安全,某一时刻只能有一个消费者线程获取临界资源。
  • 生产者与生产者之间的互斥关系的理解:类似于超市货架各个供应商不能无需摆放商品,即:某一时刻只能有一个生产者向临界资源中写数据,某一时刻只能有一个生产者线程访问临界资源。

实现生产者与消费者之间的同步关系,就需要依靠条件变量来实现线程同步,实现消费者与消费者、生产者与生产者之间的互斥关系,就需要依靠互斥锁来实现线程互斥。

生产者与消费者模型,具有以下的优势:

  • 实现了数据写入和数据读取的解耦,更符合软件工程高内聚低耦合的设计思想。
  • 利用线程之间的并发特性,提高了读写效率。如果仅单纯的从读写操作来看,好像并不会提高效率,但是,生产者向缓冲区写数据的数据源,可能是从网络中来的,并且消费者在获取数据后也可能对数据进行特定处理工作,生产者获取数据源、消费者处理数据都需要消耗时间,多线程的生产消费者模型,在某些线程执行数据读写的前后操作时,可以并发的进行数据读写工作,这样就实现了效率的提高。

3.2 生产与消费者模型实现代码

如图3.2所示,采用阻塞队列的方式来实现生产者与消费者之间的线程同步,假设阻塞队列最多容纳5个数据,如果阻塞队列中数据已满,那么生产者就应当等待消费者读取数据,而如果阻塞队列中数据为空,那么消费者就应当等待生产者向阻塞队列中写数据。并且,同一时刻只允许有一个生产者向阻塞队列中写数据、一个消费者从阻塞队列中读数据,这样就模拟出来了生产者与生产者、消费者与消费者之间的互斥关系。

图3.2 阻塞队列实现生成与消费者模型

代码3.1和3.2一同构成了生产者与消费者的代码,在阻塞队列class type BlockQueue中,给出了写数据函数push、删除数据函数pop、判断阻塞队列是否已满函数IsFull以及判空函数IsEmpty,在main函数中为生产者和消费者个创建两个线程,定义线程函数为consume何product,实现同步式的读取数据和写入数据。

代码3.1:阻塞队列实现的头文件BlockQueue.hpp

#include <iostream>
#include <queue>
#include <pthread.h>

// 使用全局变量定义阻塞队列的默认容量
const int g_DEF_SIZE = 5;

template<class T>
class BlockQueue
{
public:
    // 构造函数
    BlockQueue(int capacity = g_DEF_SIZE)
        : _capacity(capacity)
    {
        // 对互斥锁和条件变量初始化
        pthread_mutex_init(&_mtx, nullptr);
        pthread_cond_init(&_full, nullptr);
        pthread_cond_init(&_empty, nullptr);
    }  

    // 判断阻塞队列是否已满的函数
    bool IsFull()
    {
        return _capacity == _bq.size();
    }  

    // 判断阻塞队列是否为空的函数
    bool IsEmpty()
    {
        return _bq.empty();
    }

    // 写数据函数(由生产者调用)
    void push(const T& val)
    {
        pthread_mutex_lock(&_mtx);   // 加锁

        // 判断阻塞队列是否已满,满了就设置条件变量进行等待
        // while是为了避免函数未成功执行以及伪唤醒问题
        while(IsFull())
        {
            pthread_cond_wait(&_full, &_mtx);
        }

        // 向阻塞队列中写数据
        _bq.push(val);

        pthread_cond_signal(&_empty);   // 唤醒消费者线程读取数据
        pthread_mutex_unlock(&_mtx);   // 解锁
    }

    // 读数据函数(由消费者调用)
    // 将数据读到pval所指向的地址中去
    void pop(T *pval)
    {
        pthread_mutex_lock(&_mtx);  // 加锁

        // 判断阻塞队列是否为空,如为空,设置对_empty条件变量的阻塞等待
        while(IsEmpty())
        {
            pthread_cond_wait(&_empty, &_mtx);
        }

        // 读取并删除队头数据
        *pval = _bq.front();
        _bq.pop();

        pthread_cond_signal(&_full);   // 唤醒生产者线程
        pthread_mutex_unlock(&_mtx);  // 解锁
    }

private:
    std::queue<T> _bq;       // 阻塞队列
    int _capacity;           // 阻塞队列容量
    pthread_mutex_t _mtx;  // 互斥锁地址
    pthread_cond_t _full;   // 用于标识阻塞队列已满的条件变量
    pthread_cond_t _empty;  // 用于标识阻塞队列为空的条件变量
};

代码3.2:ConProd.cc -- 生成消费者模型源文件代码

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "BlockQueue.hpp"

void *consume(void *args)
{
    BlockQueue<int> *pbq = (BlockQueue<int>*)args;

    int val = 0;
    while(true)
    {
        pbq->pop(&val);
        std::cout << "消费者获取了一个数据: " << val << std::endl;
        sleep(1);
    }

    return nullptr;
}

void *product(void *args)
{
    BlockQueue<int> *pbq = (BlockQueue<int>*)args;

    int a = 0;
    while(true)
    {
        pbq->push(a);
        std::cout << "生产者向阻塞队列中写入一个数据:" << a++ << std::endl;    
    }

    return nullptr;
}

int main()
{
    pthread_t c[2], p[2];   // 消费者与生产者线程id

    // 创建互斥锁
    pthread_mutex_t mtx;

    // 创建条件变量
    pthread_cond_t isFull;
    pthread_cond_t isEmpty;

    BlockQueue<int> *pbq = new BlockQueue<int>();

    // 创建两个生产者线程
    pthread_create(c, nullptr, consume, (void*)pbq);
    pthread_create(c + 1, nullptr, consume, (void*)pbq);

    // 创建两个消费者线程
    pthread_create(p, nullptr, product, (void*)pbq);
    pthread_create(p + 1, nullptr, product, (void*)pbq);

    // 主线程阻塞等待子线程退出
    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);

    delete pbq;

    return 0;
}

四. 总结

  • 单纯的线程互斥存在单个线程频繁多次申请到锁造成其他线程饥饿,以及临界资源不就绪的时候频繁申请和释放锁造成线程资源浪费的问题。
  • 引入线程同步可以解决上面的问题,线程同步就是让多个线程按照特定的次序访问临界资源。
  • pthread_cond_init、pthread_cond_wait、pthread_cond_signal、pthread_cond_broadcast 这几个函数配合使用,可以实现线程的同步。
  • 生产与消费者模型可以实现数据写端和读端的同步、实现写端和读端的解耦,充分利用多线程的并发特性提高效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/950987.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Revit SDK:PointCurveCreation 创建点来拟合曲线

前言 这个例子通过留个例子来展示如何通过点来拟合曲线或者曲面。 内容 PointsParabola 生成抛物线的核心逻辑&#xff1a; double yctr 0; XYZ xyz null; ReferencePoint rp null; double power 1.2; while (power < 1.5){double xctr 0;double zctr 0;while (…

数据集学习笔记(七):不同任务数据集的标签介绍(包含目标检测、图像分割、行为分析)

文章目录 一、目标检测1.1 TXT1.2 COCO1.3 XML 二、图像分割2.1 json2.1 TXT2.1.1 json转txt 三、行为分析3.1 TXT3.2 JSON 一、目标检测 1.1 TXT 每行表示&#xff08;类别&#xff0c;中心x相对坐标&#xff0c;中心y相对坐标&#xff0c;相对宽度、相对高度&#xff09; 1…

FFmpeg4.3.1+h264在windows下编译与VS2017项目集成

前言 在Android音视频开发中&#xff0c;网上知识点过于零碎&#xff0c;自学起来难度非常大&#xff0c;不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》&#xff0c;结合我自己的工作学习经历&#xff0c;我准备写一个音视频系列blog。本文是音视频系…

python spyder环境配置

首先安装python&#xff0c;配置环境变量等等 其次 pip install spyder 安装 spyder 最后启动 spyder&#xff0c;cmd下 执行 spyder&#xff0c;就打开了 调试下面的代码看看是否是系统的python import sys print(sys.executable) print(sys.path) 工具-偏好-python调试器 …

QT(8.30)常用类与组件,实现登录界面

1.作业&#xff1a; 完成一个登录界面(图片未附带): 头文件: #ifndef WIDGET_H #define WIDGET_H#include <QWidget>#include <QLineEdit>//行编辑器#include<QIcon>//图标#include<QLabel>//标签#include<QPushButton>//按钮#include<QIc…

【pyqt5界面化工具开发-13】QtDesigner功能择优使用

目录 0x00 前言&#xff1a; 一、完成基本的布局 二、其他功能的使用 三、在代码行开发 0x00 前言&#xff1a; QtDesigner工具的择优使用&#xff1a; 1、他的界面开发&#xff0c;是我们主要需要使用的功能 2、他的其他功能的使用&#xff0c;有需要就可使用&#xff…

报This application has no explicit mapping for /error 解决方法

Application启动类的位置不对&#xff0c;要将Application类放在最外侧&#xff0c;即包含所有子包。 没发现这个问题&#xff0c;卡了我老半天&#xff0c;兄弟们可以看一下你们是不是这个问题。

万人在线,一站式自动化运维 SysOM 3.0重磅发布!龙蜥社区系统运维 MeetUp 回顾来了

8 月 12 日&#xff0c;由龙蜥社区系统运维 SIG 主办&#xff0c;乘云数字协办的&#xff0c;主题为“观测&#xff0c;让运维更简单&#xff01;”的系统运维 MeetUp 于杭州圆满结束。来自乘云数字、谐云科技、乐维、云杉网络、擎创科技、观测云、阿里云以及浙江大学等众多厂商…

基于爬行动物算法优化的BP神经网络(预测应用) - 附代码

基于爬行动物算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于爬行动物算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.爬行动物优化BP神经网络2.1 BP神经网络参数设置2.2 爬行动物算法应用 4.测试结果&#xff1a;5…

vue3批量导出文件,打包成压缩包

1.下载插件 npm install jszip npm install file-saver2.封装方法 新建一个exportFileZip.js文件 // 引入实现下载压缩包的两个库 import JSZip from jszip; import FileSaver from file-saver; // 引入请求模块 import axios from axios // 实现下载压缩包按钮的方法 fileA…

kotlin实现猜数游戏

游戏规则 1.程序随机生成一个1到100的数字&#xff0c;作为MagicNumber 2.用户根据提示输入数据&#xff0c;只有三次机会输入数据 代码 代码很简单&#xff0c;使用了let内置函数 fun main() {//生成随机数可以使用java的方法//val magicNumber Random().nextInt(11)val ma…

GiraffeDet助力yolov8暴涨分---有可执行源码

Yolov8魔改–加入GiraffeDet模型提高小目标效果 VX搜索晓理紫关注并回复有yolov8-GiraffeDet获取代码 [晓理紫] 1 GiraffeDet模型 GiraffeDet是一种新颖的粗颈范例&#xff0c;一种类似长颈鹿的网络&#xff0c;用于高效的目标检测。 GiraffeDet 使用极其轻量的主干和非常深且…

助力网络管理的利器:企业办公网络中的VLAN划分策略

企业办公网络的性能和安全性对员工的高效工作和信息安全具有重要意义。在实现这一目标时&#xff0c;VLAN&#xff08;Virtual Local Area Network&#xff09;划分在网络设计中发挥着至关重要的作用。通过将办公网络划分为多个虚拟局域网&#xff0c;VLAN划分可以实现网络资源…

【论文精读】Swin Transformer: Hierarchical Vision Transformer using Shifted Windows

Swin Transformer: Hierarchical Vision Transformer using Shifted Windows 前言Abstract1. Introduction2. Related Work3. Method3.1. Overall Architecture3.2. Shifted Window based Self-AttentionSelf-attention in non-overlapped windowsShifted window partitioning …

streamlit-高级功能

缓存 st.cache_data st.cache_resource 为应用程序添加会话状态 会话状态 会话状态应用到应用程序 会话状态和小部件关联 可序列化会话状态 注意事项和限制 命令行选项 应用程序菜单 菜单选项 开发者选项 streamlit配置 按钮行为和示例 连接到数据 数据框 形式 小部件语义 …

pycharm 打开Terminal时报错activate.ps1,因为在此系统上禁止运行脚本,并因此无法进入虚拟环境

pycharm 打开Terminal时报错activate.ps1&#xff0c;因为在此系统上禁止运行脚本&#xff0c;并因此无法进入虚拟环境 如下图所示&#xff1a; 网上说可以set_restrictFalse什么的&#xff0c;虽然也可但可能会降低电脑安全性&#xff0c;可以将下面的终端改为cmd.exe即可

d3dx9_35.dll丢失怎么解决

今天&#xff0c;我将为大家介绍关于电脑d3dx9_35.dll丢失的4种详细修复方法。希望通过这次分享&#xff0c;能够帮助大家解决在日常工作和生活中遇到的一些问题。 首先&#xff0c;让我们来了解一下d3dx9_35.dll是什么&#xff1f; d3dx9_35.dll是一个非常重要的动态链接库文…

自测scRNA-Seq+scWGBS=3分三区文章?

写在前面 最近在捣鼓表观遗传学&#xff0c;处理了一批Bulk RNA-Seq和WGBS(Whole Genome Bisulfite Sequencing)的数据。熟悉我风格的粉丝都知道&#xff0c;我一般会读几篇文献再下手&#xff0c;遂于PubMed中检索了几篇文章&#xff0c;发现一个2022年发表的题为"WGBS …

十种高级的代码书写方式,提高代码质量和工作效率

1.集合遍历 不使用lambda&#xff1a; List<String> list Arrays.asList("kk", "oneone", "11"); for (String name : list) {System.out.println(name); }使用lambda&#xff1a; List<String> list Arrays.asList("kk&q…

如何使用工具将批量查询的物流信息导出到表格

现如今&#xff0c;物流行业发展迅速&#xff0c;人们对于物流信息的查询需求也越来越高。为了满足用户的需求&#xff0c;我们推荐一款便捷高效的物流信息查询工具——"固乔快递查询助手"软件。 首先&#xff0c;用户需要下载并安装"固乔快递查询助手"软件…