【Linux】多线程---线程控制

news2025/1/11 18:43:49

进程在前面已经讲过了,所以这次我们来讨论一下多线程。

前言:线程的背景

  • 进程是Linux中资源及事物管理的基本单位,是系统进行资源分配和调度的一个独立单位。但是实现进程间通信需要借助操作系统中专门的通信机制,但是只这些机制将占用大量的空间资源,特别是少量数据传递时显得庞大而欠灵活。因此才出现了线程。

  • 线程和进程一样,具有创建,退出,取消,等待等基本操作,可以独立完成特定事物的处理。并且线程占用的资源更少。

1.线程的基本概念?

1.1什么是线程:

在一个程序里的一个执行路线叫做线程。一切进程至少有一个执行路线。

1.2线程与进程的对比

1.21用户空间资源对比

一个进程的创建包含着进程控制块(task_struct),进程地址空间(mm_struct),以及页表的创建。

线程是当前进程内的一个执行流,并且在进程地址空间里运行,这个进程申请的所有资源都被线程共享。
  • 但是如果用fork函数创建一个子进程,而线程用ptread_creat()函数创建一个新线程,一起对比一下所占资源情况!

总结:

  1. 每个进程在创建是额外申请了新的内存和空间以及存储代码,数据段,堆,栈空间。并且初始化为父进程的值,父子进程在创建后不能互访对方资源。

  1. 每个创建的新线程在用户阶段仅申请自己的栈空间,并且与同进程的其他线程共享其他地址空间,这使得同进程下的个线程共享数据很方便。但是带来的问题也是同步的,接下来我们会讲到。

1.22内核空间资源对比

1.前面的进程中我们说了,每一个进程在内核中都有自己的进程控制块PCB来识别当前进程所能够访问的系统资源。通过该进程的PCB可以访问到进程的所有资源。
目前在Linux下的进程统称为轻量级进程,甚至很多的书中都说LINUX并不区分进程与线程,如果我们站在内核的角度想:在创建线程时Linux内核仍然创建一个新的PCB来表示这个线程,而内核对他俩的认识都来自PCB,所以内核并不认为他俩有区别。

2.在Linux下每一个进程的PCB的mm_struct都用来描述地址空间。而父子进程间的地址空间是分开的;而同一个进程下的线程共享这个地址空间,所以才在 用户的角度说两者是有区别的。但是从 调度的角度来看,操作系统是基于线程调度的,及内核并不认为他俩有区别。

3.一个进程如果不创建新的线程,那他就是只有一个线程的进程;如果创建了额外的线程,原来的进程也称为 主线程
总结一下:
4.进程在使用时占用了大量的内存,特别是进程间通信,这使得进程不够灵活,而且耗费资源,而线程占用资源少,使用灵活,且同进程下的线程间数据交互不需要经过OS,所以很多应用程序都使用了大量的进程,但是线程不能脱离进程而存在。

线程引进能做啥事?

如果说进程中有10个函数,但是只有一个线程,那这10个函数一定是按照顺序一次进行,但是如果有多个线程被创建,那这几个线程就可以分别实现这几个函数。
  • 单线程执行流被调度:

  • 多个执行流被调度:

linxu下没有真正的多线程

1.操作系统中存在大量的进程,一个进程内又存在一个或多个线程,因此线程的数量一定比进程的数量多,当线程的数量足够多的时候,很明显线程的执行粒度要比进程更细。

2.如果一款操作系统要支持真的线程,那么就需要对这些线程进行管理。比如说创建线程、终止线程、调度线程、切换线程、给线程分配资源、释放资源以及回收资源等等,这个和进程的绝大多数功能重复,所以委员会决定在内核层面没必要去区分他俩,都是用test_struct去表示。

3.因此,如果要支持真的线程一定会提高设计操作系统的复杂程度。在Linux看来,描述线程的控制块和描述进程的控制块是类似的,因此Linux并没有重新为线程设计数据结构,而是直接复用了进程控制块,所以我们说Linux中的所有执行流都叫做轻量级进程。

既然linxu中没有真正意义上的线程,所以就没有真正意义上的线程调用了。但是Linux可以提供创建轻量级进程的接口,也就是创建进程,共享空间,其中最典型的代表就是vfork函数。

提到vfork函数就不得不提到他的老大哥fork函数,这个vfork()函数其实就是fork的阉割版,fork创建的子进程是完全1:1模仿的父进程,但是操作者创建的进程如果是只想实现一些小功能函数,就不需要完全复制父进程。
fork():父进程的一个副本(代码+数据)
vfork():共享父进程的代码与数据

vfork()原型:

pid_t vfork(void);

vfork函数的返回值与fork函数的返回值相同:

  • 给父进程返回子进程的PID。

  • 给子进程返回0。

例如在下面的代码中,父进程使用vfork函数创建子进程,子进程将全局变量g_val由100改为了200,父进程休眠3秒后再读取到全局变量g_val的值。

可以看到,父进程读取到g_val的值是子进程修改后的值,也就证明了vfork创建的子进程与其父进程是共享地址空间的。

1.3从新理解啥是进程

  • 在用户视角:

内核数据结构+该进程对应的代码和数据
  • 内核视角:

承担系统资源分配的基本实体

刚开始创建一个进程时,该进程就会向操作系统要资源(内核区+栈+堆...),他是以进程的身份向系统要资源。当资源要完了,又创建新的线程时,线程就不在伸手向系统要了,而是向你这个进程要了。换句话说,创建新进程时,向操作系统要资源的不是线程,而是以进程为单位要的。既然是进程要的,那系统分配资源时不就是以进程为基本单位进行分配的吗?

怎样看我们写的代码呢?

原来我们写的代码都是一个task_struct,但是现在有多个task_struct,如果说以前的叫做单进程多线程的话,那现在的就叫做多进程多线程了,也就是说原来的是现在多个task_struct的一个子集。

也就是说一个进程就是一个地址空间,而一个线程就是一个task_struct。

  • CPU视角:

其实CPU不关心到底系统是咋区分进程和线程的,他只看task_struct,只要运行队列中有task_struct,就直接执行其中的代码和数据。CPU不关心到底代码和数据是和谁共享的,只要能执行就行。

1.4进程和线程之间的关系

  1. 线程的创建

2.1线程创建函数

一般我们都是用pthread_create()来创建一个线程的

man pthread_create   //查看其函数声明
  • 第一个参数:线程ID,没错进程有进程ID,线程有线程ID

  • 第二个参数:设置线程属性,没毛病知道我们学完线程这个默认也没关系,不用改。

  • 第三个参数:设置线程运行的代码起始地址是个函数指针。因为线程是进程一部分,所以用函数指针来接受进程的地址

  • 第四个参数:运行函数的参数地址。

2.2创建线程

先创建一个Makefile文件:

mythread:mythread.cc
    g++ -o $@ $^ -lpthread
.PHONY:clean
clean:
    rm -f mythread

那个lpthread是引入线程库,否则要是新创建一个线程的话就会报错

然后在创建一个thread.cc来存放代码:

#include <iostream>
#include <unistd.h> //这个getpid函数头文件
#include <pthread.h>
#include <cstdio>
#include <string.h>

using namespace std; // 这是C++中的命名空间

void *threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {

        cout << name << ", pid:" << getpid() << "\n"
             << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid[5]; // 一次创建5个线程
    char name[64];    // 这是线程名,但是可能有点小问题
    for (int i = 0; i < 5; i++)
    {
        snprintf(name, sizeof name, "%s-%d", "thread", i); // 给线程重新命名
        pthread_create(tid + i, NULL, threadRun, (void *)name);
        sleep(1);
    }
    while (true)
    {
        cout << "main thread, pid:" << getpid() << endl;
        sleep(3); // 避免和上面的混淆
    }
}
ldd mythread

确认一下有没有用到这里的库

运行一下mythread--->./mythread

接下来我们创建一个分屏:然后用ps axj查看命令来查看:

ps axj | head -1 && ps axj |grep mythread

但是这里却只有一个进程

不是说系统可以创建轻量级进程吗?在哪呢?

输入以下命令:

ps -aL
ps -aL | head -1 && ps -aL |grep mythread

看见PID后面那个LWP没那个就是轻量级进程对应的PID,19,20,26...

而第一个线程的PID和LWP是一样的,所以他叫做主线程。

所以操作系统调度线程时看的是LWP,因为PID和线程是一对多的关系。

我们输入一下kill -9 PID

当我们结束一个进程,那他其中的所有线程就都结束了。

代码区的共享

我再新写一个函数

void show(const string &name)
{
    cout << name << ", pid:" << getpid() << "\n" << endl;
}

void *threadRun(void *args)
{
    const string name = (char *)args;
    while (true)
    {

        show(name);     
        sleep(1);
    }
}

int main()
{
    pthread_t tid[5]; // 一次创建5个线程
    char name[64];    // 这是线程名,但是可能有点小问题
    for (int i = 0; i < 5; i++)
    {
        snprintf(name, sizeof name, "%s-%d", "thread", i); // 给线程重新命名
        pthread_create(tid + i, NULL, threadRun, (void *)name);
        sleep(1);
    }
    while (true)
    {
        cout << "main thread, pid:" << getpid() << endl;
        sleep(3); // 避免和上面的混淆
    }
}

这个函数能被所有的线程进行访问,再重新make一下,再编译也能跑。

  • 当然全局变量也可以被共享

2.3线程的缺点

说了这么多,我们就来聊一聊写线程时遇到的缺点:

  • 1.性能损失

一个很少被外部事件阻塞的计算机密集型线程往往无法与其他线程共享同一个处理器,如果密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的损失指的是增加了额外的同步和调度开销,而可以资源不变。

  • 2.健壮性降低

编写多线程需要更全面,更深入的考虑, 在一个多线程程序中,因时间分配上的细微差别或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程间是缺乏保护的。

  • 3.缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

  • 4.编程难度提高

编写与调试一个多线程程序比单线程困难的多。

2.4线程异常

这个后面会做示范,这里就提一下:

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随之崩溃。

  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程1,进程终止,该进程中的所有线程也就随即退出。

这里我们再重新创建一个新线程跑一跑试试

makefile文件和上面一样,为了方便继续用mythread.cc

#include <iostream>
#include <unistd.h> //这个getpid函数头文件
#include <pthread.h>
#include <cstdio>
#include <string.h>

using namespace std;

void *threadRoutine(void *args)
{ 
    while(true)
    {
        cout << "新线程: " << (char *)args << "running..." << endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid; // 创建线程
    pthread_create(&tid, NULL, threadRoutine, (void *)"thread 1");
    while (true)
    {
        cout << "main线程: "
             << " running ..." << endl;
        sleep(1);
    }
    return 0;
}

这两个新线程都可以跑

上面不是说如果一个线程崩溃其他线程也不能跑了,我们试一下吧。

我们把代码改一下:

这里的\=0代表 硬件异常,CPU中的状态标记为被置为0,这里我们来模拟线程出错。

重新make一下,报错不管,然后编译。

出现这个错误,然后在输入kill -l

又出现这个信号

这里就不存在我们创建的mythread线程了。

  1. 所以线程谁先运行与调度器有关。

  1. 如果一个线程出现异常,都可能导致整个进程退出。

  1. 线程在创建和执行的时候也是需要等待的。如果主线程不等待,就会导致

3.线程等待

3.1线程等待函数pthread_join()

man pthread_join
  • thread:被等待线程的ID。

  • retval:线程退出时的退出码信息。

void *threadRoutine(void *args)
{
    int i = 0;
    while (true)
    {
        cout << "新线程: " << (char *)args << "running..." << endl;
        sleep(1);
        if (i++ == 10)
        {
            break;
        }
    }
    cout << " new thread quit..." << endl;
}
int main()
{
    pthread_t tid; // 创建线程
    pthread_create(&tid, NULL, threadRoutine, (void *)"thread 1");

    pthread_join(tid, NULL);   //进程等待函数
    cout << "main thread wait down ...." << endl;
    while (true)
    {
        cout << "main线程: "
             << " running ..." << endl;
        sleep(1);
    }
    return 0;
}

如果发生进程等待就是先打印新线程,后打印main线程

一点毛病没有。

总结一下:

  1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。

  1. 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。

  1. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。

  1. 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。

3.2pthread_join第二个参数

我们写的功能函数的返回值是void*, 这个返回类型可以自己设置。

就比如我们让threadRoutine函数返回10,但是必须要强制类型转换成void*, 说白了就是把10当做一个指针数据,也就是说有一个地址,这个地址是10。

返回值但是返回给谁呢?

当然是返回给主线程了,主线程创建分线程就是办事的,所以必须要知道事办的咋样。但是主线程咋接收?所以此时就用到pthread_join()的第二个参数了。

指针就是一个地址,地址说白了就是一个数据,我们就把他看做自变常量。

void* 10看做一个数据。下面的ret看做空间。取指针的地址,所以第二个参数就是二级指针。

void *threadRoutine(void *args)
{
    int i = 0;
    while (true)
    {
        cout << "新线程: " << (char *)args << "running..." << endl;
        sleep(1);
      
        if (i++ == 3)
        {
            break;
        }
    }
    cout << " new thread quit..." << endl;
    return (void*) 10;
}
int main()
{
    pthread_t tid; // 创建线程
    pthread_create(&tid, NULL, threadRoutine, (void *)"thread 1");

    void* ret = NULL;
    pthread_join(tid, &ret); // 进程等待函数,默认阻塞等待新线程退出

    cout << "main thread wait down .... new thread init:" <<(long long)ret<<"\n"<< endl;
    
    return 0;
}

新线程退出以后,主线程就活动新线程的返回值了。

我们在来个新玩法:

void *threadRoutine(void *args)
{
    int i = 0;
    int *date = new int[10];
    while (true)
    {
        cout << "新线程: " << (char *)args << "running..." << endl;
        sleep(1);
      date[i]=i;
        if (i++ == 10)
        {
            break;
        }
    }
    cout << " new thread quit..." << endl;
   // return (void*) 10;
   return (void*)date;
}
int main()
{
    pthread_t tid; // 创建线程
    pthread_create(&tid, NULL, threadRoutine, (void *)"thread 1");

   int *ret = NULL;
    pthread_join(tid, (void**)&ret); // 进程等待函数,默认阻塞等待新线程退出

    cout << "main thread wait down .... new thread init:"<<"\n"<< endl;
    for(int i=0;i<10;i++)
    {
        cout<<ret[i]<<endl;
    }
    return 0;
}

这就显出来了

4.线程终止

进程可以终止,线程可以终止吗?

他自动停止了。

在线程中绝对不能调用exit,他是终止线程的。

4.1pthread_exit()

线程有他自己的结束函数:pthread_exit()

当然return也可以线程退出,但是这里就不过多的介绍了。

4.2pthread_cancel

线程取消函数也可以退出线程

man pthread_cancel
//q键退出

取消那个线程就把它的id传入就行了。

主线程可以取消新线程,取消成功的线程的退出码一般是-1,我们让新线程进入死循环,并且把新线程id传给取消函数。

using namespace std;

void *threadRoutine(void *args)
{
    int i = 0;
    int *date = new int[10];
    while (true)
    {
        cout << "新线程: " << (char *)args << "running..." << endl;
        sleep(1);
      date[i]=i;
        if (i++ == 5)
        {
            break;
        }
    }
    cout << " new thread quit..." << endl;
}
int main()
{
    pthread_t tid; // 创建线程
    pthread_create(&tid, NULL, threadRoutine, (void *)"thread 1");
    
    int count=0;
    while(true)
    {
    
        cout<<"main()线程:"<<"running..."<<endl;
        sleep(1);
        count++;
        if(count>=5)break;
    }
    pthread_cancel(tid);
    cout<<"pthread_cancel:"<<tid<<endl;
  int *ret = NULL;
    pthread_join(tid, (void**)&ret); // 进程等待函数,默认阻塞等待新线程退出
    cout << "main thread wait down .... new thread init:"<<(long long)ret<<"\n"<< endl;
    sleep(3);  //让主线程多活几秒
return 0;
}

接下来执行命令来监视一波:

while :; do ps -aL  | head -1 && ps -aL | grep mythread ; sleep 1 ; done 

既然主线程可以取消新线程,那反过来其实也可以,但是有啥实际意义呢,主线程取消,谁来帮忙管理其他新线程呢?而且内容比较凌乱,这里就不过多介绍了。

4.3线程id

刚才我们看到了一大串的数字,他们就被称作线程id,但是有些老铁认为这个线程id和线程的lwp其实是一个东西,但是他俩真的一样吗?

其实他俩没啥关系。id后面那一串数字是它的64位。而且id代表的是一个地址,所以它才是这一串数字。其实当我们调用pthread函数来实现一些对线程的操作时,我们调用的是pthread库而不是调用linux系统。当使用pthread库时,这个库就被加载到内存上面,而线程通过一系列操作最终被页表映射到内存上面。所以id其实就代表线程被映射到内存上的地址。

另外,主线程被系统存在cpu的栈里,而新线程则被存放在共享区新开辟的栈里。当时单执行流就调用cpu里主线程栈来操作。

获取自身线程id的函数:pthread_self

5.线程分离

  • 默认情况下,新创建的线程时joinable的,线程退出后,pthread_join操作,否则无法释放资源,从而造成系统泄漏。

  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

分离函数pthread_detach

int pthread_detach(pthread_t thread);

这里就不在细讲了,各位有兴趣去搜一下资料吧,饿死了!!!

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

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

相关文章

java并发入门(一)共享模型—Synchronized、Wait/Notify、pack/unpack

一、共享模型—管程 1、共享存在的问题 1.1 共享变量案例 package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;Slf4j(topic "c.MTest1") public class MTest1 {static int counter 0;public static void main(String[] args) throws InterruptedEx…

如何科学管理技术团队的研发交付速率?

每当提及「研发效能」&#xff0c;我们都在谈论什么&#xff1f; 研发效能管理要在保证质量的前提下&#xff0c;思考如何更快地向客户交付价值。在管理实践中&#xff0c;效能度量涉及三大维度&#xff1a;交付速率、交付质量、交付价值。 技术团队对内如何优化开发流程&…

STM32实战项目-基本定时器

前言&#xff1a; 通过基本定时器TIM6&#xff0c;让三个LED灯每间隔1s闪烁一次。 目录 1.基本定时器参数配置 1.1框图分析 1.2参数配置 2.软件程序 2.1整体框架 2.2定时器结构体 2.3定时器回调函数 1.基本定时器参数配置 1.1框图分析 TIM6作为基本定时器 它是挂载…

【Linux】-- 线程池

目录 铺垫 内存 线程的角度 线程池 基本代码结构 对于线程池的生产消费的完善 初步实现线程池生产消费 结合日志完善线程池 铺垫 内存 &#xff08;以STL处理方式&#xff0c;引入提供效率的一种思想&#xff09; 通过进行C语言与C语言的学习中&#xff0c;平时我们使…

C语言 深度剖析数据在内存中的存储(2)

本次博客是继上次博客&#xff0c;继续向下剖析数据在内存当中的存储。练习浮点型在内存中的存储练习代码1&#xff1a;int main() {char a -1;signed char b-1;unsigned char c-1;printf("a%d,b%d,c%d",a,b,c);return 0; }1.在本题中首先我们要知道的是%d打印的是有…

【数据结构之树】——什么是树,树的特点,树的相关概念和表示方法以及在实际的应用。

文章目录一、1.树是什么&#xff1f;2.树的特点二、树的相关概念三、树的表示方法1.常规方法表示树2.使用左孩子右兄弟表示法3. 使用顺序表来存储父亲节点的下标三、树在实际的应用总结一、1.树是什么&#xff1f; 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n&…

MatCap模拟光照效果实现

大家好&#xff0c;我是阿赵 之前介绍过各种光照模型的实现方法。那些光照模型的实现虽然有算法上的不同&#xff0c;但基本上都是灯光方向和法线方向的计算得出的明暗结果。 下面介绍一种叫做MatCap的模拟光照效果&#xff0c;这种方式计算非常简单&#xff0c;脱离灯光的计算…

javaWeb核心05-FilterListenerAjax(Axios)json

文章目录Filter&Listener&Ajax1&#xff0c;Filter1.1 Filter概述1.2 Filter快速入门1.2.1 开发步骤1.2.2 代码演示1.3 Filter执行流程1.4 Filter拦截路径配置1.5 过滤器链1.5.1 概述1.5.2 代码演示1.5.3 问题1.6 案例1.6.1 需求1.6.2 分析1.6.3 代码实现1.6.3.1 创建F…

Linux 安装 nginx 详细教程

文章目录Linux 安装 nginx 详细步骤①安装依赖包②下载并解压安装包③安装 nginx④启动 nginx 服务⑤配置 nginx.conf提示&#xff1a;以下是本篇文章正文内容&#xff0c;Linux 系列学习将会持续更新 Linux 安装 nginx 详细步骤 ①安装依赖包 下载模块依赖性 Nginx 需要依赖…

resp无法连接Redis服务的解决方法

在保证Windows主机和Linux虚拟机能够相互ping通的前提下&#xff0c;resp仍无法连接到Linux上的redis服务&#xff0c;那么需要考虑以下原因&#xff1a; Linux防火墙问题&#xff0c;Linux未关闭防火墙&#xff0c;或防火墙未放通6379/tcp端口&#xff1b;redis配置问题&#…

Project ERROR: Unknown module(s) in QT: webenginewidgets

Qt系列文章目录 文章目录Qt系列文章目录前言一、问题定位二、解决方法1.引入WebEngine库2.重新打开工程3. 解决办法&#xff1a;运行结果前言 最近项目中需要用到&#xff1a;Qt中使用cesium三维引擎库&#xff0c;涉及到Qt和和JavaScript之间通信&#xff0c;工程源码报错&am…

202109-3 CCF 脉冲神经网络 66分题解 + 解题思路 + 解题过程

解题思路 根据题意&#xff0c;脉冲源的阈值大于随机数时&#xff0c;会向其所有出点发送脉冲 神经元当v>30时&#xff0c;会向其所有出点发送脉冲&#xff0c;unordered_map <int, vector > ne; //存储神经元/脉冲源的所有出点集合vector 所有脉冲会有一定的延迟&am…

opencv-图像操作

访问和修改像素值 我们先加载一个彩色图像&#xff1a; import cv2img cv2.imread(b.png) print(img)########### 打印结果 ########### [[[243 243 243][243 243 243][243 243 243]...[243 243 243][243 243 243][243 243 243]][[243 243 243][243 243 243][243 243 243].…

每天五分钟机器学习:你理解贝叶斯公式吗?

本文重点 贝叶斯算法是机器学习算法中非常经典的算法,也是非常古老的一个算法,但是它至今仍然发挥着重大的作用,本节课程及其以后的专栏将会对贝叶斯算法来做一个简单的介绍。 贝叶斯公式 贝叶斯公式是由联合概率推导而来 其中p(Y|X)称为后验概率,P(Y)称为先验概率…

mysql navicat忘记密码

mysql忘记密码是常用的事情&#xff0c;那么如何解决它呢&#xff1f;1、首先将MySQL的服务关闭&#xff0c;两种方法&#xff1a;&#xff08;1&#xff09;打开命令行cmd输入net stop mysql命令即可关闭MySQL服务。&#xff08;2&#xff09;打开任务管理器&#xff0c;找到服…

【观察】亚信科技:“飞轮效应”背后的数智化创新“延长线”

著名管理学家吉姆柯林斯在《从优秀到卓越》一书中提出“飞轮效应”&#xff0c;它指的是为了使静止的飞轮转动起来&#xff0c;一开始必须使很大的力气&#xff0c;每转一圈都很费力&#xff0c;但达到某一临界点后&#xff0c;飞轮的重力和冲力就会成为推动力的一部分&#xf…

海思ubootsd卡协议

在start_armboot()函数中调用mmc_initialize(0)初始化mmc;最终调用到int hi_mci_initialize(unsigned int dev_num)函数;内容如下:static int hi_mci_initialize(unsigned int dev_num) {struct mmc *mmc NULL;static struct himci_host *host;unsigned int regval;unsigned l…

磨皮插件portraiture2023最新中文版

Portraiture滤镜是一款 Photoshop&#xff0c;Lightroom 和 Aperture 插件&#xff0c;DobeLighttroom 的 Portraiture 消除了选择性掩蔽和逐像素处理的繁琐的手工劳动&#xff0c;以帮助您在肖像修整方面取得卓越的效果。它是一个强大的&#xff0c;但用户友好的插件照明.这是…

深度解析首个Layer3 链 Nautilus Chain,有何优势?

以流支付为主要概念的Zebec生态&#xff0c;正在推动流支付这种新兴的支付方式向更远的方向发展&#xff0c;该生态最初以Zebec Protocol的形态发展&#xff0c;并从初期的Solana进一步拓展至BNB Chian以及Near上。与此同时&#xff0c;Zebec生态也在积极的寻求从协议形态向公链…

观察UE4里“在外部存储Actor”功能的基础行为

目标 一般情况下&#xff0c;Actor保存于关卡文件中。 但是&#xff0c;如果将Actor的 packaging mode 设置为 External&#xff1a; 则此Actor就会存储在另一个文件而非关卡文件中。 本篇目标是&#xff1a; 观察此功能的基础行为观察外部文件的路径名规则 “在外部存储A…