C#的多线程、线程池和Task

news2024/9/29 15:33:23

        线程 被定义为程序的执行路径。每个线程都定义了一个独特的控制流。如果您的应用程序涉及到复杂的和耗时的操作,那么设置不同的线程执行路径往往是有益的,每个线程执行特定的工作。

        线程是轻量级进程。一个使用线程的常见实例是现代操作系统中并行编程的实现。使用线程节省了 CPU 周期的浪费,同时提高了应用程序的效率。

一、多线程

1、创建和暂停线程

        当程序运行时,会新建一个线程, 该线程会执行PrintNumbersWithDelay方法中的代码。然后会立即执行PrintNumbers方法。关键之处在于在PrintNumbersWithDelay方法中加入了Thread.Sleep方法调用,这将导致线程执行该代码时,在打印任何数字之前会等待指定的时间(本例中是2秒钟)。然而,PrintNumbers方法的执行是不受新线程的影响的

class Program
{
    static void Main(string[] args)
    {
        Thread t = new Thread(PrintNumbersWithDelay);
        t.Start();
        PrintNumbers();
        Console.ReadKey();
    }

    static void PrintNumbers()
    {
        Console.WriteLine("Starting...");
        for (int i = 1; i < 5; i++)
        {
            Console.WriteLine(i);
        }
    }

    static void PrintNumbersWithDelay()
    {
        Console.WriteLine("Starting...");
        for (int i = 1; i < 5; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(2));//暂停2S
            Console.WriteLine(i);
        }
    }
}

执行结果如下:

2、线程等待

        当程序运行时,启动了一个耗时较长的线程来打印数字,打印每个数字前要等待两秒。但我们在主程序中调用了t.Join方法,该方法允许我们等待直到线程t完成。当线程t完成 "时,主程序会继续运行。借助该技术可以实现在两个线程间同步执行步骤。第一个线程会等待另一个线程完成后再继续执行。第一个线程等待时是处于阻塞状态(正如暂停线程中调用 Thread.Sleep方法一样)

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Starting program...");
        Thread t = new Thread(PrintNumbersWithDelay);
        t.Start();
        t.Join();
        Console.WriteLine("Thread completed");
    }

    static void PrintNumbersWithDelay()
    {
        Console.WriteLine("Starting...");
        for (int i = 1; i < 10; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine(i);
        }
    }
}

执行结果如下:

 

3、终止线程

        当主程序和单独的数字打印线程运行时,我们等待6秒后对线程调用了t.Abort方法。这给线程注入了ThreadAbortException方法,导致线程被终结。这非常危险,因为该异常可以在任何时刻发生并可能彻底摧毁应用程序。另外,使用该技术也不一定总能终止线程。目-标线程可以通过处理该异常并调用Thread.ResetAbort方法来拒绝被终止。因此并不推荐使用,Abort方法来关闭线程。可优先使用一些其他方法,比如提供一个CancellationToken方法来,取消线程的执行。

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Starting program...");
        Thread t = new Thread(PrintNumbersWithDelay);
        t.Start();
        Thread.Sleep(TimeSpan.FromSeconds(6));
        t.Abort();
        Console.WriteLine("A thread has been aborted");
    }

    static void PrintNumbersWithDelay()
    {
        Console.WriteLine("Starting...");
        for (int i = 1; i < 10; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine(i);
        }
    }
}

4、线程优先级

        当主程序启动时定义了两个不同的线程。第一个线程优先级为ThreadPriority.Highest,即具有最高优先级。第二个线程优先级为ThreadPriority.Lowest,即具有最低优先级。我们先, ,打印出主线程的优先级值,然后在所有可用的CPU核心上启动这两个线程。如果拥有一个1以上的计算核心,将在两秒钟内得到初步结果。最高优先级的线程通常会计算更多的迭代.但是两个值应该很接近。然而,如果有其他程序占用了所有的CPU核心运行负载,结果则会截然不同。

  为了模拟该情形,我们设置了ProcessorAffinity选项,让操作系统将所有的线程运行在单个CPU核心(第一个核心)上。现在结果完全不同,并且计算耗时将超过2秒钟。 .这是因为CPU核心大部分时间在运行高优先级的线程,只留给剩下的线程很少的时间来,运行。

  请注意这是操作系统使用线程优先级的一个演示。通常你无需使用这种行为编写程序。

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Current thread priority: {0}", Thread.CurrentThread.Priority);
        Console.WriteLine("Running on all cores available");
        RunThreads();
        Thread.Sleep(TimeSpan.FromSeconds(2));
        Console.WriteLine("Running on a single core");
        Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
        RunThreads();
    }

    static void RunThreads()
    {
        var sample = new ThreadSample();

        var threadOne = new Thread(sample.CountNumbers);
        threadOne.Name = "ThreadOne";
        var threadTwo = new Thread(sample.CountNumbers);
        threadTwo.Name = "ThreadTwo";

        threadOne.Priority = ThreadPriority.Highest;
        threadTwo.Priority = ThreadPriority.Lowest;
        threadOne.Start();
        threadTwo.Start();

        Thread.Sleep(TimeSpan.FromSeconds(2));
        sample.Stop();

        Console.ReadKey();
    }

    class ThreadSample
    {
        private bool _isStopped = false;

        public void Stop()
        {
            _isStopped = true;
        }

        public void CountNumbers()
        {
            long counter = 0;

            while (!_isStopped)
            {
                counter++;
            }

            Console.WriteLine("{0} with {1,11} priority " +
                        "has a count = {2,13}", Thread.CurrentThread.Name,
                        Thread.CurrentThread.Priority,
                        counter.ToString("N0"));
        }
    }
}

运行结果:

 

 5、前台线程和后台线程

        当主程序启动时定义了两个不同的线程。默认情况下,显式创建的线程是前台线程。通过手动的设置threadTwo对象的IsBackground属性为ture来创建一个后台线程。通过配置来实现第一个线程会比第二个线程先完成。然后运行程序。

  第一个线程完成后,程序结束并且后台线程被终结。这是前台线程与后台线程的主要区,别:进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。

  一个重要注意事项是如果程序定义了一个不会完成的前台线程,主程序并不会正常结束。

class Program
{
    static void Main(string[] args)
    {
        var sampleForeground = new ThreadSample(10);
        var sampleBackground = new ThreadSample(20);

        var threadOne = new Thread(sampleForeground.CountNumbers);
        threadOne.Name = "ForegroundThread";
        var threadTwo = new Thread(sampleBackground.CountNumbers);
        threadTwo.Name = "BackgroundThread";
        threadTwo.IsBackground = true;

        threadOne.Start();
        threadTwo.Start();

        Console.ReadKey();
    }

    class ThreadSample
    {
        private readonly int _iterations;

        public ThreadSample(int iterations)
        {
            _iterations = iterations;
        }
        public void CountNumbers()
        {
            for (int i = 0; i < _iterations; i++)
            {
                Thread.Sleep(TimeSpan.FromSeconds(0.5));
                Console.WriteLine("{0} prints {1}", Thread.CurrentThread.Name, i);
            }
        }
    }
}

6、向线程传递参数 

        当主程序启动时,首先创建了ThreadSample类的一个对象,并提供了一个迭代次数。然后使用该对象的CountNumbers方法启动线程。该方法运行在另一个线程中,但是使用数 ,字10,该数字是通过ThreadSample对象的构造函数传入的。因此,我们只是使用相同的间接方式将该迭代次数传递给另一个线程。

  另一种传递数据的方式是使用Thread.Start方法。该方法会接收一个对象,并将该对象,传递给线程。为了应用该方法,在线程中启动的方法必须接受object类型的单个参数。在创建threadTwo线程时演示了该方式。我们将8作为一个对象传递给了Count方法,然后 Count方法被转换为整型。

  接下来的方式是使用lambda表达式。lambda表达式定义了一个不属于任何类的方法。我们创建了一个方法,该方法使用需要的参数调用了另一个方法,并在另一个线程中运行该 ,方法。当启动threadThree线程时,打印出了12个数字,这正是我们通过lambda表达式传递,的数字。

  使用lambda表达式引用另一个C#对象的方式被称为闭包。当在lambda表达式中使用任何局部变量时, C#会生成一个类,并将该变量作为该类的一个属性。所以实际上该方式与 threadOne线程中使用的一样,但是我们无须定义该类, C#编译器会自动帮我们实现。

  这可能会导致几个问题。例如,如果在多个lambda表达式中使用相同的变量,它们会共享该变量值。在前一个例子中演示了这种情况。当启动threadFour和threadFive线程时,.它们都会打印20,因为在这两个线程启动之前变量被修改为20。

class Program
{
    static void Main(string[] args)
    {
        var sample = new ThreadSample(10);

        var threadOne = new Thread(sample.CountNumbers);
        threadOne.Name = "ThreadOne";
        threadOne.Start();
        threadOne.Join();

        Console.WriteLine("--------------------------");

        var threadTwo = new Thread(Count);
        threadTwo.Name = "ThreadTwo";
        threadTwo.Start(8);
        threadTwo.Join();

        Console.WriteLine("--------------------------");

        var threadThree = new Thread(() => CountNumbers(12));
        threadThree.Name = "ThreadThree";
        threadThree.Start();
        threadThree.Join();
        Console.WriteLine("--------------------------");

        int i = 10;
        var threadFour = new Thread(() => PrintNumber(i));
        i = 20;
        var threadFive = new Thread(() => PrintNumber(i));
        threadFour.Start(); 
        threadFive.Start();
    }

    static void Count(object iterations)
    {
        CountNumbers((int)iterations);
    }

    static void CountNumbers(int iterations)
    {
        for (int i = 1; i <= iterations; i++)
        {
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
            Console.WriteLine("{0} prints {1}", Thread.CurrentThread.Name, i);
        }
    }

    static void PrintNumber(int number)
    {
        Console.WriteLine(number);
    }

    class ThreadSample
    {
        private readonly int _iterations;

        public ThreadSample(int iterations)
        {
            _iterations = iterations;
        }
        public void CountNumbers()
        {
            for (int i = 1; i <= _iterations; i++)
            {
                Thread.Sleep(TimeSpan.FromSeconds(0.5));
                Console.WriteLine("{0} prints {1}", Thread.CurrentThread.Name, i);
            }
        }
    }
}

二、线程池的使用

        在之前部分我们讨论了创建线程和线程协作的几种方式。现在考虑另一种情况,即只花费极少的时间来完成创建很多异步操作。创建线程是昂贵的操作,所以为每个短暂的异步操作创建线程会产生显著的开销。

  为了解决该问题,有一个常用的方式叫做池( pooling),线程池可以成功地适应于任何需要大量短暂的开销大的资源的情形。我们事先分配一定的资源,将这些资源放入到资源池。每次需要新的资源,只需从池中获取一个,而不用创建一个新的。当该资源不再被使用,时,就将其返回到池中。

1、在线程池中调用委托

        当程序运行时,使用旧的方式创建了一个线程,然后启动它并等待完成。由于线程的构造函数只接受一个无任何返回结果的方法,我们使用了lambda表达式来将对Test方法的调用包起来。我们通过打印出Thread. CurrentThread.IsThreadPoolThread属性值来确,保该线程不是来自线程池。我们也打印出了受管理的线程ID来识别代码是被哪个线程执行的。

  然后定义了一个委托并调用Beginlnvoke方法来运行该委托。BeginInvoke方法接受一个回调函数。该回调函数会在异步操作完成后会被调用,并且一个用户自定义的状态会传给该回调函数。该状态通常用于区分异步调用。结果,我们得到了一个实现了IAsyncResult接口的result对象。BeginInvoke立即返回了结果,当线程池中的工作线程在执行异步操作时,仍允许我们继续其他工作。当需要异步操作的结果时,可以使用BeginInvoke方法调用返回的result对象。我们可以使用result对象的IsCompleted属性轮询结果。但是在本例子中,使用的是AsyncWaitHandle属性来等待直到操作完成。当操作完成后,会得到一个结果,可以通过委托调用EndInvoke方法,将IAsyncResult对象传递给委托参数。

  事实上使用AsyncWaitHandle并不是必要的。如果注释掉r.AsyncWaitHandle.WaitOne,代码照样可以成功运行, 因为EndInvoke方法事实上会等待异步操作完成。调用 "EndInvoke方法(或者针对其他异步API的EndOperationName方法)是非常重要的, '因为该方法会将任何未处理的异常抛回到调用线程中。当使用这种异步API时,请确保始终调用了Begin和End方法。

  当操作完成后,传递给BeginInvoke方法的回调函数将被放置到线程池中,确切地说是,一个工作线程中。如果在Main方法定义的结尾注释掉Thread.Sleep方法调用,回调函数将不,会被执行。这是因为当主线程完成后,所有的后台线程会被停止,包括该回调函数。对委托和回调函数的异步调用很可能会被同一个工作线程执行。通过工作线程ID可以容易地看出。使用BeginOperationName/EndOperationName方法和.NET中的IAsyncResult对象等方 ,式被称为异步编程模型(或APM模式),这样的方法对被称为异步方法。该模式也被应用于多个,NET类库的API中,但在现代编程中,更推荐使用任务并行库( Task Parallel Library,简称TPL)来组织异步API

using System;
using System.Diagnostics;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        int threadId = 0;
        RunOnThreadPool poolDelegate = Test;
        var t = new Thread(() => Test(out threadId));
        t.Start();
        t.Join();

        Console.WriteLine("Thread id: {0}", threadId);

        IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "a delegate asynchronous call");
        r.AsyncWaitHandle.WaitOne();
        string result = poolDelegate.EndInvoke(out threadId, r);
        Console.WriteLine("Thread pool worker thread id: {0}", threadId);
        Console.WriteLine(result);

        Thread.Sleep(TimeSpan.FromSeconds(2));

        Console.ReadKey();
    }

    private delegate string RunOnThreadPool(out int threadId);

    private static void Callback(IAsyncResult ar)
    {
        Console.WriteLine("Starting a callback...");
        Console.WriteLine("State passed to a callbak: {0}", ar.AsyncState);
        Console.WriteLine("Is thread pool thread: {0}", Thread.CurrentThread.IsThreadPoolThread);
        Console.WriteLine("Thread pool worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
    }


    private static string Test(out int threadId)
    {
        Console.WriteLine("Starting...");
        Console.WriteLine("Is thread pool thread: {0}", Thread.CurrentThread.IsThreadPoolThread);
        Thread.Sleep(TimeSpan.FromSeconds(2));
        threadId = Thread.CurrentThread.ManagedThreadId;
        return string.Format("Thread pool worker thread id was: {0}", threadId);
    }
}

运行结果:

 2、向线程池中放入异步操作

        首先定义了AsyncOperation方法,其接受单个object类型的参数。然后使用QueueUser WorkItem方法将该方法放到线程池中。接着再次放入该方法,但是这次给方法调用传入了一个状态对象。该对象将作为状态参数传递给AsynchronousOperation方法。

  在操作完成后让线程睡眠一秒钟,从而让线程池拥有为新操作重用线程的可能性。如果注释掉所有的Thread.Sleep调用,那么所有打印出的线程ID多半是不一样的。如果ID是一样的,那很可能是前两个线程被重用来运行接下来的两个操作。

  首先将一个lambda表达式放置到线程池中。这里没什么特别的。我们使用了labmbda表达式语法,从而无须定义一个单独的方法。

  然后,我们使用闭包机制,从而无须传递lambda表达式的状态。闭包更灵活,允许我,们向异步操作传递一个以上的对象而且这些对象具有静态类型。所以之前介绍的传递对象给,方法回调的机制既冗余又过时。在C#中有了闭包后就不再需要使用它了。

class Program
{
    static void Main(string[] args)
    {
        const int x = 1;
        const int y = 2;
        const string lambdaState = "lambda state 2";

        ThreadPool.QueueUserWorkItem(AsyncOperation);
        Thread.Sleep(TimeSpan.FromSeconds(1));

        ThreadPool.QueueUserWorkItem(AsyncOperation, "async state");
        Thread.Sleep(TimeSpan.FromSeconds(1));

        ThreadPool.QueueUserWorkItem( state => {
                Console.WriteLine("Operation state: {0}", state);
                Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(TimeSpan.FromSeconds(2));
            }, "lambda state");

        ThreadPool.QueueUserWorkItem( _ =>
        {
            Console.WriteLine("Operation state: {0}, {1}", x+y, lambdaState);
            Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(TimeSpan.FromSeconds(2));
        }, "lambda state");

        Thread.Sleep(TimeSpan.FromSeconds(2));

        Console.ReadKey();
    }

    private static void AsyncOperation(object state)
    {
        Console.WriteLine("Operation state: {0}", state ?? "(null)");
        Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(TimeSpan.FromSeconds(2));
    }
}

三、Task的使用

        Net Framework4.0引入了一个新的关于异步操作的API,它叫做.任务并行库( Task Parallel Library,简称TPL), .Net Framework 4.5版对该API进行了轻微的改进,使用更简单。在本书的项目中将使用最新版的TPL,即.Net Framework 4.5版中的 API, TPL可被认为是线程池之上的又一个抽象层,其对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度的APL, TPL的核心概念是任务。一个任务代表了一个异步操作,该操作可以通过多种方式运行,可以使用或不使用独立线程运行。在本章中将探究任务的所有使用细节。

1、创建任务

        当程序运行时,我们使用Task的构造函数创建了两个任务。我们传入一个lambda表达式作为Action委托。这可以使我们给TaskMethod提供一个string参数。然后使用Start方法运行这些任务。

  请注意只有调用了这些任务的Start方法,才会执行任务。很容易忘记真正启动任务。

  然后使用Task.Run和Task.Factory.StartNew方法来运行了另外两个任务。与使用Task构造函数的不同之处在于这两个被创建的任务会立即开始工作,所以无需显式地调用这些任务的Start方法。从Task 1到Task 4的所有任务都被放置在线程池的工作线程中并以未指定,的顺序运行。如果多次运行该程序,就会发现任务的执行顺序是不确定的。

  Task.Run方法只是Task.Factory.StartNew的一个快捷方式,但是后者有附加的选项。通!常如果无特殊需求,则可使用前一个方法,如Task 5所示。我们标记该任务为长时间运行,结果该任务将不会使用线程池,而在单独的线程中运行。然而,根据运行该任务的当前的任务调度程序( task scheduler)运行方式有可能不同。

class Program
{
    static void Main(string[] args)
    {
        var t1 = new Task(() => TaskMethod("Task 1"));
        var t2 = new Task(() => TaskMethod("Task 2"));
        t2.Start();
        t1.Start();
        Task.Run(() => TaskMethod("Task 3"));
        Task.Factory.StartNew(() => TaskMethod("Task 4"));
        Task.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning);
        Thread.Sleep(TimeSpan.FromSeconds(1));

        Console.ReadKey();
    }

    static void TaskMethod(string name)
    {
        Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
            name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
    }
}

2、使用任务执行基本的操作

        首先直接运行TaskMethod方法,这里并没有把它封装到一个任务中。结果根据它提供给我们的主线程的信息可以得知该方法是被同步执行的。很显然它不是线程池中的线程。

  然后我们运行了Task 1,使用Start方法启动该任务并等待结果。该任务会被放置在线程池中,并且主线程会等待,直到任务返回前一直处于阻塞状态。

  Task 2和Task 1类似,除了Task 2是通过RunSynchronously()方法运行的。该任务会运行在主线程中,该任务的输出与第一个例子中直接同步调用TaskMethod的输出完全一样。这是个非常好的优化,可以避免使用线程池来执行非常短暂的操作。

  我们用以运行Task 1相同的方式来运行Task 3,但这次没有阻塞主线程,只是在该任务完成前循环打印出任务状态。结果展示了多种任务状态,分别是Creatd, Running和 RanToCompletion.

class Program
{
    static void Main(string[] args)
    {
        TaskMethod("Main Thread Task");
        Task<int> task = CreateTask("Task 1");
        task.Start();
        int result = task.Result;
        Console.WriteLine("Result is: {0}", result);

        task = CreateTask("Task 2");
        task.RunSynchronously();
        result = task.Result;
        Console.WriteLine("Result is: {0}", result);

        task = CreateTask("Task 3");
        Console.WriteLine(task.Status);
        task.Start();

        while (!task.IsCompleted)
        {
            Console.WriteLine(task.Status);
            Thread.Sleep(TimeSpan.FromSeconds(0.5));
        } 
        
        Console.WriteLine(task.Status);
        result = task.Result;
        Console.WriteLine("Result is: {0}", result);

        Console.ReadKey();
    }

    static Task<int> CreateTask(string name)
    {
        return new Task<int>(() => TaskMethod(name));
    }

    static int TaskMethod(string name)
    {
        Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",
            name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
        Thread.Sleep(TimeSpan.FromSeconds(2));
        return 42;
    }
}

参考文献地址:https://www.cnblogs.com/wyt007/p/9486752.html

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

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

相关文章

如何使用Python和ftplib模块连接到FTP服务器并列出远程目录中的文件?

ftp服务可以用在以下一些使用场景&#xff1a; 文件共享&#xff1a;使用Python和FTP服务器可以轻松地搭建一个文件共享服务&#xff0c;使得用户可以上传和下载文件&#xff0c;从而促进协作和信息共享。 数据备份&#xff1a;FTP可以用于将数据备份到另一个服务器或云存储中…

Git ---- GitHub 操作

Git ---- GitHub 操作1. 创建远程仓库2. 远程仓库操作1. 创建爱你远程仓库别名2. 推送本地分支到远程仓库3. 克隆远程仓库到本地4. 邀请加入团队5. 拉取远程库内容3. 跨团队协作4. SSH 免密登录GitHub 网址&#xff1a;https://github.com/ Ps&#xff1a;全球最大同性交友网站…

实现弹窗功能并修改其中一个系数

把鼠标放在number-info上面,会是一个delon/chart的类库,可以在NG-ALAIN上找到阅读NG ALAIN的图表,以及number-info样式,数据文本 它拥有[title] [subtitle]两个可以是TemplateRef类型的,而template可以在里面放一些东西,比如按钮,所以可以放一个修改按钮 这里刚开始把template放…

学习 Python 之 Pygame 开发魂斗罗(三)

学习 Python 之 Pygame 开发魂斗罗&#xff08;三&#xff09;继续编写魂斗罗1. 角色站立2. 角色移动3. 角色跳跃4. 角色下落继续编写魂斗罗 在上次的博客学习 Python 之 Pygame 开发魂斗罗&#xff08;二&#xff09;中&#xff0c;我们完成了角色的创建和更新&#xff0c;现…

MySQL高级第一讲

目录 一、MySQL高级01 1.1 索引 1.1.1 索引概述 1.1.2 索引特点 1.1.3 索引结构 1.1.4 BTREE结构(B树) 1.1.5 BTREE结构(B树) 1.1.6 索引分类 1.1.7 索引语法 1.1.8 索引设计原则 1.2 视图 1.2.1 视图概述 1.2.2 创建或修改视图 1.3 存储过程和函数 1.3.1 存储过…

openresty的部署、nginx高速缓存的配置、nginx日志的可视化

文章目录一、openresty1.OpenResty简介2.OpenResty的技术3.OpenResty的优势4.openresty部署实验二、nginx配置高效缓存三、nginx日志可视化一、openresty 1.OpenResty简介 OpenResty官网 http://openresty.org/cn/ OpenResty是一个基于 Nginx 与 Lua 的高性能 Web 平台&#x…

shell基础学习

文章目录查看shell解释器写hello world多命令处理执行变量常用系统变量自定义变量撤销变量静态变量变量提升为全局环境变量特殊变量$n$#$* $$?运算符:条件判断比较流程控制语句ifcasefor 循环while 循环read读取控制台输入基本语法:函数系统函数basenamedirname自定义函数shel…

FL StudioV21电脑版水果编曲音乐编辑软件

这是一款功能十分丰富和强大的音乐编辑软件&#xff0c;能够帮助用户进行编曲、剪辑、录音、混音等操作&#xff0c;让用户能够全面地调整音频。FL水果最新版是一款专业级别的音乐编曲软件&#xff0c;集合更多的编曲功能为一身&#xff0c;可以进行录音、编辑、制作、混音、调…

计算机网络(六): HTTP,HTTPS,DNS,网页解析全过程

文章目录一、HTTP头部包含的信息通用头部请求头部响应头部实体头部二、Keep-Alive和非Keep-Alive的区别三、HTTP的方法四、HTTP和HTTPS建立连接的过程4.1 HTTP4.2 HTTPS五、HTTP和HTTPS的区别六、HTTPS的加密方式七、cookie和sessionsessioncookie八、HTTP状态码状态码200&…

【微信小程序】-- WXML 模板语法 - 数据绑定(九)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…

DPDK系列之四DPDK整体框架分析说明

一、网络发展和DPDK 在上篇分析过网络应用对DPDK出现的影响。而具体体现在技术上&#xff0c;从最简单来看就是从C10K到c100K甚至更多。而相应的计算的发展也从挖掘单CPU的性能发展到了瓶颈&#xff0c;同样&#xff0c;对于网络设备也遇到了类似的问题。而目前解决问题的方法…

MySQL到Elasticsearch实时同步构建数据检索服务的选型与思考[转载]

前言 本文具体探讨 MySQL 数据实时同步到 Elasticsearch (以下简称 ES ) 技术方案和思考&#xff0c;同时使用一定篇幅介绍一些前置知识&#xff0c;从理论到实践&#xff0c;让读者更好的理解这块内容和相关问题。包括&#xff1a; 为什么我们要将数据从 MySQL 实时同步到 ES …

Day899.Join语句优化 -MySQL实战

Join语句优化 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于Join语句优化的内容。 join 语句的两种算法&#xff0c;分别是 Index Nested-Loop Join(NLJ) 和 Block Nested-Loop Join(BNL)。 发现在使用 NLJ 算法的时候&#xff0c;其实效果还是不错的&#xff0c…

【手把手一起学习】(五) Altium Designer 20 STM32核心板Demo----PCB封装库添加元件

1 PCB封装库添加元件 元件的PCB封装非常重要&#xff0c;关系到实际电子元件能否焊接到制作的电路板上。PCB封装的引脚顺序&#xff0c;引脚间距&#xff0c;焊盘大小&#xff0c;焊盘形状等都需要与元件实物严格对应&#xff0c;因此绘制PCB封装库时&#xff0c;需要参考元件…

在Windows上编译Nginx

《在Windows上编译Nginx》视频教程官方编译说明 Building nginx on the Win32 platform with Visual C 环境准备 1. Microsoft Visual Studio(Microsoft Visual C 编译器)&#xff0c;下载地址&#xff1a;https://visualstudio.microsoft.com/zh-hans/。 2. Git(备用)&…

OSS存储使用之centOS系统ossfs挂载

以CentOS7系统为例 下载CentOS系统支持的ossfs工具的版本&#xff0c;以下载CentOS 7.0 (x64)版本为例&#xff0c;可以通过wget命令进行安装包的下载 wget http://gosspublic.alicdn.com/ossfs/ossfs_1.80.6_centos7.0_x86_64.rpm 也可以通过yum命令来进行安装包的下载 sud…

【网络原理9】HTTP响应篇

在前两篇文章当中&#xff0c;已经分别介绍了HTTP是什么&#xff0c;以及常见的请求头当中的属性。【网络原理7】认识HTTP_革凡成圣211的博客-CSDN博客HTTP抓包&#xff0c;Fiddler的使用https://blog.csdn.net/weixin_56738054/article/details/129148515?spm1001.2014.3001.…

excel格式调整:表格应用中格式刷技法汇总

格式刷很简单&#xff0c;点一下&#xff0c;就可以把格式复制到其他单元格、图形、文字上。但是格式刷的用法又不仅仅这么一点&#xff0c;它还可以实现快速隔行填色、隔行隐藏&#xff0c;实现“无损”合并单元格等。在excel中&#xff0c;位于开始菜单中左侧的格式刷&#x…

澜沧古茶再冲刺港交所上市:多项核心指标下滑,杜春峄为董事长

近日&#xff0c;普洱澜沧古茶股份有限公司&#xff08;下称“澜沧古茶”&#xff09;向港交所主板提交上市申请&#xff0c;中信建投国际、招商证券国际为其联席保荐人。据贝多财经了解&#xff0c;这已经是澜沧古茶第二次在港交所递表&#xff0c;此前曾于2022年5月30日在港交…

不同方案特性对比

特性对比项 2.4G 蓝牙 868M WIFI 通信速率 低 低 低 高 距离&#xff08;实用可靠&#xff09; 20米 10米 30米 15米 确定性 高 低 高 高 可靠性&#xff08;距离内&#xff09; 高 低 高 高 刷新一个标签时间&#xff08;通常&#xff09; 0.5-1s …