C#并行编程(6):线程同步面面观

理解线程同步

线程的数据访问

在并行(多线程)环境中,不可避免地会存在多个线程同时访问某个数据的情况。多个线程对共享数据的访问有下面3种情形:

  1. 多个线程同时读取数据;

  2. 单个线程更新数据,此时其他线程读取数据;

  3. 多个线程同时更新数据。

显而易见,多个线程同时读取数据是不会产生任何问题的。仅有一个线程更新数据的时候,貌似也没有问题,但真的没有问题吗?多个线程同时更新数据,很明显,你可能把我的更改覆盖掉了,数据从此不再可信。

什么是线程同步

为了解决多线程同时访问共享数据可能导致数据被破坏的问题,我们需要采取一些措施来保证数据的一致性,让每个线程都能准确地读取或更新数据。

问题的根源在于多个线程同时访问数据,那么只要我们保证同一时间只有一个线程访问数据,就能解决问题。保证同一时间只有一个线程访问数据的处理,就是线程同步了。我在访问数据的时候,你们都先等着,我完事了你们再来。

C#中的线程同步

.NET提供了很多线程同步的方式,这些方式分为用户模式和内核模式以及混合模式(即用户模式与内核模式的结合),下面会总结C#/.NET中各模式下的线程同步。

用户模式与内核模式

Windows操作系统下,CPU跟据所执行代码的不同,会在两种模式下进行切换。CPU执行应用程序代码(如我们开发的.NET程序)时,一般运行在用户模式下;执行操作系统核心代码(内核函数或者某些设备驱动程序)时,CPU则切换到内核模式。

用户模式的代码只能访问自身进程的专有地址空间,代码异常不会影响到其他程序或者操作系统;内核模式的所有代码共享单个地址空间,代码异常将可能导致系统崩溃。CPU的模式切换,是为了保证应用程序和操作系统的稳定性。

应用程序中,线程可以通过Windows API调用操作系统内核函数,这时候执行线程的CPU将从用户模式切换到内核模式,执行完操作系统函数后,再由内核模式切换到用户模式。CPU的模式切换是很耗时的,据《Windows核心编程》中的描述,CPU模式的切换,要占用1000个以上的CPU周期。因此,在我们的.NET程序中,应该尽可能地避免CPU的模式切换。

用户模式线程同步

用户模式下,利用特殊的CPU指令来协调线程,使同一时间只有一个线程能访问某内存地址,这种协调在硬件中发生,速度很快。这种模式下,CPU指令对线程的阻塞很短暂,操作系统调度线程时不会认为该线程已被阻塞,这种情况下,线程池不会创建新的线程来替换该线程。

用户模式下,等待资源的线程会一直被操作系统调度,导致线程的“自旋”并因此浪费很多的CPU资源。如果某线程一直占着资源不释放,等待该资源的线程将一直处于自旋状态,这样就造成了“活锁”,活锁除了浪费内存外,还会浪费大量CPU。

.NET提供两种用户模式的线程同步,volatileinterlocked,即易变和互锁。

volatile关键字和Volatile

上面我们遗留了一个问题:只有一个线程更新数据,其他线程读取数据,会不会出现问题?先看一个例子:

private static bool _stop;
public static void Run()
{
Task.Run(() =>
{
int number = 1;
while (!_stop) 
{
number++;
}
Console.WriteLine($"increase stopped,value = {number}");
});

Thread.Sleep(1000);
_stop = true;
}

编译器和CPU会对上面的代码进行优化(调试模式不会优化),任务线程在执行时,会把_stop读取到CPU寄存器中,while循环的时候,每次都从当前CPU寄存器中读取_stop;同样,主线程执行的时候CPU也会把_stop读取到寄存器,更新_stop时,先更新是CPU寄存器中的_stop值,再把值存到变量_stop;在并行环境中,主线程和任务线程独立执行,主线程对_stop的更新并不会公开到任务线程,这样,任务线程的while循环便不会停止,永远无法得到输出。

把变量读到寄存器只是CPU优化代码的一种方式,CPU还可能调整代码的执行顺序,当前,CPU任务这种调整不会改变代码的意图。上面的代码说明,由于编译器和CPU的优化,只有一个线程更新数据,也可能存在问题

这种情况,我们可以使用volatile关键字或者类System.Threading.Volatile来阻止编译器和CPU的优化,这种阻止利用的是内存屏障MemoryBarrier,它告诉CPU在执行完屏障之前的内存存取后才能执行屏障后面的内存存取。上面代码的问题在于,while循环读取到的值总是CPU寄存器中的false。我们把while循环的条件改成!Volatile.Read(ref _stop)或者把用volatile声明变量_stop,while条件直接读取内存中的值,问题就能得到解决。

Interlocked原子访问

.NET提供的另一种用户模式线程同步方式是System.Threading.InterlockedInterlocked的工作依赖于代码运行的CPU平台,如果是X86的CPU,Interlocked函数会在总线上维持一个硬件信号,来阻止其他CPU访问同一内存地址(《Windows核心编程第五版》)。计算机对变量的修改一般来说并不是原子性的,而是分为3个步骤:

  1. 将变量值加载到CPU寄存器

  2. 改变值

  3. 将更新后的值存储到内存中

假如执行了前两个步骤后,CPU被抢占,变量在之前线程中的修改将丢失。Interlocked函数保证对值的修改是原子性的,一个线程完成变量的修改和存储后,另一个线程才能修改变量

System.Threading.Interlocked提供了很多方法,例如递增、递减、求和等,下面用Interlocked的递增方法展示其线程同步功能。

public static void Run()
{
DoIncrease(100000);
}

private static void DoIncrease(int incrementPerThread)
{
int number1 = 0;
int number2 = 0;

Console.WriteLine($"use two threads to increase zero. each thread increase {incrementPerThread}.");

IList<Task> increaseTasks = new List<Task>();

increaseTasks.Add(Task.Run(() =>
{
Console.WriteLine($"thread #{Thread.CurrentThread.ManagedThreadId} increasing number1.");
for (int i = 0; i < incrementPerThread; i++)
{
Interlocked.Increment(ref number1);
}
}));
increaseTasks.Add(Task.Run(() =>
{
Console.WriteLine($"thread #{Thread.CurrentThread.ManagedThreadId} increasing number1.");
for (int i = 0; i < incrementPerThread; i++)
{
Interlocked.Increment(ref number1);
}
}));
increaseTasks.Add(Task.Run(() =>
{
Console.WriteLine($"thread #{Thread.CurrentThread.ManagedThreadId} increasing number2.");
for (int i = 0; i < incrementPerThread; i++)
{
number2++;
}
}));
increaseTasks.Add(Task.Run(() =>
{
Console.WriteLine($"thread #{Thread.CurrentThread.ManagedThreadId} increasing number2.");
for (int i = 0; i < incrementPerThread; i++)
{
number2++;
}
}));

Task.WaitAll(increaseTasks.ToArray());

Console.WriteLine($"use interlocked: number1 result = {number1}");
Console.WriteLine($"normal increase: number2 result = {number2}");
}

运行上面的代码多次(每个线程增加的数量尽量大,否则不容易体现结果),每次number1的结果都一样,number2的结果都不同,足以体现Interlocked的线程同步功能。

SpinLock自旋锁

System.Threading.SpinLock是基于InterLocked和SpinWait实现的轻量级自旋锁,具体的实现方式这里不去关心。SpinLock的简单用法如下:

private static SpinLock _spinlock = new SpinLock();
public static void DoWork()
{
bool lockTaken = false;
try
{
_spinlock.Enter(ref lockTaken);

}
finally
{
if (lockTaken)
{
_spinlock.Exit(false);
}
}
}

SpinLock很轻量级,性能较高,但由于是自旋锁,锁定的操作应该是很快完成,否则会因线程自旋而浪费CPU。

内核模式线程同步

除了用户模式的两种线程同步方式,我们还会利用Windows系统的内核对象实现线程的同步。使用系统内核对象将会导致执行线程的CPU运行模式的切换,这会有很大的消耗,所以能够使用用户模式的线程同步就尽量避免使用内核模式。

内核模式下,线程在等待资源时会被系统阻塞,避免了CPU的浪费,这是内核模式优势。假如线程等待的资源一直被占用则线程将一直处于阻塞状态,造成“死锁”。相对于活锁,死锁只会浪费内存资源。

我们使用系统内核中的事件、信号量和互斥量进行内核模式的线程同步。

利用内核事件实现线程同步

事件实际上是由系统内核维护的一个布尔值。

.NET提供System.Threading.EventWaitHandle进行线程的信号交互。EventWaitHandle继承WaitHandle(封装等待对共享资源独占访问的操作系统特定的对象),有三个关键方法:

  • Set():将事件状态设置为终止状态,允许一个或多个等待线程继续。

  • Reset():将事件状态设置为非终止状态,导致线程阻塞

  • WaitOne():阻塞线程直到收到事件状态信号

线程交互事件有自动重置和手动重置两种类型,分别由AutoResetEventManualResetEvent继承EventWaitHandle得到。自动重置事件在Set唤醒第一个阻塞线程之后,会自动Reset事件,其他阻塞线程仍保持阻塞状态;而手动重置事件Set时,会唤醒所有被该事件阻塞的线程,手动Reset后,事件才会继续起作用。手动重置事件的这种性质,导致它不能用于线程同步,因为不能保证同一时间只有一个线程访问资源;相反,自动重置时间则很适合用来处理线程同步。

下面的例子演示了利用自动重置时间进行的线程同步。

public static void Run()
{
DoIncrease(100000);
}

private static void DoIncrease(int incrementPerThread)
{
int number = 0;
Console.WriteLine($"use two threads to increase zero. each thread increase {incrementPerThread}.");

AutoResetEvent are = new AutoResetEvent(true);

IList<Task> increaseTasks = new List<Task>();
increaseTasks.Add(Task.Run(() =>
{
Console.WriteLine($"thread #{Thread.CurrentThread.ManagedThreadId} is increasing the number.");
for (int i = 0; i < incrementPerThread; i++)
{
are.WaitOne();
number++;
are.Set();
}
}));
increaseTasks.Add(Task.Run(() =>
{
Console.WriteLine($"thread #{Thread.CurrentThread.ManagedThreadId} is increasing the number.");
for (int i = 0; i < incrementPerThread; i++)
{
are.WaitOne();
number++;
are.Set();
}
}));

Task.WaitAll(increaseTasks.ToArray());
are.Dispose();
Console.WriteLine($"use AutoResetEvent: result = {number}");
}

利用信号量进行线程同步

信号量是系统内核维护的一个整型变量。

信号量值为0时,所有等待信号量的线程会被阻塞;信号量值大于零0,等待的线程会被解除阻塞,每唤醒一个阻塞的线程,系统内核就会把信号量的值减1。此外,我们能够对信号量进行最大值限制,从而控制访问同一资源的最大线程数量。

.Net中,利用System.Threading.Semaphore进行信号量操作。下面时利用信号量实现线程同步的一个例子。

public static void Run()
{
DoIncrease(100000);
}

private static void DoIncrease(int incrementPerThread)
{
int number = 0;
Console.WriteLine($"use two threads to increase zero. each thread increase {incrementPerThread}.");

Semaphore semaphore = new Semaphore(1,1);

IList<Task> increaseTasks = new List<Task>();
increaseTasks.Add(Task.Run(() =>
{
Console.WriteLine($"thread #{Thread.CurrentThread.ManagedThreadId} is increasing the number.");
for (int i = 0; i < incrementPerThread; i++)
{
semaphore.WaitOne();
number++;
semaphore.Release(1);

}
}));
increaseTasks.Add(Task.Run(() =>
{
Console.WriteLine($"thread #{Thread.CurrentThread.ManagedThreadId} is increasing the number.");
for (int i = 0; i < incrementPerThread; i++)
{
semaphore.WaitOne();
number++;
semaphore.Release(1);

}
}));

Task.WaitAll(increaseTasks.ToArray());
semaphore.Dispose();
Console.WriteLine($"use Semaphore: result = {number}");
}

利用互斥体进程线程同步

互斥体Mutex的使用与自动重置事件和信号量类似,这里不再进行详细的总结。

互斥体常被用来保证应用程序只有一个实例运行,具体用法如下:

bool createNew;
using (new Mutex(true, Assembly.GetExecutingAssembly().FullName, out createNew))
{
if (!createNew)
{

Environment.Exit(0);
}
else
{

}
}

线程同步的混合模式

通过上面的总结我们知道,用户模式和内核模式由各自的优缺点,需要有一种模式既能兼顾用户和内核模式的优点又能避免他们的缺点,这就是混合模式。

混合模式会优先使用用户模式的线程同步处理,当多个线程竞争同步锁的时候,才会使用内核对象进行处理。如果多个线程一直不产生资源竞争,就不会发生CPU用户模式到内核模式的转换,开始资源竞争时,又会通过线程阻塞来防止CPU资源的浪费。

.NET中提供了多种混合模式的线程同步方式。例如手工重置事件和信号量的简化版本ManualResetEventSlimSemaphoreSlim,他们是线程在用户模式中自旋,直到发生资源竞争。具体使用与各自的内核模式一样,这里不再赘述。

lock关键字和Monitor

相信lock加锁是很多人做常用的线程同步方式。lock的使用很简单,如下:

private static readonly object _syncObject = new object();
public static void DoWork()
{
lock (_syncObject)
{

}
}

实际上,lock语法是对System.Threading.Monitor使用的一种简化,Monitor的用法如下:

private static readonly object _syncObject = new object();
public static void DoWork()
{
Monitor.Enter(_syncObject);

Monitor.Exit(_syncObject);
}

使用Monitor的可能会出先一些意象不到的问题。例如,如果不相关的业务代码在使用Monitor进行线程同步的时候,锁定了同一字符串,将会造成不相关业务代码的同步执行;此外需要注意的是,Monitor不能使用值类型作为锁对象,值类型会被装箱,装箱后的对象不同,将导致无法同步。

读写锁ReaderWriterLockSlim

ReaderWriterLockSlim可以用来实现多线程读取或独占写入的资源访问。读写锁的线程控制逻辑如下:

  • 一个线程写数据时,其他请求资源的线程全部被阻塞;

  • 一个线程读数据时,写线程被阻塞,其他读线程能继续运行;

  • 写结束时,解除其他某个写线程的阻塞,或者解除所有读线程的阻塞;

  • 读结束时,解除一个写线程的阻塞。

下面是读写锁的简单用法,详细用法可参考msdn文档。

private static readonly ReaderWriterLockSlim _rwlock = new ReaderWriterLockSlim();
public static void DoWork()
{
_rwlock.EnterWriteLock();

_rwlock.ExitWriteLock();
}

ReaderWriterLockSlim还有一个比较老的版本ReaderWriterLock,据说存在较多问题应尽量避免使用。

线程安全集合

.NET除了提供包含上面总结到的各种线程同步的诸多方式外,还封装了一些线程安全集合。这些集合在内部实现了线程同步,我们直接使用即可,很友好。线程安全集合在命名空间System.Collections.Concurrent下,包括ConcurrentQueue (T),ConcurrentStack<T>,ConcurrentDictionary<TKey,TValue>,ConcurrentBag<T>,BlockingCollection<T>,具体可阅读《何时使用线程安全集合》。

各种线程同步性能对比

下面我们对整数零进行多线程递增操作,每个线程固定递增量,来测试以下各种同步方式的性能对比。测试代码如下。




private static int _numberToIncrease;

public static void Run()
{
int increment = 100000;
int threadCount = 4;
DoIncrease(increment, threadCount, DoIncreaseByInterLocked);
DoIncrease(increment, threadCount, DoIncreaseWithSpinLock);
DoIncrease(increment, threadCount, DoIncreaseWithEvent);
DoIncrease(increment, threadCount, DoIncreaseWithSemaphore);
DoIncrease(increment, threadCount, DoIncreaseWithMonitor);
DoIncrease(increment, threadCount, DoIncreaseWithReaderWriterLockSlim);

}

public static void DoIncrease(int increment, int threadCount, Action<int> action)
{
_numberToIncrease = 0;
IList<Task> increaseTasks = new List<Task>(threadCount);
Stopwatch watch = Stopwatch.StartNew();
for (int i = 0; i < threadCount; i++)
{
increaseTasks.Add(Task.Run(() => action(increment)));
}
Task.WaitAll(increaseTasks.ToArray());
Console.WriteLine($"{action.Method.Name}=> Result: {_numberToIncrease} , Time: {watch.ElapsedMilliseconds} ms.");
}

#region 使用Interlocked,用户模式

public static void DoIncreaseByInterLocked(int increment)
{
for (int i = 0; i < increment; i++)
{
Interlocked.Increment(ref _numberToIncrease);
}
}

#endregion

#region 使用SpinLock,用户模式

private static SpinLock _spinlock = new SpinLock();
public static void DoIncreaseWithSpinLock(int increment)
{
for (int i = 0; i < increment; i++)
{
bool lockTaken = false;
try
{
_spinlock.Enter(ref lockTaken);
_numberToIncrease++;
}
finally
{
if (lockTaken)
{
_spinlock.Exit(false);
}
}
}
}

#endregion

#region 使用信号量Semaphore,内核模式

private static readonly Semaphore _semaphore = new Semaphore(1, 10);

public static void DoIncreaseWithSemaphore(int increment)
{
for (int i = 0; i < increment; i++)
{
_semaphore.WaitOne();
_numberToIncrease++;
_semaphore.Release(1);
}
}

#endregion

#region 使用事件AutoResetEvent,内核模式

private static readonly AutoResetEvent _are = new AutoResetEvent(true);
public static void DoIncreaseWithEvent(int increment)
{
for (int i = 0; i < increment; i++)
{
_are.WaitOne();
_numberToIncrease++;
_are.Set();
}
}

#endregion

#region 使用Monitor,混合模式

private static readonly object _monitorLocker = new object();
public static void DoIncreaseWithMonitor(int increment)
{
for (int i = 0; i < increment; i++)
{
bool lockTaken = false;
try
{
Monitor.Enter(_monitorLocker, ref lockTaken);
_numberToIncrease++;
}
finally
{
if (lockTaken)
{
Monitor.Exit(_monitorLocker);
}
}
}
}

#endregion

#region 使用ReaderWriterLockSlim,混合模式

private static readonly ReaderWriterLockSlim _rwlock = new ReaderWriterLockSlim();
public static void DoIncreaseWithReaderWriterLockSlim(int increment)
{
for (int i = 0; i < increment; i++)
{
_rwlock.EnterWriteLock();
_numberToIncrease++;
_rwlock.ExitWriteLock();
}
}

#endregion

下面是一组测试结果,可以很明显地看出,内核模式是相当耗时的,应尽量避免使用。而用户模式和混合模式,也需要根据具体的场景进行选择。这个测试过于简单,不具有普遍性。

DoIncreaseByInterLocked=> Result: 400000 , Time: 15 ms.
DoIncreaseWithSpinLock=> Result: 400000 , Time: 75 ms.
DoIncreaseWithEvent=> Result: 400000 , Time: 1892 ms.
DoIncreaseWithSemaphore=> Result: 400000 , Time: 1779 ms.
DoIncreaseWithMonitor=> Result: 400000 , Time: 14 ms.
DoIncreaseWithReaderWriterLockSlim=> Result: 400000 , Time: 22 ms.

小结

本文对C#/.NET中的线程同步进行了尽量详尽的总结,并行环境中在追求程序的高性能、响应性的同时,务必要保证数据的安全性。

C#并行编程系列的文章暂时就告一段落了。刚开始写博客,文章肯定存在不少问题,欢迎各位博友指出。

原文地址:https://www.cnblogs.com/chenbaoshun/p/10695343.html

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com 
640?wx_fmt=jpeg

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

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

相关文章

P2522 [HAOI2011]Problem b

P2522 [HAOI2011]Problem b 题意&#xff1a; 对于给出的 n 个询问&#xff0c;每次求有多少个数对 (x,y)&#xff0c;满足 a≤x≤b&#xff0c;c≤y≤d&#xff0c;且 gcd(x,y)k&#xff0c;gcd(x,y) 函数为 x 和 y 的最大公约数。 题解&#xff1a; 这个题跟P3455 [POI20…

.NET Core/Framework 创建委托以大幅度提高反射调用的性能

都知道反射伤性能&#xff0c;但不得不反射的时候又怎么办呢&#xff1f;当真的被问题逼迫的时候还是能找到解决办法的。为反射得到的方法创建一个委托&#xff0c;此后调用此委托将能够提高近乎直接调用方法本身的性能。&#xff08;当然 Emit 也能够帮助我们显著提升性能&…

[省选联考 2020 A/B 卷] 冰火战士(树状数组上二分)

文章目录problemsolution(10pts)code(10pts)solution(30pts)code(30pts)solution(60pts)code(60pts)solution(100pts)code(100pts)problem luogu-P6619 一场比赛即将开始。 每位战士有两个属性&#xff1a;温度和能量。 有两派战士&#xff1a; 冰系战士的技能会对周围造成…

P1829 [国家集训队]Crash的数字表格 / JZPTAB

P1829 [国家集训队]Crash的数字表格 / JZPTAB 题意&#xff1a; 求∑i1n∑j1mlcm(i,j)\sum_{i1}^{n}\sum_{j1}^{m}lcm(i,j)∑i1n​∑j1m​lcm(i,j) 1<n<m<1e7 结果mod20101009 题解&#xff1a; 跟这个题P3911 最小公倍数之和很相近&#xff0c;但是本题数据范围大…

C# 跨设备前后端开发探索

每个人都拥有 好奇心&#xff0c;好奇心驱使着我们总是去尝试做一些有趣的事情。带起你的好奇心&#xff0c;本文将使用 C# 开发各种各样好玩的东西。每个人都拥有 好奇心&#xff0c;好奇心驱使着我们总是去尝试做一些有趣的事情。比如这件事&#xff1a;在好奇心的驱使下&…

Docker最全教程之Python爬网实战(二十二)

Python目前是流行度增长最快的主流编程语言&#xff0c;也是第二大最受开发者喜爱的语言&#xff08;参考Stack Overflow 2019开发者调查报告发布&#xff09;。笔者建议.NET、Java开发人员可以将Python发展为第二语言&#xff0c;一方面Python在某些领域确实非常犀利&#xff…

P2870 [USACO07DEC]Best Cow Line G

P2870 [USACO07DEC]Best Cow Line G 题意&#xff1a; 给你一个字符串&#xff0c;每次从首或尾取一个字符组成字符串&#xff0c;问所有能够组成的字符串中字典序最小的一个。 题解&#xff1a; 现在要组成字典序最小的&#xff0c;那我们每次就尽可能取小的 我们从两端开…

中间件是什么?在.NET Core中的工作原理又是怎样的呢?

本文出自《从零开始学ASP.NET CORE MVC》推荐文章&#xff1a;ASP.NET Core appsettings.json文件ASP.NET Core 中的中间件(Middleware)在这个视频中&#xff0c;我们将了解&#xff0c;ASP.NET Core 中的中间件是 什么&#xff1f;中间件很重要&#xff0c;尤其是在你想当架构…

从高德采集最新的省市区三级坐标和行政区域边界,用js在浏览器中运行

本文描述的是对国家统计局于2019-01-31发布的《2018年统计用区划代码和城乡划分代码(截止2018年10月31日)》中省市区三级的坐标和行政区域边界的采集。本文更新&#xff08;移步查阅&#xff09;&#xff1a;19-04-15 新采集了2018的省市区三级的坐标和行政区域边界数据csv格式…

Display Substring

Display Substring 题意&#xff1a; 一个长度为n的字符串&#xff0c;每个字符有自己的价值&#xff0c;求第k小价值的不重复子串价值 题解&#xff1a; 首先众所周知&#xff0c;所有子串都可以用后缀的前缀来表示&#xff0c;这就和后缀数组扯上关系了 我们可以直接二分…

使用 DotNet CLI 创建自定义的 WPF 项目模板

描述当我们安装完 DotNetCore 3.0 版本的 SDK 后&#xff0c;我们就可以创建基于 DotNetCore 的 WPF 项目模板&#xff0c;通过如下 CLI 可以方便快捷的创建并运行我们的项目&#xff1a;Copydotnet new wpf -n WpfAppcd WpfAppdotnet restoredotnet run做过 WPF 开发的朋友都知…

[翻译] ASP.NET Core 利用 Docker、ElasticSearch、Kibana 来记录日志

一步一步指导您使用 ElasticSearch, Kibana, ASP.NET Core 2.1 和 Docker 来记录日志在本教程中&#xff0c;我将向您展示如何启动和运行 ElasticSearch&#xff0c;Kibana 和 ASP.NET Core 2.1在开始之前&#xff0c;让我们来看看 ElasticSearch&#xff0c;Kibana 和 Serilog…

【学习笔记】WQS二分详解及常见理解误区解释

文章目录应用分析算法分析WQS二分精髓的两点细节&#xff08;博客重点&#xff01;&#xff09;真题分析[国家集训队]Tree Ⅰ忘情星际广播网上很多博客写得模模糊糊的&#xff0c;对我这个新手可是一点都不友好。 昨天一天都在研究这个东西&#xff0c;分享一下自己的拙见。 百…

ASP.NET Core中HTTP管道和中间件的二三事

本文出自《从零开始学ASP.NET CORE MVC》推荐文章&#xff1a;中间件是什么&#xff1f;在.NET Core中的工作原理又是怎样的呢&#xff1f;配置ASP.NET Core请求(Request)处理管道在本视频中&#xff0c;我们将讨论使用中间件组件为asp.net core 应用程序配置请求处理管道。作为…

.Net Core Razor 预编译,动态编译,混合编译

预编译预编译是ASP .Net Core的默认方式。在发布时&#xff0c;默认会将系统中的所有Razor视图进行预编译。编译好的视图DLL统一命名为 xxx.PrecompiledViews.dll 或者 xxx.Views.dll动态编译将项目整个配置成动态编译很简单&#xff0c;添加一个配置项目MvcRazorCompileOnPubl…

一张图来看看.NETCore和前后端技术的演进之路

一张图2019年3月10日&#xff0c;在长沙.NET 技术社区组织的技术沙龙《.NET Core和前后端分离那些事儿》上&#xff0c;我们曾经试图通过一系列抽丝剥茧的过程来引导大家在这条基于.NET Core的前后端分离有关的技术路线上持续发散&#xff0c;由于各种原因未能成功&#xff0c;…

【学习笔记】我命由天不由我之随机化庇佑 —— 爬山法 和 模拟退火法

以下均假设最优解是在最低点。 爬山法 爬山算法是一种局部择优的方法&#xff0c;采用启发式方法&#xff0c;是对深度优先搜索的一种改进&#xff0c;它利用反馈信息帮助生成解的决策。 直白地讲&#xff0c;就是当目前无法直接到达最优解&#xff0c;但是可以判断两个解哪…

Strange Memory Gym - 102832F

Strange Memory Gym - 102832F 题意&#xff1a; 有一颗n个节点的树&#xff0c;求下面公式的值&#xff1a; 题解&#xff1a; a ⊕ b c 可以推出a ⊕ c b 那么ai⊕ajalca(i,j)a_{lca(i,j)}alca(i,j)​,可以得到&#xff1a;ai⊕alca(i,j)a_{lca(i,j)}alca(i,j)​aj,ai就…

.net core 使用RSA获取私钥证书并签名

以前我们使用RSA加密主要是使用RSACryptoServiceProvider这个类&#xff0c;在.NET Core中也有这个类&#xff0c;但是这个类并不支持跨平台&#xff0c;所以如果你是用这个类来进行加/解密在windows上运行是完全没有错误的&#xff0c;但是只要你一放到Linux下就会出现异常。查…

CDN流量调度问题(2021CCPC华为云挑战赛)

CDN流量调度问题 题意&#xff1a; 有n个线路&#xff0c;每个线路的任务量为a[i],初始状态下&#xff0c;每个线路起初只有一个节点&#xff0c;每个线路完成任务所需时间为 现在你要给每个线路增加节点&#xff0c;所有增加的节点总数不超过m&#xff0c;第i个线路增加节点…