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

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

环境: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中不起作用哦。

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

 

  • 25
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值