2024年1月

不止八股:阿里内部语雀一些有趣的并发编程笔试题1——一半容量才可以出队的阻塞队列

0丶引入

笔者社招一年半经验跳槽加入阿里约1年时间,无意间发现一些阿里语雀上的一些面试题题库,出于学习目的在此进行记录。

  • 这一篇主要写一些有趣的笔试题(非leetcode),这些有的考验并发编程,有的考验设计能力。
  • 笔者不是什么技术大牛,此处笔试题充满主观思考,并不一定是满分答案,欢迎评论区一起探讨。
  • 不止八股:面试题之外,笔者会更多的思考下底层原理,不只是简单的背诵。

下面这个题目也是笔者面试阿里笔试做过的一道笔试题,现在回想自己那时候写的也是一坨

1 题目

通过List实现一个阻塞队列类,该队列有最大长度限制,以下是该队列的一些特性

  • take方法:获取队列中队头元素,当队列中元素数量超过一半时,则可以出队,否则阻塞当前线程
  • offer(E element)方法:插入元素到队尾,当队列大小已经超过最大限制时,则阻塞当前线程
  • size()返回当前队列大小

2 笔者的题解

感觉这个队列现实中没啥实际用途,为啥超过一半才能出队
这一题主要考察候选人对并发编程的理解,可是笔者工作也没用到这些知识呀doge

在juc相关笔记中,我们学习了Condition和Object#wait notify的原理和基本使用,也学习了juc中常见的阻塞队列原理,这里可用使用等待唤醒进行实现。

2.1 基于Object等待唤醒的简单版本

2.1.1代码

import java.util.List;

public class HalfTakeAvailableBlockingQueueV1<E> {

    private List<E> delegateList;

    private int maxSize;

    private final Object lock;

    public HalfTakeAvailableBlockingQueueV1(List<E> delegateList, int maxSize) {
        this.delegateList = delegateList;
        this.maxSize = maxSize;
        lock = new Object();
    }

    /***
     * take方法:获取队列中队头元素,当队列中元素数量超过一半时,则可以出队,否则阻塞当前线程
     * @return 队列头部元素
     * @throws InterruptedException
     */
    public E take() throws InterruptedException {
        synchronized (lock) {
            // 使用while 避免虚假唤醒
            while (size() <= maxSize / 2) {
                lock.wait();
            }

            // 这里要使用remove,得删去
            E e = delegateList.remove(0);
            // notifyAll 不能和上面的remove换位置
            lock.notifyAll();
            return e;
        }
    }

    /**
     * offer(E element)方法:插入元素到队尾,当队列大小已经超过最大限制时,则阻塞当前线程
     *
     * @param e e
     */
    public void offer(E e) throws InterruptedException {
        synchronized (lock) {
            while (size() > maxSize) {
                lock.wait();
            }
            
            delegateList.add(e);
            // notifyAll 和上面的不能调换位置
            lock.notifyAll();
        }
    }

    /**
     * 需要加锁,否则有线程可见性问题
     * @return
     */
    public int size(){
        synchronized (lock){
            return delegateList.size();
        }
    }

}

2.1.2细节

虽然这个版本似乎很简单,但是还是存在一些细节。

  1. 命名

    HalfTakeAvailable是不是很见名知意,一半的容量才能获取。

  2. 使用while而不是if

    while (条件)
    为什么使用的while,而不是使用if?

    想想一种case,线程A和线程B阻塞于offer,线程C获取锁正在执行take,执行完take后进行notifyAll。随后线程A获得锁结束了if进行元素的塞入,此时size大于了maxSize,但是线程A的唤醒导致了线程B继续执行,线程B也结束了if,继续塞入了元素。

    这种情况被称作虚假唤醒

  3. size方法为什么要加锁

    保证线程可见性
    ,本质上size方法就是读没有并发修改的问题,这里加synchronized只是为了保证线程可见性

    当线程释放锁时,它所做的更改会被刷新到主内存中,同样,当线程获取锁时,它会从主内存中读取共享变量的最新值。这意味着通过synchronized 块的同步变量的更新对所有其他线程是可见的

  4. notifyAll不能和数据操作调换位置


    image-20240106115447716

    为什么不能先notifyAll然后再remove昵?

    在涉及等待/通知模式的同步代码中,典型的模式是
    先改变状态或条件(比如执行 remove(0) 移除队列头部元素)

    然后调用 notifyAll() 或 notify() 来唤醒等待该条件的线程

    如果你先调用 notifyAll(),然后再改变状态(执行 remove(0)),可能会出现以下问题:


    • 之前被唤醒的线程可能无法察觉到状态改变: 如果你先发送通知,那些等待的线程可能会在状态实际改变之前被唤醒(比如,某个元素还没从队列中移除)。那些线程可能检查到状态尚未改变(因为 remove(0) 还没执行),然后无意义地再次进入等待状态。

    • 可能会发生竞态条件: 如果一个线程执行完 notifyAll() 并且在 remove(0) 执行之前失去了锁,那么等待的线程将会开始运行。此时,由于元素还未被移除,它们可能看到错误的状态,或者在执行它们各自的操作时与其他线程发生冲突。

    • 降低了效率: 当线程被过早地唤醒,它们可能只是进行一次无效的状态检查然后再次挂起。这不仅浪费了CPU资源,还增加了线程上下文切换的开销。

2.2.3 这个版本的不足

  1. 为什么size需要加锁

    这里加锁不是因为存在并发更新的问题,而是需要保证线程可见性,但是这里的加锁却导致offer和take的阻塞,以及多个size不能并行。

    如何优化——volatile

  2. 一个等待条件的弊端

    想象一种case,当前容量为maxSize - 1,线程AB都在执行offer,线程A执行成功了进行唤醒了线程B,线程B唤醒后自旋while继续wait。

    有什么问题:线程B不应该被唤醒,应该是执行take的线程来唤醒执行offer的线程

    如何解决:多个等待条件,仿照ArrayBlockingQueue

2.2 使用volatile优化size

我们在HalfTakeAvailableBlockingQueueV2中,使用
private volatile int size
来记录当前容量,然后offer和take的时候修改加锁修改size。

然后size方法就不需要加锁了。

image-20240106120858629
image-20240106120921590

2.3 使用Condition实现多等待条件

我们使用juc中的ReentrantLock来解决并发修改问题,ReentrantLock#newCondition可创建多种等待唤醒条件,我们下面的代码创建了两个Condition

  • takeAvailable:如果无法take那么在此Condition上进行等待
  • offerAvailable:如果无法offer那么在此Condition上进行等待

从而实现offer的线程来唤醒阻塞在take上的线程,减少无意义的唤醒,这里也可以看出ReentrantLock相对于synchronized更牛的一点

import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class HalfTakeAvailableBlockingQueueV3<E> {
    private List<E> delegateList;
    private int maxSize;
    private volatile int size;
    private final ReentrantLock lock;

    /**
     * 可拿取条件
     */
    private final Condition takeAvailable;
    /**
     * 可塞入条件
     */
    private final Condition offerAvailable;

    public HalfTakeAvailableBlockingQueueV3(List<E> delegateList, int maxSize) {
        this.delegateList = delegateList;
        this.maxSize = maxSize;
        lock = new ReentrantLock();
        takeAvailable = lock.newCondition();
        offerAvailable = lock.newCondition();
        size = 0;
    }

    /***
     * take方法:获取队列中队头元素,当队列中元素数量超过一半时,则可以出队,否则阻塞当前线程
     * @return 队列头部元素
     * @throws InterruptedException
     */
    public E take() throws InterruptedException {
        lock.lock();
        try {
            // 使用while 避免虚假唤醒
            while (size() <= maxSize / 2) {
                takeAvailable.await();
            }

            // 这里要使用remove,得删去
            E e = delegateList.remove(0);
            size--;
            offerAvailable.signalAll();
            return e;
        } finally {
            lock.unlock();
        }
    }

    /**
     * offer(E element)方法:插入元素到队尾,当队列大小已经超过最大限制时,则阻塞当前线程
     *
     * @param e e
     */
    public void offer(E e) throws InterruptedException {
        lock.lock();
        try {
            while (size() > maxSize) {
                // 无法offer 那么await
                offerAvailable.await();
            }
            delegateList.add(e);
            size++;
            // 可拿去唤醒
            takeAvailable.signalAll();
        } finally {
            lock.unlock();
        }
    }


    public int size() {
        return size;
    }

}

2.3.1继续优化

这里其实还有两个点可优化

  • 如果容量大于等于一半才进行唤醒!

image-20240106122227548

  • 使用signal而不是signalAll

    signal只会唤醒一个消费者线程,线程这里我们也只塞了一个元素,不需要signalAll从而减少无意义的锁竞争,take部分同理

public void offer(E e) throws InterruptedException {
    lock.lock();
    try {
        while (size() > maxSize) {
            // 无法offer 那么await
            offerAvailable.await();
        }
        delegateList.add(e);
        size++;
        if (size>=maxSize/2){
            // 可拿唤醒
            takeAvailable.signal();
        }
    } finally {
        lock.unlock();
    }
}

3.不止八股

3.1 synchronized 原理

ObjectMonitor的结构:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //锁计数器
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

当线程想要进入同步块(即被 synchronized 关键字修饰的方法或代码块)时,它必须首先获取该对象的内置锁。如果锁已经被另一个线程持有,该线程将会被阻塞,直到锁被释放。锁机制确保在任何时刻,只有一个线程能持有对象的内置锁,从而保障了同步块内代码的线程安全性。

image

ObjectMonitor 是如何实现线程安全的:

  • 互斥(Mutual Exclusion):

    当一个线程(称为线程A)尝试执行 synchronized 代码块时,它会尝试获取与对象关联的内置锁。
    如果该锁已经被另一个线程(称为线程B)持有,则线程A会被放入等待锁(_entryList)的队列中,并被阻塞。
    当线程B释放锁时,线程A或其他等待锁的线程将有机会获取锁。
    操作原子性(Operation Atomicity):在持有锁的过程中,线程可以原子性地执行同步代码块中的操作。意味着这些操作不会被其他同步代码块并发执行的线程打断。

  • 可见性(Visibility):

    当线程释放锁时,其所做的更改会立即对其他线程可见。即将工作内存中所作的更改同步到主内存,这确保了对共享变量的更改会及时地被其他线程察觉。

  • 等待/通知机制(Wait/Notify Mechanism):

    wait()、notify() 和 notifyAll() 方法提供了线程间的协调机制。
    当一个线程调用 wait() 方法时,它会释放当前持有的锁并进入等待状态,直到另一个线程调用 notify() 或 notifyAll()。
    调用 notify() 会随机唤醒等待队列中的一个线程,而 notifyAll() 会唤醒所有等待的线程,但这些线程仍需要在锁可用时竞争获取锁

3.2 ReentrentLock&Conditio原理

image

1. ReentrentLock 如何实现线程安全

JUC源码学习笔记1——AQS独占模式和ReentrantLock

ReentrentLock 基于AQS的独占模式,本质上是使用一个双向链表实现了
同步队列

img

AQS内部是state来标识当前同步状态,在不同的juc工具类中,state的含义各不相同。在ReentrentLock 中state标识占有锁线程加锁的次数(从而实现可重入)当state=0的时候,ReentrentLock 会使用CAS来改变state的值,cas成功的线程意味着加锁成功,反之加锁失败

  • 加锁失败的线程将自旋+cas的入队——将自己封装成node链接到队列尾部,然后调用LockSupport#park进行阻塞等待

  • 加锁成功的线程将成为同步队列的头部,继续执行后续的业务逻辑,执行完成后调用unlock解锁将修改state的值-1(重入多少次就要释放多少次),如果state的值为0意味着锁被释放,这时候将
    唤醒后续节点,如果后续节点为null or 后续节点阻塞等待的时候被打断那么从队列尾部找最靠近头部
    的阻塞于锁的线程调用LockSupport#unpark进行唤醒

    为什么要从尾部找?=>
    JUC源码学习笔记1——AQS独占模式和ReentrantLock

那么为什么cas可保证执行过程的原子性?LockSupport#park和unpark是如何实现线程的挂起和唤醒的?

  • CPU硬件级别的CAS是作为单个原子指令执行的,这意味着一旦CAS指令开始执行,它的整个状态转换(读取-比较-交换)过程就会在一个不可分割的步骤中完成。这个过程是连续的,CPU不会在CAS指令执行到一半时中断并切换到另一个线程。

    那么,CPU是如何保证这一点的呢?


    • 锁总线(Locking the Bus):

      通过锁定CPU和内存间的总线。在执行这样的原子操作时,处理器会向总线发出一个锁定的信号,没有其他的处理器可以访问内存,直到原子操作完成。

    • 缓存锁定(Cache Locking):
      在多核处理器中通常使用MESI(Modified, Exclusive, Shared, Invalid)协议来确保缓存一致性。当执行原子操作的时候,如果数据被缓存在当前核的缓存中并且是以独占状态存在的,那么该核可以直接对这个缓存行进行操作,而无需锁定总线。


    重要的是,虽然线程可能因为时间片用尽而被挂起,但CPU内部的原子指令一旦开始执行,就会完成整个指令序列,中间不会被操作系统的线程调度打断。

  • LockSupport是Java并发包里一个提供基本线程同步原语的工具类,其中park和unpark是它的两个核心方法。这两个方法为线程提供了阻塞(等待)和唤醒的能力,它们不需要同步器对象(比如锁或者信号量),可以非常灵活地使用。


    • park方法:

      当一个线程调用park时,如果已经有一个unpark操作授予了该线程一个许可(permit),它会立即返回。否则,它可能会阻塞。线程调用park时会被挂起(进入等待状态),等待unpark的唤醒,或者响应中断。

    • unpark方法:

      unpark方法用于“唤醒”或者“重新调度”一个被park挂起的线程。这个方法会给指定的线程提供一个许可(如果之前没有许可的话),使得当该线程调用park方法时不会被阻塞,或者如果该线程已经在调用park时挂起了,那么它将被唤醒。


    在Java中,LockSupport的实现依赖了本地方法,调用了操作系统层面的同步机制。简单来说,就是基于操作系统提供的挂起(suspend)和恢复(resume)线程的原语来实现的。

    在Linux平台下,park和unpark是通过pthread库中的条件变量(condition variables)来实现的。当线程调用park方法时,实际上它调用了本地方法,该本地方法会使用条件变量进行等待。当另一个线程调用unpark时,它实际上会发送信号到条件变量来唤醒挂起的线程。

2. Condition如何实现等待唤醒

JUC源码学习笔记3——AQS等待唤醒机制Condition和CyclicBarrier,BlockingQueue

  • 等待(await方法)

    如果当前线程未持有与Condition相关联的Lock,则将抛出IllegalMonitorStateException。
    将当前线程封装成一个节点,和其他等待的线程一起加入到AQS的条件队列。条件队列是一个单向队列,用来存放调用await()进入等待状态的线程。
    释放已持有的锁并将当前线程挂起
    。在AQS中叫做"fullyRelease",意味着可能会释放多个重入锁。这一操作必须确保至少能原子地释放锁并挂起线程,不然就可能出现死锁。
    如果在等待过程中线程被中断,根据不同的await()实现,线程可能会抛出InterruptedException,或者线程的中断状态将被设置。

  • 唤醒(signal或signalAll方法)

    如果当前线程未持有与Condition相关联的Lock,则将抛出IllegalMonitorStateException。
    对于signal()方法,AQS将从条件队列中选择一个等待最长时间的线程节点,并将其转移到同步队列。同步队列是用来竞争获取锁的队列。
    对于signalAll()方法,AQS将把条件队列的所有线程节点全部转移到同步队列。
    被移动到同步队列的线程节点将在锁释放时竞争获取锁,一旦获取锁成功,它们可以从await()方法返回并继续执行。

3. ReentrentLock如何实现线程可见性

这里说的不是AQS state属性的可见性,因为state是volatile修饰,自然保证了其可见性

看下面这个代码,为什么list没用被volatile修饰,但是保证了其线程可见性,main线程可用读到writeThread对list的修改

image-20240107160123065

这一切都是是因为unlock释放锁,会修改volatile修饰的state变量

对于volatile变量的写操作,会在其后插入一个内存屏障。在Java中,volatile变量的写操作后通常会插入一个"store-store"屏障(Store Barrier),以及一个"store-load"屏障。这些内存屏障确保了以下几点:

  • Store-Store Barrier:这个屏障防止volatile写与之前普通写的指令重排序。也就是说,对volatile变量的写操作之前的所有普通写操作都必须在volatile写之前完成,确保了volatile写之前的修改对于其他线程是可见的。
  • Store-Load Barrier:这个屏障防止volatile写与之后的读写操作重排序。它确保了volatile变量写之后的读取操作不会在volatile写之前发生。这保证了volatile写操作的结果对后续操作是可见的。

在x86架构中,由于其内存模型的特性,每次对volatile变量的写操作通常会伴随着一个lock前缀的指令,该指令实际上会执行一个"全能型"的内存屏障(Memory Fence),它包括了上述的store-store和store-load屏障,同时还会兼具load-store屏障的效果。这种屏障确保了指令执行的顺序性和数据的可见性。

volatile的这些内存屏障特性是由Java内存模型(JMM)强制实施的,JVM负责在编译时期和运行时期将这些规则映射到具体的硬件指令上。

也就是说
对volatile变量的写操作确实可以确保在这次写操作之前的所有普通变量的修改对其他线程是可见的

这是因为对volatile变量的写操作会在其写操作后插入一个内存屏障,防止之前的写入操作被重排序到这个内存屏障之后

简单来说,按照下面的顺序执行操作:

  • 线程A修改普通变量x。
  • 线程A写入volatile变量v。

这时,根据happens-before原则,当线程A写入volatile变量v后,任何线程B读取同一个volatile变量v,并看到线程A写入的值,那么它也保证看到线程A在写入volatile变量v之前对普通变量x所做的修改。

这个特性在并发编程中经常用来确保重要信号、状态或数据的传递是准确且及时的。当使用volatile变量作为状态标志或锁的一部分时,这个特性特别有用。

4.感想

看似很简单的一题,其实知识点也是蛮多的,结合这些原理可以看出——编程没有魔法,都是操作系统提供的机制吧,操作系统牛逼!

1.首先nutGet 进行使用

2.如果需要使用管方的Key 进行激活

3.直接上写的Demo代码

1 usingSystem;2 usingSystem.Collections.Generic;3 usingSystem.ComponentModel;4 usingSystem.Data;5 usingSystem.Drawing;6 usingSystem.Linq;7 usingSystem.Text;8 usingSystem.Threading.Tasks;9 usingSystem.Windows.Forms;10 usingSteema.TeeChart;11 usingSteema.TeeChart.Styles;12 
13 namespaceTeechartForm14 {15 public partial classFormTeeChart : Form16 {17 privateTChart tChart;18 private Label labelValue = newLabel();19 publicFormTeeChart()20 {21 InitializeComponent();22 //在构造函数中初始化TChart
23 tChart = newTChart();24 //tChart.Parent = this.panelCharts;
25 tChart.Dock =DockStyle.Fill;26 this.panelCharts.Controls.Add(tChart);27
28 //初始化 labelValue29 //labelValue.AutoSize = true;30 //labelValue.BackColor = Color.Transparent;31 //labelValue.Visible = false;//默认情况下不可见32 //this.Controls.Add(labelValue);33
34 //tChart.MouseMove += TChart_MouseMove;
35 }36
37 private void FormTeeChart_Load(objectsender, EventArgs e)38 {39
40 }41
42 //添加一个标志来跟踪鼠标是否被按下
43 private bool isMouseDown = false;44
45
46
47 private bool mOnBoxing = false;48 Point mPoint1 = newPoint();49 Point mPoint2 = newPoint();50 private void TChart_MouseHover(objectsender, MouseEventArgs e)51 {52 MessageBox.Show("1");53 //labelValue.Text = "666666";54 //labelValue.Visible = true;
55
56 TChart chart =(TChart)sender;57 if (Control.ModifierKeys ==Keys.None)58 {59 mPoint2 =e.Location;60
61 chart.Invalidate();62 }63 }64
65
66 private void buttonLine_Click(objectsender, EventArgs e)67 {68 //条形图
69 BarChart();70
71 }72
73 private void buttonBoxPlot_Click(objectsender, EventArgs e)74 {75 BoxPlot();76 //tChart.MouseMove += TChart_MouseMove;
77 }78
79
80 /// <summary>
81 ///进行获取Tcharts 鼠标移动的位置82 /// </summary>
83 /// <param name="sender"></param>
84 /// <param name="e"></param>
85 private void TChart_MouseMove(objectsender, MouseEventArgs e)86 {87 //获取鼠标在图表上的位置
88 int x =e.X;89 int y =e.Y;90
91 Series s = tChart.Series[0];92 int pointIndex =s.Clicked(e.X, e.Y);93 if (pointIndex != -1)94 {95 //获取数据点的值
96 double value =s.YValues[pointIndex];97
98 //设置标签文本
99 labelValue.Text = "Value:" +value.ToString();100
101 //移动标签位置并显示
102 labelValue.Left = x + 10; //鼠标位置的偏移量
103 labelValue.Top =y;104 labelValue.Visible = true;105 }106 else
107 {108 //如果不在任何数据点上,则不显示标签109 //labelValue.Visible = false;
110 }666666 }112
113 private void GantMap_Click(objectsender, EventArgs e)114 {115 Gant();116 }117
118 private void LineMap_Click(objectsender, EventArgs e)119 {120 LineChart();121 }122
123
124 /// <summary>
125 ///折线图126 /// </summary>
127 public voidLineChart()128 {129 //清除现有的系列
130 tChart.Series.Clear();131
132 //创建一个折线图系列
133 Line lineSeries = newLine(tChart.Chart);134
135 //添加数据到图表
136 lineSeries.Add(0, "Apples");137 lineSeries.Add(5, "Pears");138 lineSeries.Add(2, "Oranges");139 lineSeries.Add(7, "Bananas");140 lineSeries.Add(4, "Pineapples");141
142 //将系列添加到TChart
143 tChart.Series.Add(lineSeries);144
145 //鼠标移动事件
146 tChart.MouseMove += (s, e) =>
147 {148 int pointIndex = -1;149 //遍历所有系列
150 foreach (Series series intChart.Series)151 {152 //尝试找到鼠标下的数据点
153 pointIndex =series.Clicked(e.X, e.Y);154 if (pointIndex != -1)155 {156 //如果找到数据点,执行所需的操作
157 string pointLabel =series.Labels[pointIndex];158 double pointValue =series.YValues[pointIndex];159 tChart.ClickTitle +=TChart_ClickTitle;160 //tChart.ClickTitle + = "Value: " + pointValue.ToString() + " for " + pointLabel;
161
162 break; //跳出循环,因为我们已经找到了数据点
163 }164 else
165 {166 tChart.Header.Text = "";167 }168 }169 };170
171 //其他代码保持不变...172
173 //设置图表标题
174 tChart.Header.Text = "Fruit Consumption";175 tChart.Header.Visible = true;176 tChart.Refresh();177 tChart.Invalidate(); //可能不需要手动调用
178 }179
180 private void TChart_ClickTitle(objectsender, MouseEventArgs e)181 {182 MessageBox.Show("6666661");183 }184
185
186 /// <summary>
187 ///条形图188 /// </summary>
189 public voidBarChart()190 {191 //清除现有的系列
192 tChart.Series.Clear();193 //创建一个条形图系列
194 Bar barSeries = newBar();195 //添加数据到图表
196 barSeries.Add(0, "Apples");197 barSeries.Add(5, "Pears");198 barSeries.Add(2, "Oranges");199 barSeries.Add(7, "Bananas");200 barSeries.Add(4, "Pineapples");201
202 //将系列添加到TChart
203 tChart.Series.Add(barSeries);204
205
206 //鼠标按下事件207 //tChart.MouseMove += (s, e) =>208 //{209 //if (e.Button == MouseButtons.Left)210 //{211 //isMouseDown = true;//设置标志为true212 //MessageBox.Show("11");213 //}214 //};215
216 //在类的成员区域初始化一个工具提示
217 System.Windows.Forms.ToolTip tooltip = newSystem.Windows.Forms.ToolTip();218
219 //鼠标释放事件
220 tChart.MouseMove += (s, e) =>
221 {222 if (e.Button ==MouseButtons.Left)223 {224 int pointIndex = -1;225 //遍历所有系列
226 foreach (Series series intChart.Series)227 {228 //尝试找到点击的数据点
229 pointIndex =series.Clicked(e.X, e.Y);230
231 if (pointIndex != -1)232 {233 //如果找到数据点,执行所需的操作
234 string pointLabel =series.Labels[pointIndex];235 double pointValue =series.YValues[pointIndex];236 //MessageBox.Show("Clicked on: " + pointLabel + " Value: " + pointValue.ToString());237
238 //显示工具提示
239 tooltip.Show("Value:" + pointValue.ToString() + "for" + pointLabel, tChart, e.Location.X + 15, e.Location.Y - 15, 2000); //显示2秒
240
241 break; //跳出循环,因为我们已经找到了数据点
242 }243 }244 }245 };246
247
248 //绑定鼠标点击事件处理程序到 tChart 控件249 //tChart.MouseClick += (s, e) =>250 //{251 //if (e.Button == MouseButtons.Left)252 //{253 //int pointIndex = -1;254 // //遍历所有系列255 //foreach (Series series in tChart.Series)256 //{257 // //尝试找到点击的数据点258 //pointIndex = series.Clicked(e.X, e.Y);259
260 //if (pointIndex != -1)261 //{262 // //如果找到数据点,执行所需的操作263 //string pointLabel = series.Labels[pointIndex];264 //double pointValue = series.YValues[pointIndex];265 //MessageBox.Show("Clicked on: " + pointLabel + " Value: " + pointValue.ToString());266 //break;//跳出循环,因为我们已经找到了数据点267 //}268 //}269 //}270 //};271
272 //其他代码保持不变...273 //其他代码保持不变...274 //在类的成员区域初始化一个工具提示
275 /*System.Windows.Forms.ToolTip tooltip = new System.Windows.Forms.ToolTip();276 // 鼠标移动事件277 chart.MouseMove += (s, e) =>278 {279 int pointIndex = -1;280 // 遍历所有系列281 foreach (Series series in chart.Series)282 {283 Point mPointss = new Point();284 // 尝试找到鼠标下的数据点285 pointIndex = series.Clicked(e.X, e.Y);286
287 if (pointIndex != -1)288 {289 // 如果找到数据点,执行所需的操作290 string pointLabel = series.Labels[pointIndex];291 double pointValue = series.YValues[pointIndex];292 // 显示工具提示293 tooltip.Show("Value: " + pointValue.ToString() + " for " + pointLabel, chart, e.Location.X + 15, e.Location.Y - 15, 2000); // 显示2秒294 // chart.Invalidate();295 break; // 跳出循环,因为我们已经找到了数据点296 }297
298 }299 };300 */
301
302 //设置图表标题
303 tChart.Header.Text = "Fruit Consumption";304 tChart.Header.Visible = true;305 tChart.Refresh();306 tChart.Invalidate();307
308
309 }310
311 /// <summary>
312 ///BoxPlot313 /// </summary>
314 public voidBoxPlot()315 {316
317 //清除现有的系列
318 tChart.Series.Clear();319
320 //创建一个 BoxPlot 系列
321 Box boxSeries = newBox(tChart.Chart);322
323 //填充 BoxPlot 数据
324 boxSeries.Add(new double[] { 3, 5, 7, 2, 6});325 boxSeries.Add(new double[] { 4, 6, 8, 3, 7});326 boxSeries.Add(new double[] { 5, 7, 9, 4, 8});327 boxSeries.Add(new double[] { 5, 7, 9, 4, 8});328 //boxSeries.Add(new double[] { 15, 17, 19, 14, 18 });329 //... 添加更多的数据点 ...330
331 //设置标题
332 tChart.Header.Text = "Sample BoxPlot";333 tChart.Header.Visible = true;334
335 //将 BoxPlot 系列添加到 TChart
336 tChart.Series.Add(boxSeries);337 tChart.Refresh();338 tChart.Invalidate();339 }340
341
342 /// <summary>
343 ///Gant344 /// </summary>
345 public voidGant()346 {347 //清除现有的系列
348 tChart.Series.Clear();349
350 //创建一个甘特图系列
351 Gantt ganttSeries = newGantt();352
353 //添加数据到图表,每个点的格式为 Add(DateTime start, DateTime end, string text, Color color)
354 ganttSeries.Add(DateTime.Today.AddDays(1), DateTime.Today.AddDays(12), 100, "Project B", Color.Green);355 ganttSeries.Add(DateTime.Today.AddDays(3), DateTime.Today.AddDays(15), 200, "Project C", Color.Red);356 //... 添加更多的数据 ...357
358 //将系列添加到TChart
359 tChart.Series.Add(ganttSeries);360
361 //设置X轴为日期时间类型
362 tChart.Axes.Bottom.Labels.Angle = 90;363 tChart.Axes.Bottom.Labels.DateTimeFormat = "dd-MMM";364 tChart.Axes.Bottom.Labels.Style =AxisLabelStyle.Value;365
366 //设置Y轴为分类类型,便于显示任务名称
367 tChart.Axes.Left.Labels.Angle = 0;368 tChart.Axes.Left.Labels.Style =AxisLabelStyle.Text;369
370 //设置图表标题
371 tChart.Header.Text = "Project Timeline";372 tChart.Header.Visible = true;373
374 //刷新图表显示
375 tChart.Refresh();376 tChart.Invalidate();377
378 }379
380 }381 }

抽象类与接口有哪些不同?

抽象类和接口是在面向对象编程中两个不同的概念,它们有一些重要的区别。以下是抽象类和接口的主要不同点:

抽象类(Abstract Class):

  1. 成员类型:


    • 抽象类可以包含抽象方法(方法没有实现,由派生类实现)和具体方法(有实现)。
    • 抽象类可以包含字段、属性、构造函数,以及其他非抽象成员。
  2. 构造函数:


    • 抽象类可以有构造函数,并且在实例化派生类时,基类的构造函数会被调用。
  3. 访问修饰符:


    • 抽象类的成员可以有各种访问修饰符,包括
      public

      protected

      internal
      等。
  4. 多继承:


    • 一个类只能继承一个抽象类(单继承)。
  5. 状态:


    • 抽象类可以包含字段,可以有状态。

接口(Interface):

  1. 成员类型:


    • 接口只能包含抽象方法和属性,而这些成员都是没有实现的。
    • 在 C# 8.0 及之后的版本中,接口还支持默认实现的方法和属性。
  2. 构造函数:


    • 接口不能包含构造函数。
  3. 访问修饰符:


    • 接口的成员默认是
      public
      的,且不能包含访问修饰符。
  4. 多继承:


    • 一个类可以实现多个接口(多继承)。
  5. 状态:


    • 接口不能包含字段,因此没有状态。

共同点:

  1. 抽象性:


    • 抽象类和接口都是抽象的,不能直接实例化。
  2. 实现:


    • 派生类必须实现抽象类中的抽象方法或接口中的所有成员。
  3. 设计目的:


    • 抽象类通常用于定义一些共享的实现或者具有状态的类。
    • 接口用于定义一组行为契约,强调类之间的合同。

在实际项目中,你可能会根据需要同时使用抽象类和接口,以便更好地组织代码并满足设计需求。选择使用抽象类还是接口通常取决于你的设计目标和具体情境。

什么时候应该使用抽象类?

抽象类是一种在面向对象编程中常见的概念,它与接口类似,但具有一些不同之处。以下是一些使用抽象类的情况:

  1. 共享代码实现:
    如果多个相关的类有一些相同的实现细节,你可以将这些共享的实现放在一个抽象类中,然后让其他类继承这个抽象类。

    public abstract class Shape
    {
        public abstract void Draw(); // 抽象方法,需要子类实现
    
        public void Move() 
        {
            // 共享的实现
        }
    }
    
    public class Circle : Shape
    {
        public override void Draw()
        {
            // 实现 Draw 方法
        }
    }
    
    public class Square : Shape
    {
        public override void Draw()
        {
            // 实现 Draw 方法
        }
    }
    
  2. 提供默认实现:
    抽象类可以包含一些已经实现的方法,而接口不能包含具体的实现。这使得抽象类可以提供一些默认的行为,而子类可以选择性地覆盖这些方法。

    public abstract class Shape
    {
        public abstract void Draw(); // 抽象方法,需要子类实现
    
        public virtual void Move() 
        {
            // 共享的实现
        }
    }
    
    public class Circle : Shape
    {
        public override void Draw()
        {
            // 实现 Draw 方法
        }
    
        // Move 方法可以选择性地覆盖
        public override void Move()
        {
            // 实现 Circle 特有的移动逻辑
        }
    }
    
  3. 有状态的类:
    抽象类可以包含字段(字段可以存储状态),而接口不能包含字段。如果你的类需要包含一些状态信息,使用抽象类可能更合适。

    public abstract class Animal
    {
        private int age;
    
        public int Age
        {
            get { return age; }
            set { age = value; }
        }
    
        public abstract void MakeSound();
    }
    
    public class Dog : Animal
    {
        public override void MakeSound()
        {
            // 实现狗的叫声
        }
    }
    

总体而言,使用抽象类还是接口取决于你的设计需求。抽象类通常用于有一些共享实现或者需要包含状态的情况,而接口通常用于定义一些行为契约。在实际项目中,你可能会同时使用抽象类和接口,以满足不同的设计需求。

现在是2024年的1月7日,被生物钟叫醒的我百无聊赖,点开了好久未打开的博客园,看着2023年发的那3篇博客随笔躺在冷清的首页,不禁感叹:真快啊,一年过去了。又不禁自嘲:害,又是一事无成的一年。

我记着2023年我给自己好好规划过关于写博客这件事的安排:每两周或者一周作为一个周期去钻研一个技术点,钻研的过程中记录下笔记,然后在周末的时候花一两个小时进行复盘和整理自己在这一两周里记得笔记,作为一篇技术博客发表出来。 想想多美好,工作的空余时间中用一两周学一个知识点绝对谈不上什么大的压力,不会耽误自己的正常工作生活,而且周末我也没有睡懒觉的习惯,完全可以在早起的一两个小时中做一些复盘整理,占用不了太多周末的美好时光。可现实是:本应该一年至少能发20多篇博客的我,只发了3篇。想到这,我是否要给自己找些理由搪塞过去:工作忙啊、成年人了别执着于写写学学了、年纪大了脑子不似少年时灵光了。。。但庆幸的是,18岁以后,我已经不会再用这些宽慰自己的话哄骗自己了,高考已经让我付出了惨痛的代价了,直到步入社会多年,愈发的明白那一纸学历对于我们的人生有着多么深刻的影响。当年从山东考生的千军万马中厮杀出来,从小有着名牌大学梦的我却只能上一个普通的本科大学,初中时很傻,从第一名掉到第三名都要懊恼半天,心里暗暗发誓一定要夺回来宝座,于是更加刻苦;高中时很聪明,成绩滑落到24名了还很自负,认为自己有这有那的理由没考好而已,依旧在数学课上睡觉。

总的来讲:我是规划上的巨人、自负的捍卫者、行动上的跛脚者。从小到大确实做成过不少事,总被当做亲戚家孩子下一辈的榜样来提及,但是也在一些关键事上掉链子,做什么都差一点,成为了名副其实的差一点先生。

下面按照时间线来看一下差一点先生是怎么一步步养成的吧:

1.2012年中考结束后,如愿考上了我们那最好的一所重点高中,但是怂,不敢向暗恋许久的女孩表白,一直犹豫胆怯,好不容易鼓起勇气了却发现在前一天,那个女孩答应了同班追了她半年的男生,女孩还劝我说你没勇气早点BiaoBai,就只能是这样的结局。好吧,现在10多年没见过了。

2.高中的我脱离束缚放飞自我了,高一上半学期光顾着交朋友认兄弟,学习没咋学,清楚着记着数学课我基本没听全靠偶尔的自学,结果期中考试数学还是考了132分,比那些刻苦认真半年的同学考的还好,于是真的飘了飘了,自负起来了,认为自己简直是天赋异禀。 行吧,高考时数学109,啪啪打脸了。

3.依稀记着三模好像考了600多分,但是高考只有540分左右,额,当年志愿都不想填了。

4.记着可以填写六个目标院校,每个院校可以填写5个专业,在家人希望我成为一名人民教师的期望下,我填了一大堆的师范专业,就是最后一个实在的想不起来填啥了,就是眼睛一撇看见了个软件工程,当年也不知道这是干啥的,就随便一写。好吧,这一随便我从人民教师变成了Java开发工程师了。

5.大二还是大三来跟风考教师资格证,报了个初中的,一共三门过了两门,这成绩可以留存三年的,我只要三年内再过了剩余一门就好了,不知道自己当时咋想的,没再去考剩下的那一门。

6.大学侥幸当了4年班长,看在我没有功劳也有苦劳的份上怎么着也是班里最接近入党的那个人,可这幅好牌还是被我打烂了,学习成绩要求上差了半名(我班人数是奇数),再多考那么一两分就够资格了。

7.后来又跟风考研了,计算机类的国家线是265,我考了325,但是我却报了一个很好的985大学的研究生,进复试要340。行吧,没过院线是我水平不够,但是超了国家线60分我怎么着选择调剂其他学校的话也能拿个研究生学历啊,但就是那骨子里的拗劲,觉得上不了985 211的研究生倒不如不去了,还不如早点工作。行吧,来到社会上才发现被学历斩掉了大批量的好的工作机会。

8.2022年11月份,有个比当下薪资多3K的工作找上了我,那时候我浑然不知互联网寒冬开始到来了,还觉得只涨3K太低了,完全看不上。再加上老东家给画了张饼,就继续呆着了。后来2023年9月份遭遇了失业危机,项目组要被砍掉了,出去面试了三天,虽然offer不少,可发现都只愿意平薪或者降薪招人,有愿意涨薪的只涨1K,把我整的怀疑人生了,曾经的互联网不都是跳槽涨薪30%起步么。最终还是龟缩回来了,还在老东家,只不过换了个项目组。

9.2023年10月28日,我参加了软考-高项的考试。本次我是真的试水的,我只在七月下旬到8月下旬规律的学了一个月,当然没有学完,后来遭遇了失业危机就中断了,以至于到考前一两天里也没再恢复计划,临阵磨枪了没?磨了,就是刷了一套选择题而已。 考试的当天中午才首次读了一篇论文。最终成绩选择:案例分析:论文 = 45,45,39 。我是真的没想到选择和案列分析我居然卡线过了,但是这成绩心里好不是滋味,我一直觉得我顶多均分20分,可没想到居然过了2门,那是不是意味最后一周我再论文上多下下功夫就能全部通关了,可惜软考是不做成绩保留的,下次三门还是要重新考,得了,又差一点。半年后见了。

10.我希望没有第10条了。

回顾我经历的这27载,确实有很多我想拿到完美结局的事情却总是最后关头差那么一点点,也就是差了那么一点点,前面99%的辛苦相当于白白付出,和那些10%辛苦的人没什么区别。要问我后悔么,是不是当年再稍微努力一点,结局就会不一样。但是我想说,其实我一点也不后悔,只是有点遗憾罢了。我相信当年作出的选择都是那时候自己认为应该去做的事情,都是当时的我想要去做的。初中的我是学习特别刻苦,但是只顾学习的我却没有个可以倾诉衷肠的好朋友,高中时看起来是玩乐多了些,可我却交到了此生都能相互扶持的好朋友好兄弟;16岁时喜欢的白月光没能在一起是很遗憾,可是青春哪有不遗憾的,况且我现在的老婆很好,我们三观很合相处很融洽,能互相支持互相理解;大学时确实没能入党,但是四年班长没有白当啊,我学到了很多软技能,比如少时人前讲话我会舌头打结颤颤巍巍,但是四年的锻炼下我在人前也能谈笑自如,学会了去安排工作去主持会议;研究生没考上确实难过备受限制,但提前工作也让我吃了几年互联网红利,看同龄人才刚研究生毕业,我已经靠自己把老婆娶回家了。所以呢,反思自己的同时也不要过于的把已经逝去的青春岁月看的一文不值,反思是为了总结问题,让自己更好的设计未来,但是一味的抨击自己只会让自己在回忆的漩涡中无法自拔,永远得不到抽离,人生总还是往前看的,过去的就过去吧,好好过好未来的每一天才是最重要的,人生苦短,我哪有那么多的时间去一直后悔荒唐青春啊!

行吧,我承认写文字真有魔力,写着写着又把自己开导了,8点开始动笔的,一转眼俩小时过去了,本来想总结自己这些年哪些事情没做好,然后镇定思痛,立个Flag,2024年好好规划下,可写到最后却释怀了。 生命中哪有那么多的对与错,不是每个人都是彭于晏,也没法像他那样自律,活在当下,遵循本心,不后悔人间来一遭即可。至于2024要不要规划,我想还是要的,毕竟已经27了,每年工作之余考考证、学学习、唱唱歌也是生活的调味剂么,只不过要保持一个舒心的状态而不是强逼自己让自己焦虑为难的状态。至于Flag么,我还是不要立了,我会遵循本心全力去做,但是立了后万一没实现,明年的这个时候又该开始批判自己了。

哈哈,希望2024年我不要再当差一点先生了,那个第10条永远不要补上了。

国内文章

CAP 8.0 版本发布通告 - CAP 7岁生日快乐!

https://www.cnblogs.com/savorboard/p/cap-8-0.html

今天宣布CAP 8.0版本正式发布,恰逢项目七周年及作者生日。七年间,CAP共发布61个版本,在GitHub获得6.3K星标,有108名贡献者,核心包在NuGet上的下载量达640万次。CAP是一种分布式事务解决方案,通过本地消息表保证数据安全,同时可作为EventBus使用。8.0版本主要更新包括全面支持.NET 8,优化Dashboard认证授权,新增配置项以自定义回溯时间窗,改进消费者预取和分组调度配置项的协同工作,NATS支持配置DeliverPolicy,默认为New。此外,修复了多个BUG,包括消息无限重试、Open Telemetry上下文丢失等问题。破坏性改动包括移除Dashboard中的某些配置项。更多信息请查看官方文档。

EF Core助力信创国产数据库

https://www.cnblogs.com/CreateMyself/p/17900180.html

本文讨论了国产数据库的发展,特别是人大金仓和华为高斯数据库,它们都基于PostgreSQL。作者在工作之余将EF Core适配到这些数据库并开源,以便社区共同改进。目前已支持人大金仓EF Core 6.x和华为高斯EF Core 8.0,未来计划支持更多版本。作者鼓励社区贡献而非仅提问题,并强调在处理问题时需要具体的代码示例。

.NET中如何实现高精度定时器

https://www.cnblogs.com/czwy/p/17915333.html

本文探讨了在.NET中实现高精度定时器的方法。首先介绍了定时器的三个核心功能:计时、等待和触发模式,并解释了如何使用Windows系统的API和.NET类来获取高精度时间戳。文章详细讨论了两种等待策略:自旋等待和阻塞等待,以及如何通过系统API调整Windows的计时器精度。最后,提到了多媒体定时器
timeSetEvent
作为实现高精度定时器的一种方法,尽管它已被官方标记为过时,但在精度和稳定性方面仍优于其他方法。

用C#也能做机器学习?

https://www.cnblogs.com/mingupupu/p/17918738.html

本文介绍了如何在C#中使用ML.NET进行机器学习,特别是通过ML.NET Model Builder构建猫狗识别模型。ML.NET是一个适合.NET开发者的免费、开源机器学习框架,支持C#或F#语言。Model Builder提供了一个直观的界面,使得在Visual Studio中生成、训练和部署机器学习模型变得简单,无需深厚的机器学习背景。它还支持AutoML自动选择最佳算法和设置。Model Builder作为Visual Studio扩展,使.NET开发者能够在熟悉的环境中工作,无需依赖云资源或其他服务。

一款基于.NET Core的快速开发框架、支持多种前端UI、内置代码生成器

https://www.cnblogs.com/Can-daydayup/p/17922742.html

本文介绍了WalkingTec.Mvvm(简称WTM),这是一个基于.NET Core的开源快速开发框架,支持多种前端UI,具备代码生成器,旨在提升开发效率。WTM支持前后端分离,降低沟通成本,内置了用户、角色、权限等管理功能,支持单点登录和分布式数据库。技术栈包括LayUI、React、VUE、Blazor等前端技术,以及.NET Core、EF Core、Redis等后端技术。数据库支持MySql、Sql Server、PostgreSQL。WTM的源代码和文档可在GitHub查看,项目已被收录为C#/.NET/.NET Core优秀项目。DotNetGuide技术社区提供.NET相关资源和交流平台,鼓励开发者分享经验和技术问题。

简便实用:在 ASP.NET Core 中实现 PDF 的加载与显示

https://www.cnblogs.com/powertoolsteam/p/17911303.html

本文讲述了如何在ASP.NET Core中加载和显示PDF文件。首先,使用Visual Studio创建ASP.NET Core Web应用程序,并选择.NET Core 6.0作为目标框架。然后,安装GrapeCity.Documents.Pdf依赖包。接着,在Index.cshtml.cs页面中定义服务器端代码生成PDF文件,包括添加图片、文本格式设置、添加标题和项目列表。最后,将生成的PDF文件保存到Web根目录。文中提供了详细的代码示例和最终效果的截图。

Semantic Kernel 正式发布 v1.0.1 版本

https://www.cnblogs.com/shanyou/p/17924196.html

微软发布了Semantic Kernel的.NET 1.0.1版本,这是一个开源SDK,结合了AI服务和传统编程语言,如C#和Python,以创建AI应用程序。它作为AI编排层,与Microsoft AI模型堆栈和Copilot AI助手配合。新版本提供了文档,介绍了如何创建能与用户互动的AI代理,执行任务如自动化流程。文档还涵盖了构建AI代理的核心组件,如插件、规划器和角色。Semantic Kernel现在稳定,未来将专注于AI连接器、Memory连接器和Agent抽象。微软鼓励社区参与AI连接器的开发,核心团队将在假期后审查社区贡献。

记一次 .NET 某药厂业务系统 CPU爆高分析

https://www.cnblogs.com/huangxincheng/p/17916751.html

本文讲述了作者帮助朋友分析程序CPU使用率过高的问题。首先,通过procdump工具抓取dump文件,然后使用Windbg的!tp命令确认CPU利用率为88%。通过!cpuid命令发现服务器只有4个CPU核心,性能较弱。进一步分析发现程序有451个线程,其中443个是后台线程,多数线程通过new Thread创建而非线程池。使用~*e !clrstack命令检查线程栈,发现大多数线程在执行Thread.SleepInternal。作者最终意识到频繁的上下文切换可能是导致CPU爆高的原因,特别是在Loop方法中发现了大量的Sleep(1)调用。

升讯威在线客服系统的并发高性能数据处理技术:超强的 SignalR

https://www.cnblogs.com/sheng_chao/p/17921167.html

本文介绍了作者在业余时间开发的升讯威在线客服系统,该系统免费开源且用户众多。系统通过客户的压力测试,表现出色,无异常掉线,消息实时到达。作者计划分析系统的高性能并发技术,首篇聚焦SignalR技术。SignalR是ASP.NET Core的实时Web功能库,适用于需要服务器实时更新的应用,如游戏、社交网络等。它支持WebSockets、Server-Sent Events和长轮询等传输技术,自动选择最佳传输方法。SignalR使用中心(Hub)进行通信,允许客户端和服务器互调方法,支持JSON和MessagePack协议。作者还展示了如何在客服系统中创建SignalR中心,配置服务器,并添加客户端代码以实现实时通信。

Net 高级调试之十四:线程同步的基础知识和常见的同步原语

https://www.cnblogs.com/PatrickLiu/p/17910805.html

本文是《Net 高级调试》系列的第十四篇,重点介绍了多线程中锁的底层实现原理,而非其使用方法和API。文章强调了对.Net框架底层理解的重要性,有助于更好地进行调试。调试环境包括Windows 10专业版、Windbg Preview、Visual Studio 2022和.Net Framework 4.8。基础知识部分讲述了线程同步原语,包括C#、CLR和OS层的线程表示,以及不同类型的锁,如AutoResetEvent、Semaphore、Monitor和ThinLock。最后,文章提供了源码调试的具体过程,通过实际代码来验证所学知识。

将Abp默认事件总线改造为分布式事件总线

https://www.cnblogs.com/jevonsflash/p/17917031.html

本文介绍了分布式事件总线的原理和实现。本地事件总线通过Ioc容器和事件订阅表实现事件的注册与触发。分布式事件总线通过中间件如RabbitMQ、Kafka、Redis转发事件,与本地事件总线兼容。自动订阅和事件转发功能确保本地事件注册时同时订阅分布式事件,并在本地事件触发时将消息转发到分布式总线。消费端接收分布式事件消息后,解析类型并触发本地事件,由本地事件处理器完成处理。

.NET周刊【12月第2期 2023-12-13】

https://www.cnblogs.com/InCerry/p/dotnet_week_23_12_2.html

本文汇总了.NET领域的多篇文章。首先介绍了使用.Net6的miniapi开发简洁API的方法,包括数据库操作和Swagger配置。接着分享了C#/.NET/.NET Core的优秀项目和框架,如CAP、ZEQP.WMS和HandyControl,以及.NET平台下的网络爬虫框架DotnetSpider。此外,C#有望成为2023年度编程语言,TIOBE榜单显示其增长迅速。文章还分析了博客园频繁崩溃的原因,探讨了SQL Server的性能问题。另外,.NET8的AOT编译性能优化被详细讨论,包括减少可执行文件大小和提升性能。还有对.NET内存管理术语的解释,以及使用.NET 8开发个人网盘项目的介绍。最后,ML.NET 3.0的发布强化了深度学习和数据处理能力。

.NET周刊【12月第1期 2023-12-06】

https://www.cnblogs.com/InCerry/p/dotnet_week_23_12_1.html

本文汇总了多篇关于.NET和相关技术的文章。介绍了openEuler操作系统的多处理器架构支持和.NET社区合作机会;MAUI Blazor应用中显示本地媒体文件的新方法;Visual Studio 2022 17.8版本的性能提升;.NET中六种定时器的使用场景;HtmlAgilityPack库在.NET中解析HTML的功能;ASP.NET Core Web API中设置Json响应格式的方法;Cron表达式在Unix系统中的应用及在.NET中的解析执行;VS2022中调试.Net源码的配置方法;以及C#中只读结构体成员可能导致的问题。这些内容涵盖了操作系统支持、开发工具更新、编程技巧和潜在BUG的警示,对.NET开发者具有实用价值。

Kernel Memory 入门系列:Kernel Memory Service

https://www.cnblogs.com/xbotter/p/kernel_memory_service.html

本文介绍了Kernel Memory Service,一个GitHub上的项目,提供独立部署后台和接口服务。首先通过Git Clone下载源码,然后执行setup脚本或命令进行初始化,配置项存储在appsettings.json中。配置完成后,运行run脚本启动服务,本地运行可通过Swagger页面访问。C#用户可用MemoryWebClient包调用接口。项目还提供了工具脚本,如启动Qdrant和RabbitMQ服务,初始化和启动Kernel Memory Service,上传文件和提问等功能。

封装Detours用于Python中x64函数hook

https://www.cnblogs.com/kanadeblisst/p/17922099.html

本文介绍了微软的Detours框架,它支持多种处理器架构,用于实现API hook。文中详细描述了如何在x64环境下编译Detours,包括解决编译错误和生成Debug版本的方法。还提供了使用Detours进行函数hook的代码示例,并说明了如何在项目中引入Detours的头文件和库文件。最后,作者推荐了使用预处理器指令在代码中直接引入库文件的方式。

Kernel Memory 入门系列:快速开始

https://www.cnblogs.com/xbotter/p/kernel_memory_quick_start.html

本文介绍了如何快速开始使用Kernel Memory,包括通过NuGet安装包、构建内存实例、内容导入和问答查询。构建时,可配置OpenAI或Azure OpenAI服务,支持自定义文本生成和Embedding模型。内容导入支持文件路径、文件流、文档集合和URL方式。问答功能通过调用
AskAsync
方法实现,可获取答案及相关文档信息。

使用代码生成工具快速开发应用-结合后端Web API提供接口和前端页面快速生成,实现通用的业务编码规则管理

https://www.cnblogs.com/wuhuacong/p/17921357.html

本文介绍了如何使用代码生成工具Database2Sharp和SqlSugar开发框架,快速开发Winform、WPF和Vue3+ElementPlus前端应用。通过通用业务编码规则的管理功能,可以统一生成和维护各种编码,如订单号等。文章展示了数据库设计,后端接口,以及Winform和WPF界面的生成过程。最后,介绍了如何利用生成的Web API控制器和Service层接口,快速实现Vue3前端界面的开发。

称重驱动二次开发教程

https://www.cnblogs.com/yizhuqing/p/17921902.html

本文介绍了如何通过二次开发快速集成新的电子秤驱动到系统。首先,需要了解系统介绍并下载相关软件。接着,创建一个控制台工程,并添加引用DDS.IOT.Weight.exe。然后,新建一个继承自WeightBase的电子秤类,并实现虚方法。最后,通过后台任务模拟电子秤,定时输出随机重量。这些步骤可以帮助开发者快速实现电子秤驱动的集成。

Blazor入门100天 : 自做一个支持长按事件的按钮组件

https://www.cnblogs.com/densen2014/p/17915285.html

本文继续介绍了博主的系列博客,提供了配套源码和在线演示demo。文章详细说明了如何创建一个支持长按功能的Blazor组件,包括设置长按触发的回调委托、点击事件处理、判断是否触摸设备等。通过代码示例,展示了组件的实现过程和功能。

AntDesignBlazor示例——Modal表单

https://www.cnblogs.com/known/p/17923002.html

本文是AntDesign Blazor的入门教程,分享了如何创建和操作Modal表单和Table。首先,介绍了在Weather.razor文件中添加新增按钮和Modal组件,设置属性并绑定事件。接着,创建WeatherForm组件,包含日期、温度和摘要字段,并在WeatherForecast模型类中添加必填特性。然后,实现了Modal表单的数据编辑功能,包括绑定Model对象、保存数据的事件处理。最后,教程展示了如何在Table中添加操作列,实现编辑和删除功能,并在WeatherService类中添加相应的方法。教程还提供了示例代码仓库链接和视频教程,方便学习和参考。

主题

宣布 .NET Aspire 预览版 2 - .NET 博客

https://devblogs.microsoft.com/dotnet/announcing-dotnet-aspire-preview-2/

.NET Aspire Preview 2 已发布。

它包括各种改进,例如改进仪表板的各种视图、改进运行容器时的配置自定义以及添加 MySQL 和 MongoDB 等组件。

OpenSilver 2.0 简介 - OpenSilver

https://opensilver.net/announcements/2-0/

OpenSilver 2.0 已经发布。

此版本包括对 Visual Basic .NET 的支持、与 Blazor、React 和 Angular 的集成、设计时实时 XAML 预览、对高级布局系统的支持、更快的基于 WebView 的模拟器以及各种性能改进。

发布 WCF 8.0 · dotnet/wcf

https://github.com/dotnet/wcf/releases/tag/v8.0.0-rtm

WCF客户端8.0已发布。

除了删除了接收配置名称的 API 之外,此版本没有重大变化,但客户端支持政策是自发布之日起提供三年支持(直至 2026 年)。对先前版本 WCF Client 6.0 的支持将于 2024 年 11 月结束。

版本 2.3.3 · mysql-net/MySqlConnector

https://github.com/mysql-net/MySqlConnector/releases/tag/2.3.3

MySQL Connector for .NET 2.3.3/2.3.2 已发布。

此版本包括改进的架构支持和多个错误修复。

ReSharper 和 Rider 2023.3.2 – 错误修复已落地! | .NET 工具博客

https://blog.jetbrains.com/dotnet/2023/12/20/resharper-and-rider-2023-3-2-bug-fix/

ReSharper / Rider 2023.3.2 已发布。

此版本修复了自 2023.3 版本以来 ReSharper 和 Rider 中的各种错误。

Win11 主题的 WPFUI 协作 · dotnet/wpf · 讨论 #8533 · GitHub

https://github.com/dotnet/wpf/discussions/8533

宣布与 WPFUI 合作将 Windows 11 Fluent UI 主题引入 WPF。

我们的代码覆盖率工具有哪些新增功能? - .NET 博客

https://devblogs.microsoft.com/dotnet/whats-new-in-our-code-coverage-tooling/

关于代码覆盖率工具(Microsoft.CodeCoverage)的新功能。

支持具有静态检测的所有平台、支持新的报告格式、引入 dotnet-coverage 命令、自动解决方案合并、性能改进等等。

2023 年降临节日历