2024年2月

分享是最有效的学习方式。

博客:
https://blog.ktdaddy.com/

故事

地铁上,小帅无力地倚靠着杆子,脑子里尽是刚才面试官的夺命连环问,“用过TheadLocal么?ThreadLocal是如何解决共享变量访问的安全性的呢?你觉得啥场景下会用到TheadLocal? 我们在日常用ThreadLocal的时候需要注意什么?ThreadLocal在高并发场景下会造成内存泄漏吗?为什么?如何避免?......”

这些问题,如同阴影一般,在小帅的脑海里挥之不去。

是的,他万万没想到,自诩“多线程小能手”的他栽在了ThreadLocal上。

这是小帅苦投了半个月简历之后才拿到的面试机会,然而又丧失了。当下行情实在是卷到了极点。

都两个月了,面试机会少,居然还每次都被问翻,这样下去真要回老家另谋出路了,小帅内心五味成杂......

小伙伴们,试问一下,如果是你,面对上述的问题,你能否对答如流呢?

概要

既然被问到了,那么作为事后诸葛的老猫就和大家一起来接面试官的招吧。

我们将从以下点来全面剖析一下ThreadLocal。

概览

基本篇

什么是ThreadLocal?

ThreadLocal英文翻译过来就是:线程本地量,它其实是一种线程的隔离机制,保障了多线程环境下对于共享变量访问的安全性。

看到上面的定义之后,那么问题就来了,ThreadLocal是如何解决共享变量访问的安全性的呢?

其实ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。由于副本都归属于各自的线程,所以就不存在多线程共享的问题了。

便于理解,我们看一下下图。

结构图

至于上述图中提及的threadLocals(ThreadLocalMap),我们后文看源代码的时候再继续来看。大家心中暂时有个概念。

既然都是保证线程访问的安全性,那么和Synchronized区别是什么呢?

在上面聊到共享变量访问安全性的问题上,其实大家还会很容易想起另外一个关键字Synchronized。聊聊区别吧,整理了一张图,看起来可能会更加直观一些,如下。

对比

通过上图,我们发现ThreadLocal其实是一种线程隔离机制。Synchronized则是一种基于Happens-Before规则里的监视器锁规则从而保证同一个时刻只有一个线程能够对共享变量进行更新。

Synchronized加锁会带来性能上的下降。ThreadLocal采用了空间换时间的设计思想,也就是说每个线程里面都有一个专门的容器来存储共享变量的副本信息,然后每个线程只对自己的变量副本做相对应的更新操作,这样避免了多线程锁竞争的开销。

ThreadLocal的使用

上面说了这么多,咱们来使用一下。就拿SimpleDateFormat来做个例子。当然也会有一道这样的面试题,SimpleDateFormat是否是线程安全的?在阿里Java开发规约中,有强制性的提到SimpleDateFormat 是线程不安全的类。其实主要的原因是由于多线程操作SimpleDateFormat中的Calendar对象引用,然后出现脏读导致的。

踩坑代码:

/**
 * @author 公众号:程序员老猫
 * @date 2024/2/1 22:58
 */
public class DateFormatTest {
    private static final SimpleDateFormat simpleDateFormat =
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String dateString) {
        Date date = null;
        try {
            date = simpleDateFormat.parse(dateString);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(20);

        for (int i = 0; i < 20; i++) {
            executorService.execute(()->{
                System.out.println(parse("2024-02-01 23:34:30"));
            });
        }
        executorService.shutdown();
    }
}

上述咱们通过线程池的方式针对SimpleDateFormat进行了测试(如果大家需要深入了解一下线程池的相关原理,可以戳“
线程池
”)。其输出结果如下。

日期

我们可以看到刚开始好好的,后面就异常了。

我们通过ThreadLocal的方式将其优化一下。代码如下:

/**
 * @author 公众号:程序员老猫
 * @date 2024/2/1 22:58
 */
public class DateFormatTest {

    private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static Date parse(String dateString) {
        Date date = null;
        try {
            date = dateFormatThreadLocal.get().parse(dateString);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 20; i++) {
            executorService.execute(()->{
                System.out.println(parse("2024-02-01 23:34:30"));
            });
        }
        executorService.shutdown();
    }
}

运行了一下,完全正常了。

Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024
Thu Feb 01 23:34:30 CST 2024

TheadLocal使用场景

那么我们什么时候会用到ThreadLocal呢?

  1. 上面针对SimpleDateFormat的封装也算是一个吧。

  2. 用来替代参数链传递:在编写API接口时,可以将需要传递的参数放入ThreadLocal中,从而不需要在每个调用的方法上都显式地传递这些参数。这种方法虽然不如将参数封装为对象传递来得常见,但在某些情况下可以简化代码结构。

  3. 数据库连接和会话管理:在某些应用中,如Web应用程序,ThreadLocal可以用来保持对数据库连接或会话的管理,以简化并发控制并提高性能。例如,可以使用ThreadLocal来维护一个连接池,使得每个请求都能共享相同的连接,而不是每次都需要重新建立连接。

  4. 全局存储信息:例如在前后端分离的应用中,ThreadLocal可以用来在服务端维护用户的上下文信息或者一些配置信息,而不需要通过HTTP请求携带大量的用户信息。这样做可以在不改变原有架构的情况下,提供更好的用户体验。

如果大家还能想到其他使用的场景也欢迎留言。

升华篇

ThreadLocal原理

上述其实咱们聊的相对而言还是比较浅的。那么接下来,咱们丰富一下之前提到的结构图,从源代码侧深度剖一下ThreadLocal吧。

结构图

对应上述图中,解释一下。

  1. 图中有两个线程Thread1以及Thread2。
  2. Thread类中有一个叫做threadLocals的成员变量,它是ThreadLocal.ThreadLocalMap类型的。
  3. ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值。

对应的我们看一下Thread的源代码,如下:

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}

在源码中threadLocals的初始值为Null。

抽丝剥茧,咱们继续看一下ThreadLocalMap在调用构造函数进行初始化的源代码:


static class ThreadLocalMap {
        
        private static final int INITIAL_CAPACITY = 16; //初始化容量
        private Entry[] table; //ThreadLocalMap数据真正存储在table中
        private int size = 0; //ThreadLocalMap条数
        private int threshold; // 默认为0,达到这个大小,则扩容
        //类Entry的实现
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //构造函数
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY]; //初始化table数组,INITIAL_CAPACITY默认值为16
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //key和16取得哈希值
            table[i] = new Entry(firstKey, firstValue);//创建节点,设置key-value
            size = 1;
            setThreshold(INITIAL_CAPACITY); //设置扩容阈值
        }
    }

在源码中涉及比较核心的还有set,get以及remove方法。我们依次来看一下:

set方法如下:

 public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程t
        ThreadLocalMap map = getMap(t);  //根据当前线程获取到ThreadLocalMap
        if (map != null)  //如果获取的ThreadLocalMap对象不为空
            map.set(this, value); //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //创建一个新的ThreadLocalMap
    }
    
     ThreadLocalMap getMap(Thread t) {
       return t.threadLocals; //返回Thread对象的ThreadLocalMap属性
    }

    void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数
        t.threadLocals = new ThreadLocalMap(this, firstValue); //this表示当前类ThreadLocal
    }

get方法如下:

    public T get() {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //3、如果map数据不为空,
        if (map != null) {
            //3.1、获取threalLocalMap中存储的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
        return setInitialValue();
    }
 
 
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

remove方法:

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

那么为什么需要remove方法呢?其实这里会涉及到内存泄漏的问题了。后面咱们细看。

对照着上述的结构图以及源码,如果面试官问ThreadLocal原理的时候,相信大家应该可以说出个所以然来。

  1. Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
  2. ThreadLocalMap方法内部维护者Entry数组,其中key是ThreadLocal本身,而value则为其泛型值。
  3. 并发场景下,每个线程都会存储当前变量副本到自己的ThreadLocalMap中,后续这个线程对于共享变量的操作,都是从TheadLocalMap里进行变更,不会影响全局共享变量的值。

高并发场景下ThreadLocal会造成内存泄漏吗?什么原因导致?如何避免?

造成内存泄漏的原因

这个问题其实还是得从ThreadLocal底层源码的实现去看。高并发场景下,如果对ThreadLocal处理得当的话其实就不会造成呢村泄漏。我们看下面这样一组源代码片段:

static class ThreadLocalMap {
        ...
        //类Entry的实现
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
       ...
    }

上文中其实我们已经知道Entry中以key和value的形式存储,key是ThreadLocal本身,上面代码中我们看到entry进行key设置的时候用的是super(k)。那就意味着调用的父类的方法去设置了key,我们再看一下父类是什么,父类其实是WeakReference。关于WeakReference底层的实现,大家有兴趣可以展开去看看源代码,老猫在这里直接说结果。

WeakReference 如字面意思,弱引用,当一个对象仅仅被weak reference(弱引用)指向, 而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。

关于这些引用的强弱,稍微聊一下,这里其实涉及到jvm的回收机制。在JDK1.2之后,java对引用的概念其实做了扩充的,分为强引用,软引用,弱引用,虚引用。

强引用:其实就是咱们一般用“=”的赋值行为,如 Student s = new Student(),只要强引用还在,对象就不会被回收。

软引用:不是必须存活的对象,jvm在内存不够的情况下即将内存溢出前会对其进行回收。例如缓存。

弱引用:非必须存活的对象,引用关系比软引用还弱,无论内存够还是不够,下次的GC一定会被回收。

虚引用:别名幽灵引用或者幻影引用。等同于没有引用,唯一的目的是对象被回收的时候会受到系统通知。

明白这些概念之后,咱们再看看上面的源代码,我们就会发现,原来Key其实是弱引用,而里面的value因为是直接赋值行为所以是强引用。

如下图:

jvm存储

图中我们可以看到由于threadLocal对象是弱引用,如果外部没有强引用指向的话,它就会被GC回收,那么这个时候导致Entry的key就为NULL,如果此时value外部也没有强引用指向的话,那么这个value就永远无法访问了,按道理也该被回收。但是由于entry还在强引用value(看源代码)。那么此时value就无法被回收,此时内存泄漏就出现了。本质原因是因为value成为了一个永远无法被访问也无法被回收的对象。

那肯定有小伙伴会有疑问了,线程本身生命周期不是很短么,如果短时间内被销毁,就不会内存泄漏了,因为只要线程销毁,那么value也会被回收。这话是没错。但是咱们的线程是计算机珍贵资源,为了避免重复创建线程带来开销,系统中我们往往会使用线程池(
线程池传送门
),如果使用线程池的话,那么线程的生命周期就被拉长了,那么就可想而知了。

如何避免

解法如下:

  1. 每次使用完毕之后记得调用一下remove()方法清除数据。
  2. ThreadLocal变量尽量定义成static final类型,避免频繁创建ThreadLocal实例。这样可以保证程序中一直存在ThreadLocal强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问Entry的value值,从而进行清除。

不过话说出来,其实ThreadLocal内部也做了优化的。在set()的时候也会采样清理,扩容的时候也会检查(这里希望大家自己深入看一下源代码),在get()的时候,如果没有直接命中或者向后环形查找的时候也会进行清理。但是为了系统的稳健万无一失,所以大家尽量还是将上面的两个注意点在写代码的时候注意下。

总结

面试的时候大家总会去背一些八股文,但是这种也只是临时应付面试官而已,真正的懂其中的原理才是硬道理。无论咋问,万变不离核心原理。当然这些核心原理在我们的日常编码中也会给我们带来很大的帮助,用法很简单,翻车了如何处理,那还不是得知其所以然么,伙伴们,你们觉得呢?

如果在做应用的时需要同时使用使用两块板子分别做主从机或者使用一块板子做单独的从机;

这是我们需要按下某个按键或者发送某条指令主机或者从机主动断开与对方的连接且设备不需要复位;

主机端我们可以调用这样一个函数:

GAPRole_TerminateLink(centralConnHandle)在程序中  centralConnHandle = pEvent->linkCmpl.connectionHandle;

注意:不要传入这个handle
centralCharHdl
这个是服务的句柄;我们需要传入的是连接句柄
centralConnHandle

从机端我们可以调用同样的函数:

GAPRole_TerminateLink(peripheralConnList.connHandle);

接下来举个简单的使用例子:

在从机中使用,当通道一的write服务发数据过来后从机主动断开连接

我们可以看到断连的原因是0x16即本地断开(从机主动断连)。

POS软件是什么?你好意思吗,还在用老掉牙的Winform。

门店被淘汰的POS机

销售终端——POS(point of sale)是一种多功能终端,把它安装在信用卡的特约商户和受理网点中与计算机联成网络,就能实现电子资金自动转账,它具有支持消费、预授权、余额查询和转账等功能,使用起来安全、快捷、可靠。

前言

万事俱备只欠东风------一个USB摄像头和一个经过改造的人脸识别程序。

下载地址:

GitHub - ViewFaceCore/ViewFaceCore: C# 超简单的离线人脸识别库。( 基于 SeetaFace6 )

开始干活,动手改造。

  1. 程序要支持无人值守,程序启动时自动打开摄像头。超过设定的时间无移动鼠标和敲击键盘,程序自动关闭摄像头,进入“休眠”
  2. 识别人脸成功后记录当前时间作为考勤记录
  3. 人脸信息放在服务器端,桌面程序和服务器端同步人脸信息
  4. 关于不排班实现考勤的思考
  5. 取消消息弹窗来和用户交互。使用能自动关闭的消息弹窗

1.检测超过设定的时间无移动鼠标和敲击键盘,判断是否无人使用。

 #region 获取键盘和鼠标没有操作的时间 [StructLayout(LayoutKind.Sequential)]structLASTINPUTINFO
{
[MarshalAs(UnmanagedType.U4)]
public intcbSize;
[MarshalAs(UnmanagedType.U4)]
public uintdwTime;
}
[DllImport(
"user32.dll")]private static extern bool GetLastInputInfo(refLASTINPUTINFO plii);/// <summary> ///获取键盘和鼠标没有操作的时间/// </summary> /// <returns></returns> private static longGetLastInputTime()
{
LASTINPUTINFO vLastInputInfo
= newLASTINPUTINFO();
vLastInputInfo.cbSize
=Marshal.SizeOf(vLastInputInfo);if (!GetLastInputInfo(refvLastInputInfo))return 0;else return Environment.TickCount - (long)vLastInputInfo.dwTime;//单位ms }#endregion

2.把人脸识别这个用途改成考勤

        /// <summary>
        ///窗体加载时/// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form_Load(objectsender, EventArgs e)
{
#region 窗体初始化WindowState=FormWindowState.Maximized;//隐藏摄像头画面控件 VideoPlayer.Visible = false;//初始化VideoDevices 检测摄像头ToolStripMenuItem_Click(null, null);//默认禁用拍照按钮 FormHelper.SetControlStatus(this.ButtonSave, false);
Text
= "WPOS人脸识别&考勤";#endregion #region TTS try{
VoiceUtilHelper
= newSpVoiceUtil();
StartVoiceTaskJob();
}
catch(Exception ex)
{
byte[] zipfile = (byte[])Properties.Resources.ResourceManager.GetObject("TTSrepair");
System.IO.File.WriteAllBytes(
"TTSrepair.zip", zipfile);
Program.UnZip(
"TTSrepair.zip", "", "", true);#region 语音引擎修复安装 try{
MessageBox.Show(
"初始化语音引擎出错,错误描述:" + ex.Message + Environment.NewLine + "正在运行语音引擎安装程序,请点下一步执行安装!", Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);string physicalRoot =AppDomain.CurrentDomain.BaseDirectory;string info1 = Program.Execute("TTSrepair.exe", 3);
}
finally{
System.IO.File.Delete(
"TTSrepair.zip");
Application.Restart();
}
#endregion}#endregion #region 自动打开摄像头Thread thread= new Thread(() =>{
Thread.Sleep(
5000);
sc.Post(SystemInit,
this);
});
thread.Start();
#endregion #region Sync face dataThread SyncThread= new Thread(() =>{while (IsWorkEnd == false)
{
var theEmployeeList = SyncServerEmployeeInfomation().Where(r => r.EmpFacialFeature != null).ToList();if (theEmployeeList != null && theEmployeeList.Count > 0)
{
foreach (var emp intheEmployeeList)
{
poolExt.Post(emp);
}
}
Thread.Sleep(
5000);
}
});
SyncThread.Start();
#endregion #region 自动关闭摄像头线程Thread CameraCheckThread= new Thread(() =>{while (IsWorkEnd == false)
{
if(IsNeedAutoCheck)
{
long Auto_close_camera_interval = long.Parse(string.IsNullOrEmpty(config.AppSettings.Settings["Auto_close_camera_interval"].Value) ? "60000" : config.AppSettings.Settings["Auto_close_camera_interval"].Value);long ts =GetLastInputTime();if (ts >Auto_close_camera_interval)
{
IsNeedAutoCheck
= false;
sc.Post(CheckCameraStatus,
this);
}
}
Thread.Sleep(
1000);
}
});
CameraCheckThread.Start();
btnSleep.Enabled
= true;
btnStopSleep.Enabled
= true;#endregion}

修改识别人脸后做的事情:

 /// <summary>
 ///持续检测一次人脸,直到停止。/// </summary>
 /// <param name="token">取消标记</param>
 private async voidStartDetector(CancellationToken token)
{
List
<double> fpsList = new List<double>();double fps = 0;
Stopwatch stopwatchFPS
= newStopwatch();
Stopwatch stopwatch
= newStopwatch();
isDetecting
= true;try{if (VideoPlayer == null)
{
return;
}
while (VideoPlayer.IsRunning && !token.IsCancellationRequested)
{
try{if(CheckBoxFPS.Checked)
{
stopwatch.Restart();
if (!stopwatchFPS.IsRunning)
{ stopwatchFPS.Start(); }
}
Bitmap bitmap
= VideoPlayer.GetCurrentVideoFrame(); //获取摄像头画面 if (bitmap == null)
{
await Task.Delay(10, token);
FormHelper.SetPictureBoxImage(FacePictureBox, bitmap);
continue;
}
if (!CheckBoxDetect.Checked)
{
await Task.Delay(1000 / 60, token);
FormHelper.SetPictureBoxImage(FacePictureBox, bitmap);
continue;
}
List
<Models.FaceInfo> faceInfos = new List<Models.FaceInfo>();using (FaceImage faceImage =bitmap.ToFaceImage())
{
var infos = await faceFactory.Get<FaceTracker>().TrackAsync(faceImage);for (int i = 0; i < infos.Length; i++)
{
Models.FaceInfo faceInfo
= newModels.FaceInfo
{
Pid
=infos[i].Pid,
Location
=infos[i].Location
};
if (CheckBoxFaceMask.Checked ||CheckBoxFaceProperty.Checked)
{
Model.FaceInfo info
=infos[i].ToFaceInfo();if(CheckBoxFaceMask.Checked)
{
var maskStatus = await faceFactory.Get<MaskDetector>().PlotMaskAsync(faceImage, info);
faceInfo.HasMask
=maskStatus.Masked;
}
if(CheckBoxFaceProperty.Checked)
{
FaceRecognizer faceRecognizer
= null;if(faceInfo.HasMask)
{
faceRecognizer
=faceFactory.GetFaceRecognizerWithMask();
}
else{
faceRecognizer
= faceFactory.Get<FaceRecognizer>();
}
var points = await faceFactory.Get<FaceLandmarker>().MarkAsync(faceImage, info);float[] extractData = awaitfaceRecognizer.ExtractAsync(faceImage, points);
UserInfo userInfo
=CacheManager.Instance.Get(faceRecognizer, extractData);if (userInfo != null)
{
faceInfo.Name
=userInfo.Name;
faceInfo.Age
=userInfo.Age;switch(userInfo.Gender)
{
caseGenderEnum.Male:
faceInfo.Gender
=Gender.Male;break;caseGenderEnum.Female:
faceInfo.Gender
=Gender.Female;break;caseGenderEnum.Unknown:
faceInfo.Gender
=Gender.Unknown;break;
}
pool.Post(userInfo);
}
else{
faceInfo.Age
= await faceFactory.Get<AgePredictor>().PredictAgeAsync(faceImage, points);
faceInfo.Gender
= await faceFactory.Get<GenderPredictor>().PredictGenderAsync(faceImage, points);
}
}
}
faceInfos.Add(faceInfo);
}
}
using (Graphics g =Graphics.FromImage(bitmap))
{
#region 绘制当前时间StringFormat format= newStringFormat();
format.Alignment
=StringAlignment.Center;
format.LineAlignment
=StringAlignment.Center;
g.DrawString($
"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}", new Font("微软雅黑", 32), Brushes.Green, new Rectangle(0, 0, Width - 32, 188), format);#endregion //如果有人脸,在 bitmap 上绘制出人脸的位置信息 if(faceInfos.Any())
{
g.DrawRectangles(
new Pen(Color.Red, 4), faceInfos.Select(p =>p.Rectangle).ToArray());if(CheckBoxDetect.Checked)
{
for (int i = 0; i < faceInfos.Count; i++)
{
StringBuilder builder
= newStringBuilder();if(CheckBoxFaceProperty.Checked)
{
if (!string.IsNullOrEmpty(faceInfos[i].Name))
{
builder.Append(faceInfos[i].Name);
}
}
if (builder.Length > 0)
g.DrawString(builder.ToString(),
new Font("微软雅黑", 32), Brushes.Green, new PointF(faceInfos[i].Location.X + faceInfos[i].Location.Width + 24, faceInfos[i].Location.Y));
}
}
}
if(CheckBoxFPS.Checked)
{
stopwatch.Stop();
if (numericUpDownFPSTime.Value > 0)
{
fpsList.Add(1000f
/stopwatch.ElapsedMilliseconds);if (stopwatchFPS.ElapsedMilliseconds >=numericUpDownFPSTime.Value)
{
fps
=fpsList.Average();
fpsList.Clear();
stopwatchFPS.Reset();
}
}
else{
fps
= 1000f /stopwatch.ElapsedMilliseconds;
}
g.DrawString($
"{fps:#.#} FPS", new Font("微软雅黑", 24), Brushes.Green, new Point(10, 10));
}
}
FormHelper.SetPictureBoxImage(FacePictureBox, bitmap);
}
catch(TaskCanceledException)
{
break;
}
catch{ }
}
}
finally{
isDetecting
= false;
}
}
#endregion

3.把人脸信息放在服务器端,桌面程序和服务器端同步人脸信息

 /// <summary>
 ///同步人员信息/// </summary>
 private List<PlatEmployeeDto>SyncServerEmployeeInfomation()
{
List
<PlatEmployeeDto> list = new List<PlatEmployeeDto>();string url = $"{config.AppSettings.Settings["Platform"].Value}/business/employeemgr/POSSyncEmployeeInfomation";try{string rs =Program.HttpGetRequest(url);if (!string.IsNullOrEmpty(rs) && JObject.Parse(rs).Value<int>("code").Equals(200))
{
JObject jo
=JObject.Parse(rs);
list
= JsonConvert.DeserializeObject<List<PlatEmployeeDto>>(jo["data"].ToString());
}
}
catch(Exception ex)
{
if (ex.Message.Contains("无法连接到远程服务器"))
{
Thread.Sleep(
100);
ViewFaceCore.Controls.MessageTip.ShowError(
"无法连接到远程服务器" + Environment.NewLine + "Unable to connect to remote server", 300);
}
}
returnlist;
}
        private void btnSave_Click(objectsender, EventArgs e)
{
try{
SetUIStatus(
false);
UserInfo userInfo
=BuildUserInfo();if (userInfo == null)
{
throw new Exception("获取用户基本信息失败!");
}
using (DefaultDbContext db = newDefaultDbContext())
{
db.UserInfo.Add(userInfo);
if (db.SaveChanges() > 0)
{
CacheManager.Instance.Refesh();
this.Close();
_
= Task.Run(() =>{//确保关闭后弹窗 Thread.Sleep(100);try{#region Post Data string url = $"{config.AppSettings.Settings["Platform"].Value}/business/employeemgr/PosNewEmployeeRegister";
PlatEmployeeDto dto
= newPlatEmployeeDto();
dto.KeyId
=Guid.NewGuid().ToString();
dto.EmpNo
=userInfo.EmpNo;
dto.EmpName
=userInfo.Name;
dto.EmpSex
= (int)userInfo.Gender.ToInt64();
dto.Mobile
=userInfo.Phone;
dto.PositionValue
=userInfo.JobPosition.ToString();
dto.EmpFacialFeature
=_globalUserInfo.Extract;
dto.EmpMainPhoto
=_globalUserInfo.Image;
dto.CreateBy
= "Client";
dto.CreateTime
=DateTime.Now;
dto.IsAdmin
= "N";
dto.Status
= 0;
dto.FirstPositionLabel
=cbxposition.Text;string jsondata =JsonConvert.SerializeObject(dto);string st =Program.PostJsonData(url, jsondata);#endregion if (!string.IsNullOrEmpty(st) && st.Contains("200"))
{
//MessageBox.Show("保存用户信息成功!同步到服务器成功,可到其他门店考勤。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); DialogResult =DialogResult.OK;
}
}
catch(Exception ex)
{
MessageBox.Show(
"本地保存用户信息成功!但同步到服务器出错,不能立即到其他门店考勤。" + ex.Message, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
});
}
}
}
catch(Exception ex)
{
MessageBox.Show(ex.Message,
"警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
finally{
SetUIStatus(
false);
}
}

4.关于不排班实现考勤的思考

        /// <summary>
        ///客户端添加attendance考勤明细/// </summary>
        /// <returns></returns>
        [HttpPost("AddAttendanceDetails")]//[ActionPermissionFilter(Permission = "business:erpattendancedetails:add")]
        [Log(Title = "attendance考勤明细", BusinessType =BusinessType.INSERT)]
[AllowAnonymous]
publicIActionResult AddAttendanceDetails([FromBody] AttendanceDetailsDto parm)
{
var modal = parm.Adapt<AttendanceDetails>().ToCreate(HttpContext);if (!string.IsNullOrEmpty(parm.FkStore))
{
int storeId = -1;int.TryParse(parm.FkStore, outstoreId);var store = _MerchantStoreService.GetFirst(s => s.Id ==storeId);if (store == null)returnBadRequest();
modal.FkStore
=store.KeyId;
}
else returnBadRequest();if (!_AttendanceDetailsService.Any(r => r.AuditDate == parm.AuditDate && r.EmpNo ==parm.EmpNo))
{
modal.Remark
= "上班&clock in";var response =_AttendanceDetailsService.AddAttendanceDetails(modal);returnSUCCESS(response);
}
else{var list = _AttendanceDetailsService.GetList(r => r.AuditDate == parm.AuditDate && r.EmpNo ==parm.EmpNo);var time1 = list.Max(r =>r.AttendanceDatetime);if (time1 != null)
{
var ts = DateTime.Now -DateTime.Parse(time1);if (ts.TotalMinutes < 61)
{
returnOk();
}
else{
modal.Remark
= "下班&clock out";var response =_AttendanceDetailsService.AddAttendanceDetails(modal);returnSUCCESS(response);
}
}
else{returnBadRequest();
}
}
}

5.取消消息弹窗来和用户交互。使用能自动关闭的消息弹窗

这个需要感谢以前在园子里的
一位
博主的分享他写的控件名字叫"LayeredWindow",对外暴露的类叫“MessageTip”,不好意思已忘记作者。

如果你仔细阅读代码还会发现集成了TTS。反正做得有点像无人值守的一些商业机器。好了,收工了。今天只上半天班。

写这个部署体验教程时候,我一直思考作为一个中年程序员,游戏爱好者来说,我到底应该写什么样的内容?以阿里云这种快捷部署游戏服务器的模式,对我们有哪些影响,于是我先起草了一个思维导图,通过思维导图大家可以快速了解下我的创作思路。
官方教程详戳:
不需要懂技术,1分钟幻兽帕鲁服务器搭建教程

幻兽帕鲁为什么这么火?

游戏概述:《幻兽帕鲁》的魅力所在

作为一名资深技术工作者和游戏玩家,我对于《幻兽帕鲁》这款游戏的深入理解并不仅仅停留在表面的游玩体验上。这款游戏独特地融合了宠物养成、生存建造以及多人游戏元素,创造出一个开放世界的生存制作游戏环境,使其成为市场上独树一帜的作品。

在《幻兽帕鲁》的世界里,玩家不仅可以享受到捕捉和培育名为“帕鲁”的神奇生物所带来的乐趣,还能通过它们的独特能力参与到战斗、农活、建造等多样化的游戏玩法中去。这种多元化的游戏设计理念不仅丰富了游戏内容,也极大地提升了玩家的游戏体验和参与度。

玩家互动与生态系统:构建与帕鲁的深度关系

《幻兽帕鲁》不仅仅是一场单向的冒险旅程,它更是一种与游戏内生物——帕鲁共同生活、共同成长的体验。玩家可以通过收集和训练帕鲁来参与战斗、建造和农业生产,甚至可以在帕鲁的帮助下在工厂工作。这种互动不仅限于生产活动,帕鲁还能繁殖、参与战斗,甚至在一些情况下被玩家售卖或食用。这种游戏设计让玩家与帕鲁之间建立起了一种复杂而深刻的关系,同时也营造出了一个生态系统,玩家的每一个决策都会影响到这个世界的平衡。

多人模式:共享冒险与创造

从我的技术背景来看,《幻兽帕鲁》在多人游戏方面的设计尤其值得称赞。游戏支持单人游戏模式和多人联机模式,后者既可以通过Steam或虚拟局域网进行4人本地联机,也支持加入最多32人的官方服务器进行游戏。这种设计不仅增加了游戏的可玩性和互动性,还为玩家提供了一个共享冒险和创造的平台。

在多人模式下,玩家可以与朋友一起分工合作,共同建设家园,或是组队去挑战困难的BOSS。这种协作和共享的体验,正是《幻兽帕鲁》作为一个开放世界生存制作游戏能够吸引广大玩家的重要原因之一。

一顿工作餐搭建自己的幻兽帕鲁元宇宙世界

购买合适的云服务器

首先,我们进入阿里云的部署页面:

我们可以从图中看到几个服务器的类型,我们可以根据自己的情况去购买,我就选第一个4核16G内存的服务器,我用了一个新的账号去购买,只需要1个月26元即可,非常爽啊。

接着我们点击一键部署,就达到了这个参数配置页面,我们填写服务器名称,选择区域,了解不同规格服务器的配置情况。

然后我们看到这个参数配置页面,大家可以根据自己对游戏的理解来灵活调整参数,当然后期也可以随时来调整重开游戏,让你成为这个游戏的上帝。

这里我先购买1个月,同时选定部署的服务器为windows,同时我还可以在系统中部署和安装其他的程序,最高效的来利用这个服务器资源。

最终通过上面的配置,价格在这里展示出来,我们点击立即创建即可。

点击立即创建后,就到了这个页面,这里我选定支付宝来支付,点击确定充值,就让你扫码支付。

支付宝完成后,大家可以看到我们的资源就创建了,这里显示出我们部署游戏的服务器资源,其中显示正在部署中,稍作等待即可,大概3-5分钟即可部署完成。

学习云游戏服务器的功能

我们可以直接点进来看下我们服务器的具体参数和相关功能。

其中这里的运维管理非常重要,方便我们以后随时修改游戏参数,包括升级服务端版本,重启,存档等等,方便灵活实时的来管理游戏。


资源中是我们游戏服务器所占用的一些服务器单元情况,包括云服务器,云盘和网络资源相关。

我们也可以在监控里面,随时查看服务器的实时状况,如果压力太大,可以升级配置或者减少游戏人数,包括并发数量。


其他的功能,不是太重要我就不在这里展示了。大家也可以从左侧服务市场,了解下其他相关的可以部署的游戏和工具,你可以可以随时更换不同的应用,合理的利用我们的云服务器资源。

试玩幻兽帕鲁

好了,大概部署完毕后。如果还没有安装游戏客户端的朋友,可以进入STEAM官网,包括注册用户,登录用户,下载客户端。

我们可以在商店,或者库去搜索PALWORLD,进行对应版本的购买。

购买,安装后,我们就可以点击启动游戏。如下是游戏的主页面。我们选择第二项多人联机的模式。

跳转到如下图中,我们在最下面填入前面购买的阿里云的服务器资源。

填写后,进行链接,这样就正式进入了游戏,我们就可以在游戏中配置角色,正式进入友好的玩耍状态了,有点小兴奋吗,这是你自己的幻兽帕鲁的元宇宙了哦,而且你可以随时调整游戏的参数。

云服务器的额外用途

同时,如果大家觉得服务器资源闲置时候比较多,我们可以直接从windows的远程桌面,输入购买服务器的IP地址和用户名密码,就可以像正常使用电脑的方式进入我们的服务器了。

进入服务器的Windows系统后,我们可以从C盘的Program Files中看到PalServer文件夹,这个就是我们部署的游戏服务端程序了。

再来检查下我们服务器的配置,没问题,Very Good。

回想起来,部署一个游戏服务器曾是一项令人头疼的任务。想象一下,首先得购买一堆硬件,然后亲自动手安装操作系统,再配置一长串的环境变量,最后还得下载游戏包。整个过程不仅繁琐而且复杂,足以让任何人感到气馁。

但是,随着云计算技术的飞速发展,像阿里云计算这样的服务模式彻底改变了游戏。现在,想要搭建自己的游戏服务器,基本上在15分钟左右就能搞定全部设置——这在过去简直是难以想象的事情。更加令人兴奋的是,如果我们抓住促销优惠期,购买资源的总成本甚至比之前投资于硬件的花费还要便宜。

从部署游戏,驱动学习新技术,还能做副业挣钱

技术探索:兴趣驱动学习

身为霍大侠,一个对技术充满热情的中年资深游戏玩家,我通过在云服务器上部署《幻兽帕鲁》这款游戏,将我的兴趣转化为学习新技术的动力。这不仅仅是游戏的部署,更是一个学习的旅程。

而且随着现在AIGC的发展,包括一些RPA自动化的技术,我也开始尝试做一些开放世界游戏的自动化的东西,包括NPC的AI智能化等等,逐步将这些技术应用于游戏中,从而不断优化和创新我的幻兽世界。

我想说的是大家一定要找到自己的兴趣,然后通过兴趣驱动我们的学习,这样不仅满足了兴趣爱好,还能利用过程中学到的本领来升职加薪,何乐而不为。

创意实践:内容创作与分享

在构建和部署《幻兽帕鲁》服务器的过程中,我发现了另一种创造价值的方式——内容创作。目前我已经从各种短视频平台发现很多博主开始围绕一些感兴趣的游戏进行视频创作,包括攻略,技巧等等,还有直播讲解等方式来构建自己的圈子,找到志同道合的朋友一起交流,让生活更有趣。

经济增值:爱好变副业

随着创建游戏服务器的技术越来越容易,我发现很多人已经开始用这种方式来变现,构建社群,开辟群友专有的游戏模式,然后收取每月的固定费用。通过技术能力和创造性内容的结合,为社区成员提供了增值服务,如定制的游戏模块和专属的幻兽等等。

同时,目前最流行的一种变现和副业方式就是内容变现,通过制作视频,直播增加内容传播,积累粉丝,然后通过打赏,平台激励和广告等策略来获取收益。做的好的已经财务自由,中部的也已经获取到丰厚的回报。

探索未来个人元宇宙

创造个性化元宇宙:AI的革新应用

在《幻兽帕鲁》的游戏世界中,我,霍大侠,不仅仅是一个玩家,也是一个创世者。借助AI技术,我能够赋予游戏内的NPC(非玩家角色)以高度智能,让它们拥有近乎真实的交互能力和独立决策的思维。

这种技术的进步意味着每个玩家都有能力创建一个真正属于自己的元宇宙,其中的居民——AI驱动的NPC,可以根据玩家的行为和喜好进行学习和适应,从而提供一个更加沉浸式和个性化的游戏体验。这种自我演化的游戏世界,将极大地扩展个人在虚拟世界中的创作和社交边界。

融合现实与虚拟:增强与虚拟现实技术

随着AR和VR技术的发展,比如最近苹果的Vision Pro 将近3万的价格,一出场便售罄。我相信在《幻兽帕鲁》这类开放式的游戏基础上,可以打造一个无缝结合现实和虚拟的体验。在这个扩展的游戏世界里,玩家通过佩戴头戴设备或使用增强现实应用,就能直接与幻兽并肩作战,或在虚拟的农田中挥汗如雨。

这样的技术不仅仅使得游戏体验更加真实和生动,还能让玩家在自己创建的元宇宙中,经历一场场电影般的史诗冒险。未来,这种技术可能会变得普遍,届时每个人的生活都将与自己构建的虚拟世界紧密相连。

驱动未来发展:每个人的元宇宙探索者

当今世界的每一个角落,都隐藏着未来世界的种子。我所掌握的游戏开发技术和对AI的理解,使我成为了推动这些种子成长的力量之一。

通过自己的创意和技术,我期望未来我可以帮助玩家构建起属于自己的元宇宙,让他们在这个自定义的世界中自由探索、创造和交流。未来,随着技术的不断进步,我们距离电影中所描述的那样的全感官虚拟现实世界,可能仅仅是一步之遥。

总结

总结一下,用游戏的方式探索兴趣爱好并与前沿技术相结合的方式,驱动我们自我学习提升,从而升值加薪,走向自由,朝着构建我们个人的元宇宙世界而努力,更重要的是让我们更快乐的生活。

中台Admin(Admin.Core)

中台Admin(Admin.Core)是前后端分离权限管理系统,前端 UI 基于Vue3开发,后端 Api 基于.NET 8.0开发。支持多租户、接口权限、数据权限、动态 Api、任务调度、OSS 文件上传、滑块拼图验证、国内外主流数据库自由切换和动态高级查询。集成统一认证授权、事件总线、数据验证、分布式雪花Id、分布式缓存、分布式事务、IP 限流、性能分析、集成测试、健康检查、接口文档等。

DncZeus

DncZeus是一个基于 .NET 7 + Vue.js 的前后端分离的通用后台管理系统框架。后端使用.NET 7 + Entity Framework Core 构建,UI 则是目前流行的基于 Vue.js 的 iView。项目实现了前后端的动态权限管理和控制以及基于 JWT 的用户令牌认证机制,让前后端的交互更流畅。

注意:DncZeus并不是一个完整的业务系统,但她提供完成业务系统的绝大多数开发场景,让每一位.NET 开发者都能基于DncZeus快速开发出交互、体验以及功能极佳的.NET 7 单页应用程序(SPA)。

WalkingTec.Mvvm框架(简称WTM)

WalkingTec.Mvvm框架(简称WTM)是基于.net core的快速开发框架。支持Layui(前后端不分离), React(前后端分离),VUE(前后端分离),内置代码生成器,最大程度的提高开发效率,是一款高效开发的利器。

WTM框架的前后端分离模式同样可以使用代码生成器同时生成前台和后台的代码,极大的降低了前后端人员的沟通成本,从本质上提升了开发效率,让“分离”不再复杂和昂贵。

OpenAuth.Net

.Net权限管理及快速开发框架、最好用的权限工作流系统。源于Martin Fowler企业级应用开发思想及最新技术组合(SqlSugar、EF、Quartz、AutoFac、WebAPI、Swagger、Mock、NUnit、Vue2/3、Element-ui/plus、IdentityServer等)。核心模块包括:角色授权、代码生成、智能打印、表单设计、工作流、定时任务等。架构易扩展,是中小企业的首选。

Blog.Core

一个开箱即用的企业级前后端分离【.NET Core6.0 Api + Vue 2.x + RBAC】权限框架(提高生产效率,快速开发就选它)。

优秀项目和框架精选


以上项目都已收录到C#/.NET/.NET Core优秀项目和框架精选中,关注优秀项目和框架精选能让你及时了解C#、.NET和.NET Core领域的最新动态和最佳实践,提高开发工作效率和质量。坑已挖,欢迎大家踊跃提交PR推荐或自荐(让优秀的项目和框架不被埋没