[Abp 源码分析]后台作业与后台工作者

点击上方蓝字关注我们

0. 简介

在某些时候我们可能会需要执行后台任务,或者是执行一些周期性的任务。比如说可能每隔 1 个小时要清除某个临时文件夹内的数据,可能用户会要针对某一个用户群来群发一组短信。前面这些就是典型的应用场景,在 Abp 框架里面为我们准备了后台作业和后台工作者来帮助我们解决这个问题。

后台作业与后台工作者的区别是,前者主要用于某些耗时较长的任务,而不想阻塞用户的时候所使用。后者主要用于周期性的执行某些任务,从 “工作者” 的名字可以看出来,就是一个个工人,而且他们每个工人都拥有单独的后台线程。

0.1 典型场景

后台作业

  • 某个用户按下了报表按钮来生成一个需要长时间等待的报表。你添加这个工作到队列中,当报表生成完毕后,发送报表结果到该用户的邮箱。

  • 在后台作业中发送一封邮件,有些问题可能会导致发送失败(网络连接异常,或者主机宕机);由于有后台作业以及持久化机制,在问题排除后,可以重试以保证任务的成功执行。

后台工作者

  • 后台工作者能够周期性地执行旧日志的删除。

  • 后台工作者可以周期性地筛选出非活跃性用户,并且发送回归邮件给这些用户。

1. 启动流程

后台作业与后台工作者都是通过各自的 Manager(IBackgroundJobManager/IBackgroundWorkerManager) 来进行管理的。而这两个 Manager 分别继承了 ISingletonDependency 接口,所以在启动的时候就会自动注入这两个管理器以便开发人员管理操作。

这里值得注意的一点是,IBackgroundJobManager 接口是 IBackgroundWorker 的派生接口,而 IBackgroudWorker 是归属于 IBackgroundWorkerManager 进行管理的。

所以,你可以在 AbpKernelModule 里面看到如下代码:

public sealed class AbpKernelModule : AbpModule
{public override void PostInitialize(){// 注册可能缺少的组件RegisterMissingComponents();// ... 忽略的代码// 各种管理器的初始化操作// 从配置项中读取,是否启用了后台作业功能if (Configuration.BackgroundJobs.IsJobExecutionEnabled){var workerManager = IocManager.Resolve<IBackgroundWorkerManager>();// 开始启动后台工作者workerManager.Start();// 增加后台作业管理器workerManager.Add(IocManager.Resolve<IBackgroundJobManager>());}}
}

可以看到,后台作业管理器是作为一个后台工作者被添加到了 IBackgroundWorkerManager 当中来执行的。

2. 代码分析

2.1 后台工作者

2.1.1 后台工作者管理器

Abp 通过后台工作者管理器来管理后台作业队列,所以我们首先来看一下后台工作者管理器接口的定义是什么样子的。

public interface IBackgroundWorkerManager : IRunnable
{void Add(IBackgroundWorker worker);
}

还是相当简洁的,就一个 Add 方法用来添加一个新的后台工作者对象。只是在这个地方,可以看到该接口又是集成自 IRunnable 接口,那么该接口的作用又是什么呢?

转到其定义可以看到,IRunable 接口定义了三个基本的方法:Start()Stop()WaitStop() ,而且他拥有一个默认实现 RunableBase,其实就是用来标识一个任务的运行状态。

public interface IRunnable
{// 开始执行任务void Start();// 停止执行任务void Stop();// 阻塞线程,等待任务执行完成后标识为停止。void WaitToStop();
}public abstract class RunnableBase : IRunnable
{// 用于标识任务是否运行的布尔值变量public bool IsRunning { get { return _isRunning; } }private volatile bool _isRunning;// 启动之后表示任务正在运行public virtual void Start(){_isRunning = true;}// 停止之后表示任务结束运行public virtual void Stop(){_isRunning = false;}public virtual void WaitToStop(){}
}

到目前为止整个代码都还是比较简单清晰的,我们接着看 IBackgroundWorkerManager 的默认实现 BackgroundWorkerManager 类,首先我们看一下该类拥有哪些属性与字段。

public class BackgroundWorkerManager : RunnableBase, IBackgroundWorkerManager, ISingletonDependency, IDisposable
{private readonly IIocResolver _iocResolver;private readonly List<IBackgroundWorker> _backgroundJobs;public BackgroundWorkerManager(IIocResolver iocResolver){_iocResolver = iocResolver;_backgroundJobs = new List<IBackgroundWorker>();}
}

在后台工作者管理器类的内部,默认有一个 List 集合,用于维护所有的后台工作者对象。那么其他的 Start() 等方法肯定是基于这个集合进行操作的。

public override void Start()
{base.Start();_backgroundJobs.ForEach(job => job.Start());
}public override void Stop()
{_backgroundJobs.ForEach(job => job.Stop());base.Stop();
}public override void WaitToStop()
{_backgroundJobs.ForEach(job => job.WaitToStop());base.WaitToStop();
}

可以看到实现还是比较简单的,接下来我们继续看他的 Add() 方法是如何进行操作的?

public void Add(IBackgroundWorker worker)
{_backgroundJobs.Add(worker);if (IsRunning){worker.Start();}
}

在这里我们看到他会针对 IsRunning 进行判定是否立即启动加入的后台工作者对象。而这个 IsRunning 属性值唯一产生变化的情况就在于 Start() 方法与 Stop() 方法的调用。

最后肯定也有相关的销毁方法,用于释放所有注入的后台工作者对象,并将集合清除。

private bool _isDisposed;public void Dispose()
{if (_isDisposed){return;}_isDisposed = true;// 遍历集合,通过 Ioc 解析器的 Release 方法释放对象_backgroundJobs.ForEach(_iocResolver.Release);// 清空集合_backgroundJobs.Clear();
}

所以,针对于所有后台工作者的管理,都是通过 IBackgroundWorkerManager 来进行操作的。

2.1.2 后台工作者

看完了管理器,我们来看一下 IBackgroundWorker 后台工作者对象是怎样的构成。

public interface IBackgroundWorker : IRunnable
{}

貌似只是一个空的接口,其作用主要是标识某个类型是否为后台工作者,转到其抽象类实现 BackgroundWorkerBase,里面只是注入了一些辅助对象与本地化的一些方法。

public abstract class BackgroundWorkerBase : RunnableBase, IBackgroundWorker
{// 配置管理器public ISettingManager SettingManager { protected get; set; }// 工作单元管理器public IUnitOfWorkManager UnitOfWorkManager{get{if (_unitOfWorkManager == null){throw new AbpException("Must set UnitOfWorkManager before use it.");}return _unitOfWorkManager;}set { _unitOfWorkManager = value; }}private IUnitOfWorkManager _unitOfWorkManager;// 获得当前的工作单元protected IActiveUnitOfWork CurrentUnitOfWork { get { return UnitOfWorkManager.Current; } }// 本地化资源管理器public ILocalizationManager LocalizationManager { protected get; set; }// 默认的本地化资源的源名称protected string LocalizationSourceName { get; set; }protected ILocalizationSource LocalizationSource{get{// 如果没有配置源名称,直接抛出异常if (LocalizationSourceName == null){throw new AbpException("Must set LocalizationSourceName before, in order to get LocalizationSource");}if (_localizationSource == null || _localizationSource.Name != LocalizationSourceName){_localizationSource = LocalizationManager.GetSource(LocalizationSourceName);}return _localizationSource;}}private ILocalizationSource _localizationSource;// 日志记录器public ILogger Logger { protected get; set; }protected BackgroundWorkerBase(){Logger = NullLogger.Instance;LocalizationManager = NullLocalizationManager.Instance;}// ... 其他模板代码
}

我们接着看继承并实现了 BackgroundWorkerBase 的类型 PeriodicBackgroundWorkerBase,从字面意思上来看,该类型应该是一个定时后台工作者基类。

重点在于 Periodic(定时),从其类型内部的定义可以看到,该类型使用了一个 AbpTimer 对象来进行周期计时与具体工作任务的触发。我们暂时先不看这个 AbpTimer,仅仅看 PeriodicBackgroundWorkerBase 的内部实现。

public abstract class PeriodicBackgroundWorkerBase : BackgroundWorkerBase
{protected readonly AbpTimer Timer;// 注入 AbpTimerprotected PeriodicBackgroundWorkerBase(AbpTimer timer){Timer = timer;// 绑定周期执行的任务,这里是 DoWork()Timer.Elapsed += Timer_Elapsed;}public override void Start(){base.Start();Timer.Start();}public override void Stop(){Timer.Stop();base.Stop();}public override void WaitToStop(){Timer.WaitToStop();base.WaitToStop();}private void Timer_Elapsed(object sender, System.EventArgs e){try{DoWork();}catch (Exception ex){Logger.Warn(ex.ToString(), ex);}}protected abstract void DoWork();
}

可以看到,这里基类绑定了 DoWork() 作为其定时执行的方法,那么用户在使用的时候直接继承自该基类,然后重写 DoWork() 方法即可绑定自己的后台工作者的任务。

2.1.3 AbpTimer 定时器

在上面的基类我们看到,基类的 Start()Stop()WaitTpStop() 方法都是调用的 AbpTimer 所提供的,所以说 AbpTimer 其实也继承了 RunableBase 基类并实现其具体的启动与停止操作。

其实 AbpTimer 的核心就是通过 CLR 的 Timer 来实现周期性任务执行的,不过默认的 Timer 类有两个比较大的问题。

  1. CLR 的 Timer 并不会等待你的任务执行完再执行下一个周期的任务,如果你的某个任务耗时过长,超过了 Timer 定义的周期。那么 Timer 会开启一个新的线程执行,这样的话最后我们系统的资源会因为线程大量重复创建而被拖垮。

  2. 如何知道一个 Timer 所执行的业务方法已经真正地被结束了。

所以 Abp 才会重新封装一个 AbpTimer 作为一个基础的计时器。第一个问题的解决方法很简单,就是在执行具体绑定的业务方法之前,通过 Timer.Change() 方法来让 Timer 临时失效。等待业务方法执行完成之后,再将 Timer 的周期置为用户设定的周期。

// CLR Timer 绑定的回调方法
private void TimerCallBack(object state)
{lock (_taskTimer){if (!_running || _performingTasks){return;}// 暂时让 Timer 失效_taskTimer.Change(Timeout.Infinite, Timeout.Infinite);// 设置执行标识为 TRUE,表示当前的 AbpTimer 正在执行_performingTasks = true;}try{// 如果绑定了相应的触发事件if (Elapsed != null){// 执行相应的业务方法,这里就是最开始绑定的 DoWork() 方法Elapsed(this, new EventArgs());}}catch{}finally{lock (_taskTimer){// 标识业务方法执行完成_performingTasks = false;if (_running){// 更改周期为用户指定的执行周期,等待下一次触发_taskTimer.Change(Period, Timeout.Infinite);}Monitor.Pulse(_taskTimer);}}
}

针对于第二个问题,Abp 通过 WaitToStop() 方法会阻塞调用这个 Timer 的线程,并且在 _performingTasks 标识位是 false 的时候释放。

public override void WaitToStop()
{// 锁定 CLR 的 Timer 对象lock (_taskTimer){// 循环检测while (_performingTasks){Monitor.Wait(_taskTimer);}}base.WaitToStop();
}

至于其他的 Start() 方法就是使用 CLR 的 Timer 更改其执行周期,而 Stop() 就是直接将 Timer 的周期设置为无限大,使计时器失效。

2.1.4 总结

Abp 后台工作者的核心就是通过 AbpTimer 来实现周期性任务的执行,用户只需要继承自 PeriodicBackgroundWorkerBase,然后将其添加到 IBackgroundWorkerManager 的集合当中。这样 Abp 在启动之后就会遍历这个工作者集合,然后周期执行这些后台工作者绑定的方法。

当然如果你继承了 PeriodicBackgroundWorkerBase 之后,可以通过设置构造函数的 AbpTimer 来指定自己的执行周期。

2.2 后台作业队列

后台工作队列的管理是通过 IBackgroundJobManager 来处理的,而该接口又继承自 IBackgroundWorker,所以一整个后台作业队列就是一个后台工作者,只不过这个工作者有点特殊。

2.2.1 后台作业管理器

IBackgroundJobManager 接口的定义其实就两个方法,一个 EnqueueAsync<TJob, TArgs>() 用于将一个后台作业加入到执行队列当中。而 DeleteAsync() 方法呢,顾名思义就是从队列当中移除指定的后台作业。

首先看一下其默认实现 BackgroundJobManager,该实现同样是继承自 PeriodicBackgroundWorkerBase 并且其默认周期为 5000 ms。

public class BackgroundJobManager : PeriodicBackgroundWorkerBase, IBackgroundJobManager, ISingletonDependency
{// 事件总线public IEventBus EventBus { get; set; }// 轮训后台作业的间隔,默认值为 5000 毫秒.public static int JobPollPeriod { get; set; }// IOC 解析器private readonly IIocResolver _iocResolver;// 后台作业队列存储private readonly IBackgroundJobStore _store;static BackgroundJobManager(){JobPollPeriod = 5000;}public BackgroundJobManager(IIocResolver iocResolver,IBackgroundJobStore store,AbpTimer timer): base(timer){_store = store;_iocResolver = iocResolver;EventBus = NullEventBus.Instance;Timer.Period = JobPollPeriod;}
}

基础结构基本上就这个样子,接下来看一下他的两个接口方法是如何实现的。

EnqueueAsync<TJob, TArgs> 方法通过传入指定的后台作业对象和相应的参数,同时还有任务的优先级。将其通过 IBackgroundJobStore 进行持久化,并返回一个任务的唯一 JobId 以便进行删除操作。

public async Task<string> EnqueueAsync<TJob, TArgs>(TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null)where TJob : IBackgroundJob<TArgs>
{// 通过 JobInfo 包装任务的基本信息var jobInfo = new BackgroundJobInfo{JobType = typeof(TJob).AssemblyQualifiedName,JobArgs = args.ToJsonString(),Priority = priority};// 如果需要延时执行的话,则用当前时间加上延时的时间作为任务下次运行的时间if (delay.HasValue){jobInfo.NextTryTime = Clock.Now.Add(delay.Value);}// 通过 Store 进行持久话存储await _store.InsertAsync(jobInfo);// 返回后台任务的唯一标识return jobInfo.Id.ToString();
}

至于删除操作,在 Manager 内部其实也是通过 IBackgroundJobStore 进行实际的删除操作的。

public async Task<bool> DeleteAsync(string jobId)
{// 判断 jobId 的值是否有效if (long.TryParse(jobId, out long finalJobId) == false){throw new ArgumentException($"The jobId '{jobId}' should be a number.", nameof(jobId));}// 使用 jobId 从 Store 处筛选到 JobInfo 对象的信息BackgroundJobInfo jobInfo = await _store.GetAsync(finalJobId);if (jobInfo == null){return false;}// 如果存在有 JobInfo 则使用 Store 进行删除操作await _store.DeleteAsync(jobInfo);return true;
}

后台作业管理器实质上是一个周期性执行的后台工作者,那么我们的后台作业是每 5000 ms 执行一次,那么他的 DoWork() 方法又在执行什么操作呢?

protected override void DoWork()
{// 从 Store 当中获得等待执行的后台作业集合var waitingJobs = AsyncHelper.RunSync(() => _store.GetWaitingJobsAsync(1000));// 遍历这些等待执行的后台任务,然后通过 TryProcessJob 进行执行foreach (var job in waitingJobs){TryProcessJob(job);}
}

可以看到每 5 秒钟我们的后台作业管理器就会从 IBackgroundJobStore 当中拿到最大 1000 条的后台作业信息,然后遍历这些信息。通过 TryProcessJob(job) 方法来执行后台作业。

而 TryProcessJob() 方法,本质上就是通过反射构建出一个 IBackgroundJob 对象,然后取得序列化的参数值,通过反射得到的 MethodInfo 对象来执行我们的后台任务。执行完成之后,就会从 Store 当中移除掉执行完成的任务。

针对于在执行过程当中所出现的异常,会通过 IEventBus 触发一个 AbpHandledExceptionData 事件记录后台作业执行失败时的异常信息。并且一旦在执行过程当中出现了任何异常的情况,都会将该任务的 IsAbandoned 字段置为 true,当该字段为 true 时,该任务将不再回被执行。

PS:就是在 GetWaitingJobsAsync() 方法时,会过滤掉 IsAbandoned 值为 true 的任务。

private void TryProcessJob(BackgroundJobInfo jobInfo)
{try{// 任务执行次数自增 1jobInfo.TryCount++;// 最后一次执行时间设置为当前时间jobInfo.LastTryTime = Clock.Now;// 通过反射取得后台作业的类型var jobType = Type.GetType(jobInfo.JobType);// 通过 Ioc 解析器得到一个临时的后台作业对象,执行完之后既被释放using (var job = _iocResolver.ResolveAsDisposable(jobType)){try{// 通过反射得到后台作业的 Execute 方法var jobExecuteMethod = job.Object.GetType().GetTypeInfo().GetMethod("Execute");var argsType = jobExecuteMethod.GetParameters()[0].ParameterType;var argsObj = JsonConvert.DeserializeObject(jobInfo.JobArgs, argsType);// 结合持久话存储的参数信息,调用 Execute 方法进行后台作业jobExecuteMethod.Invoke(job.Object, new[] { argsObj });// 执行完成之后从 Store 删除该任务的信息AsyncHelper.RunSync(() => _store.DeleteAsync(jobInfo));}catch (Exception ex){Logger.Warn(ex.Message, ex);// 计算下一次执行的时间,一旦超过 2 天该任务都执行失败,则返回 nullvar nextTryTime = jobInfo.CalculateNextTryTime();if (nextTryTime.HasValue){jobInfo.NextTryTime = nextTryTime.Value;}else{// 如果为 null 则说明该任务在 2 天的时间内都没有执行成功,则放弃继续执行jobInfo.IsAbandoned = true;}// 更新 Store 存储的任务信息TryUpdate(jobInfo);// 触发异常事件EventBus.Trigger(this,new AbpHandledExceptionData(new BackgroundJobException("A background job execution is failed. See inner exception for details. See BackgroundJob property to get information on the background job.", ex){BackgroundJob = jobInfo,JobObject = job.Object}));}}}catch (Exception ex){Logger.Warn(ex.ToString(), ex);// 表示任务不再执行jobInfo.IsAbandoned = true;// 更新 StoreTryUpdate(jobInfo);}
}

2.2.2 后台作业

后台作业的默认接口定义为 IBackgroundJob<in TArgs> ,他只有一个 Execute(TArgs args) 方法,用于接收指定类型的作业参数,并执行。

一般来说我们不建议直接通过继承 IBackgroundJob<in TArgs> 来实现后台作业,而是继承自 BackgroundJob<TArgs> 抽象类。该抽象类内部也没有什么特别的实现,主要是注入了一些基础设施,比如说 UOW 与 本地化资源管理器,方便我们开发使用。

后台作业本身是具体执行的对象,而 BackgroundJobInfo 则是存储了后台作业的 Type 类型和参数,方便在需要执行的时候通过反射的方式执行后台作业。

2.2.2 后台作业队列存储

从 IBackgroundJobStore 我们就可以猜到以 Abp 框架的套路,他肯定会有两种实现,第一种就是基于内存的 InMemoryBackgroundJobStore。而第二种呢,就是由 Abp.Zero 模块所提供的基于数据库的 BackgroundJobStore

IBackgroundJobStore 接口所定义的方法基本上就是增删改查,没有什么复杂的。

public interface IBackgroundJobStore
{// 通过 JobId 获取后台任务信息Task<BackgroundJobInfo> GetAsync(long jobId);// 插入一个新的后台任务信息Task InsertAsync(BackgroundJobInfo jobInfo);/// <summary>/// Gets waiting jobs. It should get jobs based on these:/// Conditions: !IsAbandoned And NextTryTime &lt;= Clock.Now./// Order by: Priority DESC, TryCount ASC, NextTryTime ASC./// Maximum result: <paramref name="maxResultCount"/>./// </summary>/// <param name="maxResultCount">Maximum result count.</param>Task<List<BackgroundJobInfo>> GetWaitingJobsAsync(int maxResultCount);/// <summary>/// Deletes a job./// </summary>/// <param name="jobInfo">Job information.</param>Task DeleteAsync(BackgroundJobInfo jobInfo);/// <summary>/// Updates a job./// </summary>/// <param name="jobInfo">Job information.</param>Task UpdateAsync(BackgroundJobInfo jobInfo);
}

这里先从简单的内存 Store 说起,这个 InMemoryBackgroundJobStore 内部使用了一个并行字典来存储这些任务信息。

public class InMemoryBackgroundJobStore : IBackgroundJobStore
{private readonly ConcurrentDictionary<long, BackgroundJobInfo> _jobs;private long _lastId;public InMemoryBackgroundJobStore(){_jobs = new ConcurrentDictionary<long, BackgroundJobInfo>();}
}

相当简单,这几个接口方法基本上就是针对与这个并行字典操作的一层封装。

public Task<BackgroundJobInfo> GetAsync(long jobId)
{return Task.FromResult(_jobs[jobId]);
}public Task InsertAsync(BackgroundJobInfo jobInfo)
{jobInfo.Id = Interlocked.Increment(ref _lastId);_jobs[jobInfo.Id] = jobInfo;return Task.FromResult(0);
}public Task<List<BackgroundJobInfo>> GetWaitingJobsAsync(int maxResultCount)
{var waitingJobs = _jobs.Values// 首先筛选出不再执行的后台任务.Where(t => !t.IsAbandoned && t.NextTryTime <= Clock.Now)// 第一次根据后台作业的优先级进行排序,高优先级优先执行.OrderByDescending(t => t.Priority)// 再根据执行次数排序,执行次数越少的,越靠前.ThenBy(t => t.TryCount).ThenBy(t => t.NextTryTime).Take(maxResultCount).ToList();return Task.FromResult(waitingJobs);
}public Task DeleteAsync(BackgroundJobInfo jobInfo)
{_jobs.TryRemove(jobInfo.Id, out _);return Task.FromResult(0);
}public Task UpdateAsync(BackgroundJobInfo jobInfo)
{// 如果是不再执行的任务,删除if (jobInfo.IsAbandoned){return DeleteAsync(jobInfo);}return Task.FromResult(0);
}

至于持久化到数据库,无非是注入一个仓储,然后针对这个仓储进行增删查改的操作罢了,这里就不在赘述。

2.2.3 后台作业优先级

后台作业的优先级定义在 BackgroundJobPriority 枚举当中,一共有 5 个等级,分别是 LowBelowNormalNormalAboveNormalHigh ,他们从最低到最高排列。

作者:myzony

出处:https://www.cnblogs.com/myzony/p/9841601.html

扫描二维码

获取更多精彩

码侠江湖


喜欢就点个在看再走吧

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

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

相关文章

【转】x.509证书在WCF中的应用(CS篇)

【转自】x.509证书在WCF中的应用(CS篇) 为什么要用x.509证书? WCF的服务端和客户端之间&#xff0c;如 果不作任何安全处理(即服务端的<security mode"None">)&#xff0c;则所有传输的消息将以明文方式满天飞&#xff0c;在internet/intranet环境下无疑是很不…

从概念到案例:初学者须知的十大机器学习算法

本文先为初学者介绍了必知的十大机器学习&#xff08;ML&#xff09;算法&#xff0c;并且我们通过一些图解和实例生动地解释这些基本机器学习的概念。我们希望本文能为理解机器学习基本算法提供简单易读的入门概念。 机器学习模型 在《哈佛商业评论》发表「数据科学家是 21 世…

测试Live Write的插件

1、文字竖排&#xff1a; 删除&#xff0c;只因首页显示时太占空间。2、酷表情&#xff1a;3、Rhapsody SongI am listening to Sad Songs And Waltzes by Cake . Rhapsody.

手把手教你用7行代码实现微信聊天机器人 -- Python wxpy

环境要求&#xff1a; Windows / Linux / Mac OS Python 3.4-3.6&#xff0c;以及 2.7 版本 wxpy安装 ## 使用国内源安装速度快 pip install -U wxpy -i "https://pypi.doubanio.com/simple/" 实例 让机器人与所有好友聊天 from wxpy import * # 实例化&#xff0c;并…

Dapr 已在塔架就位 将发射新一代微服务

微服务是云原生架构的核心&#xff0c;通常使用Kubernetes 来按需管理服务扩展。微软一直走在 Cloud Native Computing Foundation的 最前沿&#xff0c;并通过使用Kubernetes来支持其超大规模Azure和其混合云Azure Stack&#xff0c;微软对云原生的投资一部分来自其工具&#…

python 复制文件_10 行 Python 代码写 1 个 USB 病毒

(给Python开发者加星标&#xff0c;提升Python技能)转自&#xff1a; 知乎-DeepWeaver昨天在上厕所的时候突发奇想&#xff0c;当你把usb插进去的时候&#xff0c;能不能自动执行usb上的程序。查了一下&#xff0c;发现只有windows上可以&#xff0c;具体的大家也可以搜索(搜索…

html5中外描边怎么写,CSS3实现文字描边的2种方法(小结)

问题最近遇到一个需求&#xff0c;需要实现文字的描边效果&#xff0c;如下图解决方法一首先想到去看CSS3有没有什么属性可以实现&#xff0c;后来被我找到了text-stroke该属性是一个复合属性&#xff0c;可以设置文字宽度和文字描边颜色该属性使用很简单&#xff1a;text-stro…

混凝土墙开洞_满城混凝土柱子切割资质齐全

满城混凝土柱子切割资质齐全专业楼板切割开洞&#xff0c;钢筋混凝土墙开门&#xff0c;开窗&#xff0c;开方洞。混泥土承重墙新开门洞、开窗、通风管道开洞、专业开楼梯口&#xff0c;楼梯口加固&#xff0c;地下室开门洞&#xff0c;水泥墙开门加固、楼板加固、砖墙开门开窗…

马云害怕的事还是发生了

当前&#xff0c;余额宝的收益维持在4%左右不能突破&#xff0c;只能用作“钱包”放点零钱了。 放银行或者余额宝收益偏低&#xff0c;股票市场又处于震荡周期&#xff0c;期货市场等不是普通人进得去的&#xff0c;还不如直接买较高收益的互联网理财产品。 比如屡受政策利好…

cmosfixr插件怎么用_3dmax插件神器|怎么用3dmax插件神器去完成背景墙的效果图设计?...

又到3dmax插件神器的小课堂时间了&#xff01;小伙伴们还记得之前几张的知识点吗&#xff1f;如果不记得自己去温习&#xff0c;温故而知新哦&#xff01;如果学会了&#xff0c;下面学习3dmax插件神器小技巧的第四章建模篇的第4.16小节&#xff1a;怎么用3dmax插件神器去完成背…

不懂这25个名词,好意思说你懂大数据?

如果你刚接触大数据&#xff0c;你可能会觉得这个领域很难以理解&#xff0c;无从下手。近日&#xff0c;Ramesh Dontha在DataConomy上连发两篇文章&#xff0c;扼要而全面地介绍了关于大数据的75个核心术语&#xff0c;这不仅是大数据初学者很好的入门资料&#xff0c;对于高阶…

ab压力测试_Apache ab压力测试的知识点

Apache-ab是著名的Web服务器软件Apache附带的一个小工具&#xff0c;它可以模拟多个并发请求&#xff0c;测试服务器的最大承载压力。ab 是apachebench的缩写,ab命令会创建多个并发访问线程&#xff0c;模拟多个访问者同时对某一URL地址进行访问。它的测试目标是基于URL的&…

现代云原生设计理念

前文传送门什么是云原生&#xff1f;现代设计理念你会如何设计云原生应用程序&#xff1f;需要遵循哪些原则、模式和最佳实践&#xff1f;需要特别关注哪些底层/操作&#xff1f;十二要素应用程序目前被普遍认可的基于云的方法论是"十二要素应用程序"&#xff0c;它给…

NFS服务器架设篇

大家好&#xff0c;本周我们的课程是NFS服务器的架设。下面我们分几个部分来介绍NFS服务器。一、NFS简介NFS是分布式计算机系统的一部分&#xff0c;一般在用unix和类unix的系统上实现文件的传输。而且可以把NFS服务器共享的目录挂载到本地&#xff0c;使用cp&#xff0c;cd&am…

用画小狗的方法来解释Java中的值传递

在开始看我画小狗之前&#xff0c;咱们先来看道很简单的题目&#xff1a; 下面程序的输出是什么&#xff1f; 如果你的回答是“小强”&#xff0c;好&#xff0c;恭喜你答对了。下面我们改一下代码&#xff1a; 是的&#xff0c;我只是在changeName方法里面加了一句代码 这一次…

gif分解工具_Python之GIF图倒放,沙雕快乐源泉

GIF图现在已经融入了我们的日常网络生活&#xff0c;微信群、QQ群、朋友圈......一言不合就斗图&#xff0c;你怕了吗&#xff1f;不用担心&#xff0c;只要学会了Python之GIF倒放技能&#xff0c;你就是“斗图王”。咱们直接开始本文的内容&#xff01;使用的工具1PIL(Python …

微软亚洲研究院全球院友线上欢聚,共话新春

金鼠辞旧岁&#xff0c;金牛报春时&#xff1b;万象正更新&#xff0c;乾坤喜气多。西雅图时间 2 月 6 日&#xff0c;北京时间 2 月 7 日&#xff0c;由微软亚洲研究院院友会西雅图分会主办的“牛转新运”院友新春线上茶话会圆满落幕。重量级嘉宾沈向洋、洪小文、张亚勤、张宏…

从串行线程封闭到对象池、线程池

今天讲一个牛逼而实用的概念&#xff0c;串行线程封闭。对象池是串行线程封闭的典型应用场景&#xff1b;线程池糅合了对象池技术&#xff0c;但核心实现不依赖于对象池&#xff0c;很容易产生误会。 本文从串行线程封闭和对象池入手&#xff0c;最后通过源码分析线程池的核心原…

netty springmvc_springmvc源码架构解析之HandlerMapping

说在前面前期回顾sharding-jdbc源码解析 更新完毕spring源码解析 更新完毕spring-mvc源码解析 更新完毕spring-tx源码解析 更新完毕spring-boot源码解析 更新完毕rocketmq源码解析 更新完毕dubbbo源码解析 更新完毕netty源码解析 更新完毕spring源码架构更新完毕springmvc源码架…

腾讯牛逼,我酸了!!

阅读本文大概需要8分钟。腾讯这两天搞了个业内爆炸沸腾的事情&#xff1a;全员阳光普照发放100股&#xff0c;解禁期一年。腾讯股价近年来一直在疯狂上涨&#xff0c;100股折合人民币6万多&#xff1a;关键是员工什么都没做&#xff0c;直接拿到价值6万的股票。作用可以说是相当…