C# 多线程(2)——线程同步

目录

    • 1 线程不安全
    • 2 线程同步方式
      • 2.1 简单的阻塞方法
      • 2.2 锁
        • 2.2.1 Lock使用
        • 2.2.2 互斥体Mutex
        • 2.2.3 信号量Semaphore
        • 2.2.3 轻量级信号量SemaphoreSlim
        • 2.2.4 读写锁ReaderWriterLockSlim
      • 2.3 信号同步
        • 2.3.1 AutoResetEvent
          • 2.3.1.1 AutoResetEvent实现双向信号
        • 2.3.2 ManualResetEvent
        • 2.3.3 CountdownEvent
      • 2.3 原子操作

1 线程不安全

class ThreadTest
{bool done;static void Main(){ThreadTest tt = new ThreadTest();   // 创建一个公共的实例new Thread (tt.Go).Start();tt.Go();}// 注意: Go现在是一个实例方法void Go(){if (!done) { Console.WriteLine ("Done"); done = true; }}
}

这个代码示例可能会输出两个Done ,也有可能输出一个Done。
这个问题是因为一个线程对if中的语句估值的时候,另一个线程正在执行WriteLine语句,这时done还没有被设置为true。所以程序的输出结果是不确定的。显然这在实际中开发是允许的。
当多个线程共享资源时,就会因为线程调度的不确定性导致线程不安全问题(即线程的执行没有正确的同步)。

修复这个问题需要在读写公共字段时,获得一个排它锁(互斥锁,exclusive lock )。C# 提供了lock来达到这个目的:

class ThreadSafe
{static bool done;static readonly object locker = new object();static void Main(){new Thread (Go).Start();Go();}static void Go(){lock (locker){if (!done) { Console.WriteLine ("Done"); done = true; }}}
}

两个线程同时争夺一个锁的时候(例子中的locker),一个线程等待,或者说阻塞(释放cpu时间片),直到锁变为可用。这样就确保了在同一时刻只有一个线程能进入临界区(critical section,不允许并发执行的代码),所以 “ Done “ 只被打印了一次。像这种用来避免在多线程下的不确定性的方式被称为 线程安全(thread-safe)。根据上述分析可知,保证线程安全的方式其实就是 对共享对象的操作能够以正确的顺序执行,通常被称作为线程同步

2 线程同步方式

线程不安全的问题发生的主要原因是因为多个线程竞争共享的资源,导致问题发生的原因是多线程的执行并没有正确同步

当在同一时刻多个线程操作共享资源时就会导致数据的错误,但是如果在单一线程中按照顺序就不出现这样的问题,这也就引申出线程同步的内容,保证多个线程提升性能的前提下,也不会出现程式数据的错误,重点就是让多个线程按照一定的顺序同步的执行代码,就是线程同步的概念。

2.1 简单的阻塞方法

这些方法会使当前线程等待另一个线程结束或是自己等待一段时间。Sleep、Join与Task.Wait都是简单的阻塞方法。
使用上述阻塞方法后,处于阻塞状态,让出了CPU时间片。此时线程调度器会保存等待线程的状态,并切换到另一个线程,直到等待的线程重新获得CPU时间片。

这种模式下, 由于阻塞可以让线程按照一定的顺序执行代码,但是这也意味着至少会引入一次上下文切换,一定程度上耗费了资源。通常建议,当线程被挂起很长时间时,这种阻塞是值得的。

若线程只需要等待一小段时间,最好只是简单的等待,而不用将线程切换到阻塞状态。虽然线程等待会耗费CPU 时间,但是我们节省了上下文切换的CPU时间和资源。这种方式非常轻量,速度很快。
比如while(flag)

2.2 锁

锁构造能够限制每次可以执行某些动作或是执行某段代码的线程数量。排它锁构造是最常见的,它每次只允许一个线程执行,从而可以使得参与竞争的线程在访问公共数据时不会彼此干扰。标准的排它锁构造是lock(一种语法糖,本质上是调用Monitor.Enter/Monitor.Exit方法)、Mutex与 SpinLock(自旋锁)。非排它锁构造是Semaphore、SemaphoreSlim以及读写锁。

在这里插入图片描述

2.2.1 Lock使用
class ThreadSafe
{static readonly object _locker = new object();static int _val1, _val2;static void Go(){lock (_locker){if (_val2 != 0) Console.WriteLine (_val1 / _val2);_val2 = 0;}}
}

lock关键字在C# 4.0编译器产生的代码为

bool lockTaken = false;
try
{Monitor.Enter (_locker, ref lockTaken);// 你的代码...
}
finally { if (lockTaken) Monitor.Exit (_locker); }

lock 排它锁的使用,确保了多个线程在访问竞态代码块时,只有一个线程是获得CPU时间片的,其他的线程处于阻塞中,并处于一个等待队列中。直到锁被释放,等待的线程属于先到先得的情形,依次等待获得锁去执行竞态代码块,保证了线程同步,因此可以保证线程的安全。

2.2.2 互斥体Mutex
    /// <summary>/// Mutex是一种原始同步的操作/// 互斥量 只有一个线程能持有这个互斥量,并阻塞其他线程/// 相较于lock关键字而言,虽然都能够构建同步代码/// 其中lock更快,使用也更方便。而Mutex的优势是它可以跨进程的使用。/// </summary>public class MutexWork{Mutex mut = new Mutex();public void Method3(object threadId) {// 命名的 Mutex 是进程范围的,它的名称需要是唯一的string mutexName = "Foxconn168!";//为了正确的关闭锁,通常使用using代码块来包围互斥体锁using (var mutex = new Mutex(false, mutexName)){// 使用mutex.WaitOne()方法来获得锁// 可能其它程序实例正在关闭,所以可以等待几秒来让其它实例完成关闭if (!mutex.WaitOne(TimeSpan.FromSeconds(3), false)){Console.WriteLine("Another app{0} instance is running. Bye!",threadId);Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));return;}RunProgram(threadId);}}public void RunProgram(object threadId) {Console.WriteLine("Running {0}. Press Enter to exit",threadId);Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));Console.ReadLine();}}
    static void Main(string[] args) {MutexWork work = new MutexWork();//使用ParameterizedThreadStart来传递参数时,需要保证方法参数类型为object,参数有且仅有一个Thread t1 = new Thread(work.Method3);Thread t2 = new Thread(work.Method3);t1.Start(1);t2.Start(2);}

在这里插入图片描述

这里使用两个线程来演示互斥体的用法。线程1获得mutex锁后,并执行RunProgram方法,需要等待控制台输入空格符。线程2在用户输入空格符前,等待3s以获得mutex锁,当没有获得锁后输出Another app2 instance is running. Bye!

2.2.3 信号量Semaphore

Semaphore限制了同时访问同一个资源的线程数量,信号量在有限并发的需求中有用,它可以阻止过多的线程同时执行特定的代码段。通过协调各个线程,以保证合理的使用资源。
可以用上厕所的行为来类比Semaphore。一个厕所的容量是一定的。一旦满员,就不允许其他人进入,其他人将在外面排队。当有一个人离开时,排在最前头的人便可以进入。

public  class SeamphoreWork{//定义信号量,总容量为3,同时允许最多3个线程访问资源//使用 Semaphore(int initialCount, int maximumCount, string name)构造函数初始化信号量//initialCount 初始空闲容量 maximumCount 最大容量 name 信号量名称Semaphore seamphore = new Semaphore(1,3, "Semaphore_One");/// <summary>/// 模拟上厕所/// </summary>public void EnterToilet(int threadId,int waitTime) {Console.OutputEncoding = Encoding.Unicode;Console.WriteLine("{0} wants to enter",threadId);seamphore.WaitOne(); //线程调用WaitOne,信号空闲容量计数减一。当容量为零时,后续请求会阻塞,直到其他线程释放信号灯。Console.WriteLine("{0} has entered the Toilet {1}",threadId,DateTime.Now.ToString("yyyy-mm-dd HH:mm:ss"));Thread.Sleep(waitTime); //线程阻塞模拟上厕所的时耗费的时间seamphore.Release(); //释放信号量,可用容量增加一Console.WriteLine("{0} has left the Toilet {1}", threadId, DateTime.Now.ToString("yyyy-mm-dd HH:mm:ss"));}}
static void Main(string[] args){SeamphoreWork seamphore = new SeamphoreWork();for (int i = 0; i < 5; i++){int tempName = i;int waitTime = (i + 1) * 1000;Thread t = new Thread(() => seamphore.EnterToilet(tempName, waitTime));t.Start();}}

在这里插入图片描述

容量为 1 的信号量与Mutex和lock类似,所不同的是信号量没有“所有者”,它是线程无关(thread-agnostic)的。任何线程都可以在调用Semaphore上的Release方法,而对于Mutex和lock,只有获得锁的线程才可以释放。类似于Mutex,命名的Semaphore也可以跨进程使用

2.2.3 轻量级信号量SemaphoreSlim

SemaphoreSlim是 Framework 4.0 加入的轻量级的信号量,功能与Semaphore相似,不同之处是它对于并行编程的低延迟需求做了优化。在Semaphore上调用WaitOne或Release会产生大概 1 微秒的开销,而SemaphoreSlim产生的开销约是其四分之一。但它不能跨进程使用。

   public class SeamaphoreSlimWork{//定义信号量,总容量为3,同时允许3个线程访问资源SemaphoreSlim seamphore = new SemaphoreSlim(3);/// <summary>/// 模拟上厕所/// </summary>public void EnterToilet(int threadId, int waitTime){Console.WriteLine("{0} wants to enter", threadId);seamphore.Wait(); //进入信号量,有效容量减一Console.WriteLine("{0} has entered the Toilet {1}", threadId, DateTime.Now.ToString("yyyy-mm-dd HH:mm:ss"));Thread.Sleep(waitTime); seamphore.Release(); //释放信号量,有效容量加一Console.WriteLine("{0} has left the Toilet {1}", threadId, DateTime.Now.ToString("yyyy-mm-dd HH:mm:ss"));}}
2.2.4 读写锁ReaderWriterLockSlim

通常,一个类型的实例对于并发读操作是线程安全的,但对并发的更新操作却不是(并发读然后更新也不是)。尽管可以简单的对所有访问都使用排它锁来确保这种类型的实例是线程安全的,但对于有很多读操作而只有少量更新操作的情况,它就会过度限制并发能力。如浏览淘宝APP,更多的用户是在进行读操作而不是写操作。在这种情况下, R e a d e r W r i t e r L o c k S l i m \textcolor{red}{ReaderWriterLockSlim} ReaderWriterLockSlim类被设计用来提供高可用性的锁。

这个类有两种基本类型的锁,读锁和写锁:

  • 写锁完全的排它。
  • 读锁可以与其它的读锁相容。
    所以,一个线程持有写锁会阻塞其它想要获取读锁或写锁的线程,如果没有线程持有写锁,任意数量的线程可以同时获取读锁。

ReaderWriterLockSlim定义了如下的方法来获取和释放读 / 写锁:

public void EnterReadLock();
public void ExitReadLock();
public void EnterWriteLock();
public void ExitWriteLock();
     /// <summary>/// ReaderWriterLockSlim 写锁阻塞所有的读写锁,在不持有写锁的情况下,所有的线程都可以持有读锁去写数据/// </summary>/// <param name="args"></param>static void Main(string[] args){Console.OutputEncoding = Encoding.Unicode;Random _rand = new Random();List<int> list = new List<int>();ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();//读写锁//读数据void Read() {while (true){Console.WriteLine(_rw.CurrentReadCount + " concurrent readers");_rw.EnterReadLock();foreach (int i in list) Thread.Sleep(10);_rw.ExitReadLock();}}//写数据void Write(object threadID) {while (true){int newNumber = GetRandNum(100);_rw.EnterWriteLock();list.Add(newNumber);_rw.ExitWriteLock();Console.WriteLine("Thread " + threadID + " added " + newNumber);Thread.Sleep(100);}}int GetRandNum(int max) { lock (_rand) return _rand.Next(max); }//3个线程读数据 ,2 个线程写数据(读线程和写线程均是后台线程)new Thread(Read) { IsBackground=true}.Start();new Thread(Read) { IsBackground = true }.Start();new Thread(Read) { IsBackground = true }.Start();new Thread(Write) { IsBackground = true }.Start("A");new Thread(Write) { IsBackground = true }.Start("B");//主线程休眠30sThread.Sleep(TimeSpan.FromSeconds(30));}

通常需要添加try / finally块来确保抛出异常时锁能够被释放。

2.3 信号同步

信号同步就是一个线程进行等待,直到它收到其它线程的通知的过程。它们有三个成员:AutoResetEventManualResetEvent以及CountdownEvent( Framework 4.0 中加入)。前两个的功能基本都是在它们的基类EventWaitHand

2.3.1 AutoResetEvent

AutoResetEvent就像验票闸机:插入一张票,就只允许一个人通过。多个用户(线程)等待闸机开放时,会阻塞等待。待人通过后,闸机会自动关闭。直到下一个人插入票。
在这个用户(线程)等待的过程,收到了另一个用户(线程)插入票的信号,阻塞态变为运行态。

在闸机处调用 W a i t O n e \textcolor{red}{WaitOne} WaitOne方法,等待这个闸机打开,线程就会进入等待或者说阻塞。如果有多个线程调用WaitOne,便会在闸机前排队(与锁同样,由于操作系统的差异,这个等待队列的先入先出顺序有时可能被破坏)。
票的插入则通过调用 S e t \textcolor{red}{Set} Set方法。票可以来自任意线程,换句话说,任何能够访问这个AutoResetEvent对象的(非阻塞)线程都可以调用Set方法来放行一个被阻塞的线程。

在接下来的例子中,一个线程开始等待直到收到另一个线程的信号。

        static void Main(string[] args){AutoResetEvent autoResetEvent = new AutoResetEvent(false);Console.OutputEncoding = Encoding.Unicode;//等待事件void Waiter(int threadId){Console.WriteLine("{0} Waiting...",threadId);autoResetEvent.WaitOne();                // 等待通知Console.WriteLine("{0} Notified", threadId);}Thread t1 = new Thread(()=>Waiter(1));t1.Start();Thread.Sleep(5000);//主线程休眠5sConsole.WriteLine("主线程发出唤醒信号");//主线程发出信号,唤醒t1线程autoResetEvent.Set();}

在这里插入图片描述

2.3.1.1 AutoResetEvent实现双向信号
        /// <summary>/// 定义两个AutoResetEvent实例,其中一个是工作线程向主线程发信号,另一个实例是从主线程向工作线程发限号。/// </summary>/// <param name="args"></param>static void Main(string[] args){//主线程信号句柄,初始化等待工作线程AutoResetEvent mainThreadSignal = new AutoResetEvent(false);//工作线程句柄AutoResetEvent workThreadSignal = new AutoResetEvent(false);Console.OutputEncoding = Encoding.Unicode;Thread t1 = new Thread(Process);t1.Start();void Process() {Console.WriteLine("工作线程准备中");Thread.Sleep(5_000);  //模拟工作线程准备工作mainThreadSignal.Set();  //通知主线程,工作线程已准备完毕workThreadSignal.WaitOne();Console.WriteLine("我是工作线程,我要处理工作业务了");Thread.Sleep(5_000); //模拟工作线程处理业务}Console.WriteLine("主线程等待工作线程准备中");mainThreadSignal.WaitOne();//主线程先等待Console.WriteLine("工作线程准备完毕,主线程通知工作线程去完成任务");workThreadSignal.Set(); //唤醒工作线程}
2.3.2 ManualResetEvent

ManualResetEvent就像一个普通的门。调用 S e t \textcolor{red}{Set} Set 方法打开门,允许任意数量的线程调用 W a i t O n e \textcolor{red}{WaitOne} WaitOne方法来通过。调用 R e s e t \textcolor{red}{Reset} Reset方法关闭门。如果线程在一个关闭的门上调用WaitOne方法将会被阻塞,当门下次打开时,会被立即放行。除这些不同以外,ManualResetEvent就和AutoResetEvent差不多了。

M a n u a l R e s e t E v e n t 在需要让一个线程解除其它多个线程的阻塞时有用。 \textcolor{blue}{ManualResetEvent在需要让一个线程解除其它多个线程的阻塞时有用。} ManualResetEvent在需要让一个线程解除其它多个线程的阻塞时有用。

        /// <summary>/// 一个线程解除其它多个线程的阻塞态/// </summary>/// <param name="args"></param>static void Main(string[] args){//ManualResetEvent(bool initialState)//初始态 门是关闭的ManualResetEvent signal = new ManualResetEvent(false);void EnterGate() {string name = Thread.CurrentThread.Name;Console.WriteLine(name + " starts and calls mre.WaitOne()");signal.WaitOne();Console.WriteLine(name + " ends.");}for (int i = 0; i < 3; i++) {Thread t = new Thread(EnterGate);t.Name = $"Thread_{0}";t.Start();}Thread.Sleep(2_000);//唤醒所有阻塞中的线程signal.Set();}
2.3.3 CountdownEvent

与ManualResetEvent让一个线程解除其它多个线程相反,CountdownEvent 可以让你等待 n 个线程,直到n个线程均发出信号后,解除等待线程的阻塞态。与Java多线程中的CountDownLatch功能类似。

/// <summary>/// 等待多个线程/// </summary>/// <param name="args"></param>static void Main(string[] args){Console.OutputEncoding = Encoding.Unicode;CountdownEvent countdownEvent = new CountdownEvent(3);void DoWork() {Thread.Sleep(2_000);//模拟单个线程执行任务的时间countdownEvent.Signal();}for (int i = 0; i < 3; i++) {new Thread(DoWork).Start();}countdownEvent.Wait();//主线程等待Console.WriteLine("所有的工作线程发出信号后执行");}

值得注意的是,如果调用Signal()没有达到指定的次数,那么Wait()将会一直等待。所有请确保使用CountDownEvent时,所有的线程完成后都要调用Signal()方法。

2.3 原子操作

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何的线程切换。在c#中提供了对int类型读写的原子操作类 I n t e r l o c k e d \textcolor{red}{Interlocked} Interlocked

       /// <summary>/// 提供了Interlocked类来实现原子操作,其方法有Add、Increment、Decrement、Exchange、CompareExchange等,/// 可以使用原子操作进行加法、加一、减一、替换、比较替换等操作/// </summary>/// <param name="args"></param>static void Main(string[] args){//初始值int a = 0;int b = 0;//+1 a++void Increment() {for (int i = 0; i < 20000; i++) {a++;}}//原子性+1void IncrementAtomic(){for (int i = 0; i < 20000; i++){Interlocked.Increment(ref b);}}CountdownEvent countdown = new CountdownEvent(10);for (int i = 0; i < 5; i++) {new Thread(Increment).Start();countdown.Signal();}for (int i = 0; i < 5; i++){new Thread(IncrementAtomic).Start();countdown.Signal();}countdown.Wait();Console.WriteLine(a);Console.WriteLine(b);}

在这里插入图片描述

a++ 是线程不安全的操作,因为是非原子性的。在底层系统执行这个加一操作时分为3个步骤:
(1)从内存中将该变量加载带CPU寄存器中
(2)CPU对该变量进行加一操作
(3)将该变量从CPU寄存器返回内存中
在多线程同时操作a++操作时,会因为线程不同步的问题而造成线程不安全的问题
Interlocked类会将上述步骤合成一个动作,在没有执行完成的时候不会进行线程上下文的切换,所以保证了线程的安全。

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

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

相关文章

麒麟系统—— openKylin 安装 Nginx

麒麟系统—— openKylin 安装 Nginx 一、准备工作1. 确保麒麟系统 openKylin 已经安装完毕。 二、下载 nginx三、解压与运行解压检查与编译安装编译运行 四、配置加入到服务中加入环境变量nginx 配置文件 五、常用命令 Nginx 是一款高性能的 HTTP 和反向代理服务器&#xff0c…

如何访问 Oracle OKE 集群

OKE是Oracle Cloud提供的托管Kubernetes服务&#xff0c;为用户提供强大而灵活的容器编排平台。在本文中&#xff0c;我们将详细介绍如何有效地与OKE集群进行交互&#xff0c;包括访问集群的不同方式、管理访问权限以及执行常见操作的步骤。 1 安装oci命令 1.1 在Oracle Linux…

【数据库】mysql触发器使用

题目&#xff1a; 创建职工表以及职工工资表职工表字段&#xff1a;工号&#xff0c;姓名&#xff0c;性别&#xff0c;年龄工资表字段&#xff1a;编号自增&#xff0c;职工工号&#xff0c;基础工资10000通过触发器实现&#xff1a;对职工进行添加时 工资表中也要体现当前职…

解说 AIGC(人工智能生成内容) 是什么?

前言 AIGC (AI Generated Content)即人工智能生成内容,一般认为是相对于PCG(专业生成内容)、UCG(用户生成内容)而提出的概念。AIGC狭义概念是利用AI自动生成内容的生产方式。广义的AIGC可以看作是像人类一样具备生成创造能力的AI技术,即生成式AI,它可以基于训练数据和生成算法…

数据可视化 pycharts实现中国各省市地图数据可视化

自用版 数据格式如下&#xff1a; 运行效果如下&#xff1a; import pandas as pd from pyecharts.charts import Map, TreeMap, Timeline, Page, WordCloud from pyecharts import options as opts from pyecharts.commons.utils import JsCode from pyecharts.globals im…

THREE.JS动态场景开发实战【赛博朋克】

在本教程中&#xff0c;我们将探索如何创建类似 Three.js 的赛博朋克场景&#xff0c;灵感来自 Pipe 网站上的背景动画。 我们将指导你完成使用 Three.js 编码动态场景的过程&#xff0c;包括后处理效果和动态光照&#xff0c;所有这些都不需要任何着色器专业知识。 我用这个场…

3D词云图

工具库 tagcanvas.min.js vue3&#xff08;框架其实无所谓&#xff0c;都可以&#xff09; 实现 <script setup> import { onMounted, ref } from vue; import ./tagcanvas.min.js;const updateFlag ref(false);// 词云图初始化 const initWordCloud () > {let …

IP定位如何进行业务风控反欺诈

IP地址作为接入互联网的唯一标识&#xff0c;分析其归属地及网络类型等多维度信息&#xff0c;帮助识别虚假流量和欺诈账号&#xff0c;保障账号和交易安全&#xff0c;帮助企业持续优化风控与反欺诈模型&#xff0c;降低经济损失。 交易聚集分析 通过IP地址数据服务得到的交易…

【EI会议征稿中|ACM出版】#先投稿,先送审#第三届网络安全、人工智能与数字经济国际学术会议(CSAIDE 2024)​

#先投稿&#xff0c;先送审#ACM出版#第三届网络安全、人工智能与数字经济国际学术会议&#xff08;CSAIDE 2024&#xff09; 2024 3rd International Conference on Cyber Security, Artificial Intelligence and Digital Economy 2024年3月8日-10日 | 中国济南 会议官网&…

四通道 LVDS 差分线缆接收器MS21148T

产品简述 MS21148T 是一款四路低压差分信号 (LVDS) 线路接收器。在 输入共模电压范围内&#xff0c;每一路差分接收器都可以把 100mV 的差 分输入电压转换成有效的逻辑输出。 该芯片可应用于约 100Ω 的 受控阻抗介质上进行点对点基带数据传输。传输介质可以是印 刷电…

Git系列---远程操作

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 引用 1.理解分布式版本控制…

如何使用docker部署Swagger Editor并实现无公网ip远程协作编辑文档

文章目录 Swagger Editor本地接口文档公网远程访问1. 部署Swagger Editor2. Linux安装Cpolar3. 配置Swagger Editor公网地址4. 远程访问Swagger Editor5. 固定Swagger Editor公网地址 Swagger Editor本地接口文档公网远程访问 Swagger Editor是一个用于编写OpenAPI规范的开源编…

lombok导致的IndexOutOfBoundsException

一、问题描述 ERROR 25152 --- [1.190-81-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.mybatis.spring.MyBatisSyste…

Python处理图片生成天际线(2024.1.29)

1、天际线简介 天际线&#xff08;SkyLine&#xff09;顾名思义就是天空与地面的边界线&#xff0c;人站在不同的高度&#xff0c;会看到不同的景色和地平线&#xff0c;天空与地面建筑物分离的标记线&#xff0c;不得不说&#xff0c;每天抬头仰望天空&#xff0c;相信大家都可…

python实现贪吃蛇小游戏(附源码)

文章目录 导入所需的模块坐标主游戏循环模块得分 贪吃蛇小游戏&#xff0c;那个曾经陪伴着00后和90后度过无数欢笑时光的熟悉身影&#xff0c;仿佛是一把打开时光之门的钥匙。它不仅是游戏世界的经典之一&#xff0c;更是我们童年岁月中不可或缺的一部分&#xff0c;一个承载回…

CIFAR-10数据集详析:使用卷积神经网络训练图像分类模型

1.数据集介绍 CIFAR-10 数据集由 10 个类的 60000 张 32x32 彩色图像组成&#xff0c;每类 6000 张图像。有 50000 张训练图像和 10000 张测试图像。 数据集分为5个训练批次和1个测试批次&#xff0c;每个批次有10000张图像。测试批次正好包含从每个类中随机选择的 1000 张图像…

Android平台如何实现RTSP转GB28181

为什么要做GB28181设备接入侧&#xff1f; 实际上&#xff0c;在做Android平台GB28181设备接入模块的时候&#xff0c;我们已经有了非常好的技术积累&#xff0c;比如RTMP推送、轻量级RTSP服务、一对一互动模块、业内几乎最好的RTMP|RTSP低延迟播放器。 Android平台GB28181接…

VBA技术资料MF113:将文件夹图像添加到PowerPoint

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套&#xff0c;分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的入门&#xff0c;到…

第七讲_JavaScript的Iterator和Generator

JavaScript的Iterator和Generator 1. Iterator1.2 for-of语法糖 2. Generator2.1 定义一个生成器函数2.2 常用的方法2.3 基本用法2.4 传参的用法2.5 异步的用法 1. Iterator ES6 中&#xff0c;默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性。一个数据结构只要拥…

vue前端html导出pdf

package.json中添加依赖 调用方&#xff1a; import htmlToPdf from ../../../utils/file/htmlToPdf.js// 下载方法&#xff0c;pdfDownloadDpi为onClickDownLoad() {htmlToPdf.getPdf(标题1, jsfgyzcpgxmShow, this.pdfDownloadDpi)}htmlToPdf.js // 页面导出为pdf格式 imp…