首页 > 编程语言 >C#笔记9 对线程Thread的万字解读 小小多线程直接拿下!

C#笔记9 对线程Thread的万字解读 小小多线程直接拿下!

时间:2024-09-10 23:54:03浏览次数:13  
标签:Thread C# void System 线程 new 多线程 public

上一条笔记有些潦草,这是因为昨天并没有很好的理解线程可以进行的操作。今天准备细化自己对这方面的理解和记录。来看看细节吧!

环境:VS2022

系统:windows10

环境:.Net 8.0 以及.Net FrameWork 4.7.2(winform)

线程是什么?

线程是什么?

每个操作系统上运行的应用程序都是一个进程,一个进程可以包括一个或多个线程。

线程是操作系统分配处理器时间的基本单元。

在进程中可以有多个线程。

简答见前文,今天主要讲具体的方法和代码。

C#笔记8 线程是什么?多线程怎么实现和操作?-CSDN博客

线程的创建和启动

都知道创建线程很简单:

Thread th = new Thread(mythreaddo);
th.Start();

void mythreaddo(){
return;
}

在创建了线程之后,我们使用start方法就可以启动这个线程,让它去执行我们指定的程序段了。对于一般的程序而言这已经似乎很够用了,安排一个去下载,安排一个去读写,然后问题来了,如果我们线程调用的方法要传参怎么办?

事实上这看起来像一个线程间交互的问题,你说为什么?当然是因为你创建线程的地方就是我们的主线程啊!你在主线程想要传递参数给子线程,这可不就涉及线程间的交互吗?

使用的方法也很简单,就和我们以前不同方法共享一个变量一样,这里在构造函数中传递的是委托名,不能携带参数,那该怎么办呢?

两种构造函数

我们知道创建线程要给他一个要执行的委托,但是我们能传递进去的委托类型其实有两种哦。我们让线程运行的方法要符合这两种委托的模式,也就是所谓的签名相同(参数数目,参数类型,参数顺序,参数修饰符),以及返回值相同。

在线程创建时你使用的构造函数其实会发生变化,但是也许你没有发现:

构造函数1:

public Thread (System.Threading.ParameterizedThreadStart start);

里面委托参数的原型: 

public delegate void ParameterizedThreadStart(object? obj);

 构造函数2:

public Thread (System.Threading.ThreadStart start);

委托参数的原型: 

public delegate void ThreadStart();

 例子:

public void serverstart()
{
    //开启服务端线程

    mywaitthread = new Thread(new ParameterizedThreadStart(serverrun));
    mywaitthread.Start();
    tm_checkmessage1.Start();

}
//注意:下面这个有参数
public void serverrun(object obj)
public void serverstart()
{
    //开启服务端线程

    mywaitthread = new Thread(new ThreadStart(serverrun));
    mywaitthread.Start();
    tm_checkmessage1.Start();

}
//没有参数
public void serverrun()

 两种构造的区别就是第一种构造函数中的委托是允许携带参数的

Visual Basic 和 C# 编译器从 and 方法的签名推断 ParameterizedThreadStart 委托,并调用正确的构造函数。因此,代码中可以没有显式的构造函数调用。 

实际上ThreadStart也可以这样,所以实际上你直接写一个serverrun在Thread的实例化的参数里也是可以的。

两种构造的相同之处就是两个类型的委托都不会有返回值。这很好理解,返回值设为什么类型?给谁呢?真要返回值,可以考虑引用类型传参,或者线程间交互的其他方法。

允许携带参数?这就让我们在之后的线程启动时传参提供了机会。在例子中,因为这样,我们能够在后面将参数传给我们的serverrun方法。

怎么传参?举例请看以下代码:

using System;
using System.Threading;

public class Work
{
    public static void Main()
    {
        // Start a thread that calls a parameterized static method.
        Thread newThread = new Thread(Work.DoWork);
        newThread.Start(42);

        // Start a thread that calls a parameterized instance method.
        Work w = new Work();
        newThread = new Thread(w.DoMoreWork);
        newThread.Start("The answer.");
    }
 
    public static void DoWork(object data)
    {
        Console.WriteLine("Static thread procedure. Data='{0}'",
            data);
    }

    public void DoMoreWork(object data)
    {
        Console.WriteLine("Instance thread procedure. Data='{0}'",
            data);
    }
}
// This example displays output like the following:
//       Static thread procedure. Data='42'
//       Instance thread procedure. Data='The answer.'

这是微软文档给的例子,或许你可以看到他定义了两个方法。在主线程中声明定义了一个线程,然后使用第一个方法初始化了这个线程。然后这个线程就启动了,之后他实例化了一个这个类的对象,传递了这个对象的方法给这个线程。你看到这里可能会疑惑,为什么newThread可以被初始化两次?额,这不是把一个线程启动两次,而是启动了两个线程。只是我们放弃了第一个线程的引用罢了。

好的你已经学会了怎么把一个具有参数的方法作为线程的启动方法了,当然了你如果不想自己的方法传递object这么单一,其实也很简单,你虽然写object,但是你只要定义一个类继承Object作为你传递的数据结构,使用子类传递给父类的位置,然后在方法内进行类型转换即可。这一手法我们在之前的委托和事件的介绍中也使用了这一传参方法,异曲同工之处嘛。

c#笔记5 详解事件的内置类型EventHandler、windows事件在winform中的运用_c# eventhandler用法-CSDN博客

using System;
using System.Threading;

// 定义一个继承自object的类
public class MessageData : Object
{
    public string Text { get; set; }
}

public class Program
{
    public static void Main()
    {
        // 创建MessageData的实例
        MessageData data = new MessageData { Text = "Hello, World!" };
        
        // 创建线程,并传递MessageData的实例

        //Visual Basic 和 C# 编译器从 and 方法的签名推断 ParameterizedThreadStart 委托,并调用正确的构造函数。因此,代码中可以没有显式的构造函数调用。 
        Thread newThread = new Thread(ThreadMethod);
        newThread.Start(data);
    }
    
    // 这个方法接受一个object类型的参数
    public static void ThreadMethod(object data)
    {
        // 将object参数转换为MessageData类型
        if (data is MessageData messageData)
        {
            Console.WriteLine(messageData.Text);
        }
    }
}

 当然你也可以使用下面的方法传递简单的参数:

public class Program
{
    public static void Main()
    {
        // 创建一个带有参数的方法的委托
        ParameterizedThreadStart threadDelegate = new ParameterizedThreadStart(PrintMessage);
        
        // 创建线程,并传递委托和参数
        Thread newThread = new Thread(threadDelegate);
        newThread.Start("Hello, World!");
    }
    
    // 这个方法接受一个object类型的参数
    public static void PrintMessage(object message)
    {
        Console.WriteLine(message);
    }
}

当然传参一定要考虑他是值类型还是引用类型,这样才知道会不会影响线程安全(同时操作一个变量。) 

构造函数时限制堆栈大小

你会发现第一种多一个另外这两种构造函数分别还有对应的多一个参数的方法:

public Thread (System.Threading.ParameterizedThreadStart start, int maxStackSize);
public Thread (System.Threading.ThreadStart start, int maxStackSize);
using System;
using System.Threading;

public class WorkerThread
{
    public void DoWork()
    {
        // 线程启动时要执行的操作
        Console.WriteLine("Thread started");
        // ... 其他代码
    }
}

public class Program
{
    public static void Main()
    {
        WorkerThread worker = new WorkerThread();
        Thread newThread = new Thread(worker.DoWork, 1024 * 1024); // 指定堆栈大小为1MB
        newThread.Start();
    }
}

堆栈是什么?

内存的组织形式,如果你学过数据结构就知道有队列和栈两个概念,一般来说堆栈就是指线程中能存储的变量和方法的大小。

用于存储和管理线程的执行上下文。当一个方法调用另一个方法时,新的方法调用会压入堆栈,而返回时,最后一个方法调用会从堆栈中弹出。堆栈空间的大小决定了可以存储多少个方法调用和局部变量。

是一块较大的、动态分配的内存区域。程序在运行时可以根据需要从堆中分配和释放内存。堆内存的管理相对复杂,但它非常灵活,适合用于存储生命周期不确定的数据,比如对象或全局变量。

特点
  • 动态分配:堆中的内存是程序运行时动态分配的(通过 newmalloc 等方式)。
  • 手动管理或垃圾回收:在某些语言(如 C/C++)中,堆内存需要程序员手动释放(通过 freedelete);而在 C# 或 Java 等语言中,堆内存由垃圾回收器(GC)自动管理,程序员不需要手动释放内存。
  • 生命周期较长:堆中的内存可以在不同函数调用之间共享,数据在堆上存活的时间可以超出当前函数的执行时间。
  • 性能开销:因为堆的动态分配和释放需要操作系统的参与,并且堆的内存管理要比栈复杂,因此堆内存分配通常比栈要慢。
适用场景

堆适用于存储需要在整个程序生命周期中动态分配的较大或复杂的数据,例如:

  • 对象实例(如类的对象)
  • 动态数组、集合等

这里给出一种错误的方式:

newThread.StackSize = 1024 * 1024; // 指定堆栈大小为1MB

 这是不合法的,也没有对应的属性支持这样修改。线程的堆栈大小在创建时就固定了。

默认栈的大小

在 Windows 上的 .NET Framework 或 .NET Core:

默认栈大小通常为 1 MB(1 兆字节)。
在 64 位进程中:

默认栈大小也为 1 MB。
在 32 位进程中:

默认栈大小通常是 1 MB,但有些平台或情况下可能是 256 KB。
在 Unix 系统(如 Linux)上运行的 .NET Core 或 .NET:

通常默认栈大小也是 1 MB。

前面说到我们给我们的线程的委托可以是一个带有参数的委托,这个参数怎么传进去呢?我们的线程已经创建好了,线程的方法也准备好了,但是我们怎么启动线程呢?

启动线程

我们前文说过了使用Start方法即可,当我们创建线程对象之后,实际上并没有创建真正的线程,必须要使用Start();方法它有两种重载。

请看代码:

using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading;



void dosomething(object num)
{
    if(num==null)
    {
        Console.WriteLine("num is null");
    }
    else
    {
        Console.WriteLine("num:" + num.ToString()) ;
    }
    
    return ;
}

dosomething(12);
Thread thread = new Thread(dosomething, 1024);
//Thread thread = new Thread(new ParameterizedThreadStart(dosomething), 1024);
thread.Start(12);

Thread thread = new Thread(dosomething, 1024);
//Thread thread = new Thread(new ParameterizedThreadStart(dosomething), 1024);

这两个效果相同,原因是:

Visual Basic 和 C# 编译器从 and 方法的签名推断 ParameterizedThreadStart 委托,并调用正确的构造函数。因此,代码中可以没有显式的构造函数调用。

除了ParameterizedThreadStart 以外,实际上ThreadStart也可以这样,所以实际上你直接写一个serverrun在Thread的实例化的参数里也是可以的。

当我们创建一个带参数的委托方法用于初始化,线程开始的时候使用start方法的另外一个重载即可。如果start不带参数就会出现以下情况:

到这就差不多了解,怎么启动线程你已经知道啦!是不是用起来很简单?

到这我们通过实际操作代码知道了:

如何创建线程,如何启动线程。

线程常见操作

创建一个线程并启动之后还可以挂起、恢复、休眠、终止。

挂起

什么叫挂起?

挂起(Suspend)线程是指将线程的状态设置为非运行状态,从而暂停线程的执行。挂起线程通常是为了实现线程间的同步或等待某些条件满足后再继续执行。挂起线程后,线程不会消耗 CPU 资源,直到它被重新激活。

当线程被挂起时,它会经历以下变化:

  1. 状态变化:线程的状态从运行状态更改为非运行状态。在大多数操作系统中,线程的状态可以是就绪、运行、阻塞或挂起。当线程被挂起时,它会从运行状态转移到挂起状态。

  2. CPU 时间:线程将不再消耗 CPU 资源。这意味着线程不会执行任何代码,直到它被恢复到运行状态。

  3. 调度优先级:挂起的线程可能会失去其在调度队列中的优先级。在某些情况下,当线程被挂起时,它会从运行队列中移除,并可能被放到挂起队列中。

  4. 资源占用:线程可能会继续占用某些资源,如内存中的数据结构、文件句柄、网络连接等。这些资源可能不会立即被释放,但线程不会使用它们来执行任务。

  5. 执行上下文:线程的执行上下文(如局部变量、调用堆栈等)可能会被保留,以便线程在恢复执行时能够继续从挂起点继续执行。

  6. 等待恢复:线程必须等待被恢复到运行状态。恢复线程通常由操作系统调度器、同步机制或其他线程管理操作触发。

  7. 性能影响:挂起线程可能会影响应用程序的整体性能。当线程被挂起时,它不会执行任务,这可能会导致应用程序响应缓慢或执行延迟。

  8. 线程管理:应用程序可能会跟踪哪些线程被挂起,以便在适当的时候恢复它们。这可能涉及到线程池管理、同步对象(如 MonitorSemaphore 等)的使用或其他线程调度策略。

总之,当线程被挂起时,它会从运行状态转移到挂起状态,不再消耗 CPU 资源,并等待被恢复到运行状态。

Suspend()方法:

事实上这个方法被标记为过时方法了。

继续线程

Resume()方法:

使一个被挂起的线程恢复运行。

示例:

Thread myThread = new Thread(MyMethod);
        myThread.Start();

        // 挂起线程
        myThread.Suspend();

        // 等待一段时间或执行其他操作
        Thread.Sleep(1000);

        // 恢复线程
        myThread.Resume();

System.PlatformNotSupportedException:“Thread suspend is not supported on this platform.” 

 看起来我们的window系统不允许挂起这个操作,去看看它的实现:


        [Obsolete("Thread.Suspend has been deprecated. Use other classes in System.Threading, such as Monitor, Mutex, Event, and Semaphore, to synchronize Threads or protect resources.")]
        public void Suspend()
        {
            throw new PlatformNotSupportedException(SR.PlatformNotSupported_ThreadSuspend);
        }

哈哈哈可以了解了,这是因为在当前版本被标记为过时,现在使用它会直接报错。

当前创建的是控制台应用,.Net 8.0。我们使用更古早的版本来看看吧:

在.NET Framework 4.7.2中的表现

如果使用winform创建一个应用:

在下面的代码中存在对界面元素的操作,请忽视那些无关的内容。

Thread myThread = new Thread(mythreaddo);
chattx.Text += "\nthread start";
myThread.Start();

chattx.Text += "\nthread suspend";
// 挂起线程
myThread.Suspend();

chattx.Text += "\nmainthreadsleep start" + DateTime.Now.ToString();
// 等待一段时间或执行其他操作
Thread.Sleep(1000);

chattx.Text += "\nmainthreadsleep over" + DateTime.Now.ToString();
// 恢复线程
myThread.Resume();

//Thread th = new Thread(mythreaddo);
//th.Start();
//th.Suspend();


void mythreaddo()
{
    listView1.Invoke(new Action(() => chattx.Text += "\nthreadsleep start"+DateTime.Now.ToString()));
    Thread.Sleep(1000);
    listView1.Invoke(new Action(() => chattx.Text += "\nthreadsleep over" + DateTime.Now.ToString()));
    return;
}

哈哈,看看表现: 

we did it!我们做到了!

看看为什么:和我们之前创建的控制台程序不同,这里就是确确实实的调用了让系统挂起线程的方法。

    [SecuritySafeCritical]
    [Obsolete("Thread.Suspend has been deprecated.  Please use other classes in System.Threading, such as Monitor, Mutex, Event, and Semaphore, to synchronize Threads or protect resources.  http://go.microsoft.com/fwlink/?linkid=14202", false)]
    [SecurityPermission(SecurityAction.Demand, ControlThread = true)]
    [SecurityPermission(SecurityAction.Demand, ControlThread = true)]
    public void Suspend()
    {
        SuspendInternal();
    }




    [MethodImpl(MethodImplOptions.InternalCall)]
    [SecurityCritical]
    private extern void SuspendInternal();

当然了,这是理所应当的事情,事实上前面报错而换成winform成功非常正常,因为.NET Framework 4.7.2这个版本还是为我们的Windows系统设计的。

.NET 8 是一个开源、跨平台的框架,支持多种操作系统,如 Windows、Linux 和 macOS。.NET 8 旨在提供更高的性能、更好的开发体验和更丰富的功能。正因为跨平台性,底层的支持或许就是因为这个受到了影响。

当然了,上面展示的界面是使用socket创建的聊天室。这在完善之后会展示代码。如果可以的话,可以点个关注。

休眠

刚刚其实已经看到了休眠的结果,使用休眠方法模拟程序执行花的时间。

休眠其实和挂起看起来很相似,但是休眠属于立刻休眠,挂起时会继续执行几句以保证能够正常挂起,休眠你可以理解成让这个线程先睡一会。然后会自动苏醒。

  1. Thread.Sleep()

    • Thread.Sleep() 方法用于暂停当前线程的执行,让出 CPU 给其他线程。
    • 线程在休眠期间会保留其执行上下文,包括局部变量、调用堆栈等。当线程被唤醒时,它可以从休眠点继续执行。
    • 休眠不会释放任何资源,但它可能会导致应用程序的响应时间变慢,因为它需要等待指定的时间。
  2. Thread.Suspend() 和 Thread.Resume()

    • Thread.Suspend() 方法用于挂起当前线程的执行,挂起后的线程不会占用 CPU 时间,但会保留其执行上下文。
    • Thread.Resume() 方法用于恢复挂起的线程的执行。
    • 挂起线程时,它会保留其执行上下文,包括局部变量、调用堆栈等,以便在恢复时能够继续执行。
    • 挂起线程通常不会释放任何资源,但它可能会增加线程间的同步操作,从而影响应用程序的性能。

例子本来可以不举了,上面的代码已经举过例子了。不过也可以看看在新版本中能不能使用sleep。

答案是可以使用。 

using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading;

Thread myThread = new Thread(mythreaddo);
myThread.Start();

void mythreaddo(){
    Console.WriteLine(DateTime.Now);
    Thread.Sleep(1000);
    Console.WriteLine(DateTime.Now);
    return;
}

好奇的小伙伴可以自己尝试是不是和我说的一样。

定时休眠和无限期休眠

休眠要定时间呢:

Sleep(Int32)	
将当前线程挂起指定的毫秒数。

Sleep(TimeSpan)	
将当前线程挂起指定的时间。

int32好理解,timespan是什么?

TimeSpan 是一个表示时间间隔的结构体,它在 .NET 框架中用于表示时间的长度,例如从现在开始经过的时间量。TimeSpan 结构体可以用来表示从现在开始经过的秒数、毫秒数、分钟数、小时数等。

Thread.Sleep(TimeSpan) 方法接受一个 TimeSpan 类型的参数,并让当前线程挂起指定的时间长度。例如,如果你调用 Thread.Sleep(new TimeSpan(0, 0, 1)),这将使当前线程挂起 1 秒钟。

特殊参数

书上说如果参数为0.那么意思就是挂起线程,如果参数为-1,则为无限期阻止线程。

无限阻止线程我试了成功了!

挂起线程这个我尝试了在.Net8.0中不起作用哦。

线程的销毁后面单独再说吧,如果你觉得记录有作用,点个赞吧。

 

标签:Thread,C#,void,System,线程,new,多线程,public
From: https://blog.csdn.net/m0_54138660/article/details/142072446

相关文章

  • 每天五分钟玩转深度学习框架PyTorch:获取神经网络模型的参数
    本文重点当我们定义好神经网络之后,这个网络是由多个网络层构成的,每层都有参数,我们如何才能获取到这些参数呢?我们将再下面介绍几个方法来获取神经网络的模型参数,此文我们是为了学习第6步(优化器)。获取所有参数Parametersfromtorchimportnnnet=nn.Sequential(nn.Linear(4......
  • 优雅地安装 miniconda 和 Jupyter(从零开始~保姆式)
    本文主要参考:如何优雅地使用miniconda|安装,envs_dirs,换源,优雅地打开Jupyter_哔哩哔哩_bilibili本人亲自上手实操,堪称最佳实践,亲测特别优雅,elegant!!!1.安装minicondaminiconda官网:https://docs.conda.io/projects/miniconda/en/latest/添加到环境变量很关键,可以省去......
  • 利用AI驱动智能BI数据可视化-深度评测Amazon Quicksight(一)
    项目简介随着生成式人工智能的兴起,传统的BI报表功能已经无法满足用户对于自动化和智能化的需求,今天我们将介绍亚马逊云科技平台上的AI驱动数据可视化神器–Quicksight,利用生成式AI的能力来加速业务决策,从而提高业务生产力。借助Quicksight中集成的AmazonQ的创作功能,业务......
  • C++题目收集2
    这是本专栏的的第二篇收录集,我们一起来看一看那些有意思的题目,拓宽自己的思路。本期的题目有一些难,所以数目少一点。题目一:约瑟夫环#include<iostream>#include<vector>intjosephus(intn,intm){std::vector<int>people(n);for(inti=0;i<n;++i)......
  • 《C++ Primer Plus》学习day3
    C++11新增的内容:char16_t和char32_tchar16_t:无符号,16位,使用前缀u表示char_16字符常量和字符串常量;char32_t:无符号,32位,使用前缀U表示char32_t常量浮点类型C++有三种浮点类型:float、double、longdouble头文件cfloat中对对浮点数进行了限制:比如最低有效位......
  • AtCoder Beginner Contest 370 补题记录
    A-RaiseBothHands题意:给出Snuke举的左右手情况,如果只举左手,输出Yes,如果只举右手,输出No,否则输出Invalid思路:举左手:(l==1&&r==0)举右手:(l==1&&r==0)其他情况都是Invalidvoidsolve(){intl=read(),r=read();if(l==1&&r==0){......
  • C语言的正则表达式
    C标准库不支持正则表达式,但大部分Linux发行版本都带有第三方的正则表达式函数库。以常见的<regex.h>为例:/*regcomp将正则表达式编译成适合后续regexec函数搜索的形式preg指向模式缓冲区,传出参数regex字符串,传入参数cflag决定编译类型,可位或:-REG_EXTENDED扩展正则表达式......
  • RabbitMQ的 RPC 消息模式你会了吗?
    前文学习了如何使用工作队列在多个工作者之间分配耗时的任务。若需要在远程计算机上运行一个函数并等待结果呢?这种模式通常被称为远程过程调用(RPC)。本节使用RabbitMQ构建一个RPC系统:一个客户端和一个可扩展的RPC服务器。由于我们没有耗时的任务可以分配,因此我们将创建一......
  • RabbitMQ的 RPC 消息模式你会了吗?
    前文学习了如何使用工作队列在多个工作者之间分配耗时的任务。若需要在远程计算机上运行一个函数并等待结果呢?这种模式通常被称为远程过程调用(RPC)。本节使用RabbitMQ构建一个RPC系统:一个客户端和一个可扩展的RPC服务器。由于我们没有耗时的任务可以分配,因此我们将创建一......
  • 记录一个vscode无法ssh树莓派,但是mobaxterm可以ssh登录的问题
    一、为什么会遇到这个问题帮别人开发一个树莓派小车的时候,买了一个新的树莓派3B,回来安装好桌面系统之后开启了ssh功能,便想开始使用vscode来ssh开发,省的后续一直要插着屏幕开发,很麻烦。但是问题就来了,在确认过hostname、IP地址、端口都无误的情况下,vscode无论无何都没法ssh登录,于......