并发编程/线程同步/线程同步深入/线程池/异步编程/Tas任务

本文详细介绍了并发编程中的关键概念,包括进程与线程、线程同步以及线程池的使用。通过实例讲解了如何利用lock、Join、Monitor等方法进行线程同步,并探讨了线程池的优势和ThreadPool类的使用。此外,还讨论了异步编程的三种模式:EAP、APM和TPL,强调了TPL在.Net 4.0后的广泛应用和优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一:并发编程:

《1》:进程
通俗的讲,一个exe运行一次就会产生一个进程,一个exe的多个进程之间数据互相隔离。
1.一个进程里至少有一个线程:主线程。我们平时写的控制台程序默认就是单线程的,代码从上往下执行,一行执行完了再执行下一行;
2.什么是多线程:一个人一边烧水一边洗衣服比“先烧水再洗衣服”效率高。同一时刻一个人做多件件事情,只是在“快速频繁切换”,如果处理不当可能比不用多线程效率还低。讨论多线程先只考虑“单核cpu”。
3.普通的代码是从上向下执行的,但是多线程的代码可以“并行”执行,我们可以把“线程”理解成独立的执行单元,线程中的代码可以“并行执行”。线程根据情况被分配给一定的“时间片”来运行,可能一个线程还没执行完,就又要把时间片交给别的线程执行。把要在单独的线程放到一个方法中,然后创建 Thread 对象,运行它,这个 Thread 中的代码就会在单独的线程中执行。
4. 多线程的好处:有很大可能增加系统的运行效率;开发winform程序,避免界面卡;注册后向用户发送欢迎邮件,如果发送邮件很慢的话,避免注册过程很慢。
5. 线程默认是“非后台线程”,一个程序必须所有“非后台线程”执行结束后程序才会退出, 否则看起来好像是主线程退出了,其实进程还没有结束(搞一个winform 程序演示)把线程设置为“后台线程”后,所有“非后台线程”执行结束后程序就会退出,不会等“后台线程”执行结束:
thread.IsBackground = true;
简单的验证:控制台程序,如果不加 t1.IsBackground = true; 是线程执行结束才退出。加上的话就是“闪退”。

二:线程同步

《1》:定义:
线程同步问题就是解决多个线程同时操作一个资源的问题,多个线程对于资源的使用,必须等所有的线程使用完成之后再进行处理
while(th1.IsAlive);操作会大量消耗cpu空转,可以改成th1.Join()就是让当前线程等待th1线程的结束。


    class Program
    {
       static int count = 0;   //定义一个变量
        static void Main(string[] args)
        {
            //实例化一个线程对象用th1表示
            Thread th1 = new Thread(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    count++;
                    Console.WriteLine("th1:" + count);
                    Thread.Sleep(100);   //将当前线程挂起100ms
                }
            });
            th1.Start();   //线程的调用

            Thread th2 = new Thread(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    count++;
                    Console.WriteLine("th2:" + count);
                    Thread.Sleep(100);
                }
            });
            th2.Start();
            while (th1.IsAlive)    //获取当前线程执行状态的值并处于死循环
            {

            }
            while (th2.IsAlive)
            {

            }
            Console.WriteLine(count);     //最终输出结果count
            Console.ReadLine();
        }
    }

在这里插入图片描述

《2》:实现:
(1):Join
原理,等待耗时最长的线程执行完成之后在处理公共资源,给耗时最长的线程调用Join方法
(2):lock
改用lock解决多个线程同时操作一个资源。lock是C#中的关键字,他要锁定一个资源
lock的特点是:同时只能有一个线程进入lock的对象的范围,其他lock的线程就要等。

class Program
    {
        static int count = 0;    //定义一个静态的变量
        static Object o = new Object();    //新实例化一个静态的object对象
        static void Main(string[] args)
        {
            Console.WriteLine(DateTime.Now.ToLongTimeString());
            //实例化一个线程对象用th1表示
            Thread th1 = new Thread(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    lock (o)    //同时只能有一个线程进入lock的对象的范围,其他lock的线程就要等。
                    {
                        count++;
                    }
                    Console.WriteLine("th1:" + count);
                    Thread.Sleep(100);   //线程休眠时间100ms
                }
            });

            th1.Start();   //启动线程

            Thread th2 = new Thread(() => //Lambda表达式传值   实际上是构建了一个匿名函数 通过函数闭包来传值

            {
                for (int i = 0; i < 10; i++)
                {
                    lock (o)
                    {
                        count++;
                    }
                    Console.WriteLine("th2:" + count);
                    Thread.Sleep(100);
                }
            });
            th2.Start();
            th1.Join();   //等待线程结束
            th2.Join();
            Console.WriteLine(count);     //最终输出结果count
            Console.WriteLine(DateTime.Now.ToLongTimeString());    //输出现在当前的时间
            Console.ReadLine();
        }
    }

在这里插入图片描述
(3):注意事项
注意 lock 要锁定同一个对象,而且必须是引用类型的对象

三:线程的其他操作

《1》:线程唤醒

class Program
    {
        static void Main(string[] args)
        {
            Thread th1 = new Thread(() =>     //实例化一个新的线程,用lambda表达式传递参数
              {
                  try
                  {
                      Console.WriteLine("子线程休眠");
                      Thread.Sleep(10000);    //设置休眠的时间
                  }
                  catch (ThreadInterruptedException) //ThreadInterruptedException表示在等待状态时引发的异常
                  {

                      Console.WriteLine("子线程被唤醒");
                  }  
              });
            th1.Start();      //启动线程
            Console.WriteLine("主线程等三秒中唤醒子线程");
            Thread.Sleep(3000);
            th1.Interrupt();     //Interrupt 用于提前唤醒一个在Sleep的线程,Sleep方法会抛出ThreadInterruptedException 异常:
            Console.ReadLine();
        }
    }

在这里插入图片描述
Interrupt 用于提前唤醒一个在Sleep的线程,Sleep方法会抛出ThreadInterruptedException 异常:
(1): sleep
Sleep 是静态方法,只能是自己主动要求睡,别人不能命令他睡
线程不是越多越好,不是线程就是好的,举例子:繁忙时候的餐馆小二。具体多少合适要根据系统的配置、业务操作的不同而不同。

四:线程同步深入:

static int countMoney = 10000;
        static void Apply(string name)
        {
            Console.WriteLine("余额:"+countMoney);
            int yue = countMoney - 1;
            Console.WriteLine(name+"取钱了");
            countMoney = yue;
            Console.WriteLine(name+"取完钱,剩余"+countMoney+"元");
        }
        static void Main(string[] args)
        {
            Thread thread1 = new Thread(()=> {
                for (int i = 0; i < 1000; i++)
                {
                    Apply("th1");
                }
            });
            Thread thread2 = new Thread(()=> {
                for (int i = 0; i < 1000; i++)
                {
                    Apply("th2");
                }
            });
            thread1.Start();
            thread2.Start();
            thread1.Join();
            thread2.Join();
            Console.WriteLine("剩余余额:"+countMoney);
            Console.ReadLine();
        }

在这里插入图片描述
(1):解决思路:使用同步的技术避免两个线程同时修改一个余额。

解决方法1:最大粒度——同步方法。
Apply方法上标注[MethodImpl(MethodImplOptions.Synchronized)],这样一个方法只能同时被一个线程访问。
解决方法2:对象互斥锁
同一时刻只能有一个线程进入同一个对象的lock代码块。必须是同一个对象才能起到互斥的作用。lock 后必须是引用类型,不一定是 object,只要是对象就行。
锁对象选择很重要,选不对起不到同步的作用;选不对可能会造成其他地方被锁,比如用字符串做锁(因为字符串拘留池导致可能用的是其他地方也在用的锁)两个方法如果都用一个对象做锁,那么访问A的时候就不能访问B,因此锁选择很重要。
解决方法3(*):Monitor
其实lock关键字就是对Monitor的简化调用,lock最终就编译成Monitor,因此一般不不直接用Monitor类

static int countMoney = 10000;    //先定义总共的钱数
        [MethodImpl(MethodImplOptions.Synchronized)]    //这样一个方法只能同时被一个线程访问。
        static void Apply(string name)     //定义一个方法参数为名称
        {
            Console.WriteLine("余额:" + countMoney+"元");
            int yue = countMoney - 1;   //每次去的钱剩余的钱数都少一
            Console.WriteLine(name + "取钱了");
            countMoney = yue;    //剩余的钱
            Console.WriteLine(name + "取完钱,剩余" + countMoney + "元");
        }
        static void Main(string[] args)
        {
            Thread thread1 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    Apply("th1");   //方法的调用
                }
            });
            Thread thread2 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    Apply("th2");
                }
            });
            thread1.Start();    //启动线程
            thread2.Start();
            thread1.Join();      //等待线程结束
            thread2.Join();
            Console.WriteLine("剩余余额:" + countMoney+"元");
            Console.ReadLine();
        }
        
    }

在这里插入图片描述

(2)WaitHandle

除了锁之外,.Net 中还提供了一些线程间更自由通讯的工具,他们提供了通过“信号”进行通讯的机制,通俗的比喻为“开门”、“关门”:Set()开门,Reset()关门,WaitOne()等着开门。

(3) ManualResetEvent

ManualResetEvent 是一旦设定 Set()后就一直开门,除非调用 Reset 关门。Manual:手动;Reset:关门。

class Program
    {
        //了锁之外,.Net 中还提供了一些线程间更自由通讯的工具,他们提供了通过“信号”进行通讯的机制,
        //通俗的比喻为“开门”、“关门”:Set()开门,Reset()关门,WaitOne()等着开门。
        static void Main(string[] args)
        {
            Console.WriteLine("主线程开始");
            //ManualResetEvent 是一旦设定 Set()后就一直开门,除非调用 Reset 关门。Manual:手动;Reset:关门。
            ManualResetEvent manual = new ManualResetEvent(false);
            Thread thread = new Thread(()=> {
                Console.WriteLine("开始等待开门");
                manual.WaitOne();     //阻止当前线程,直到当前WaitHandle收到信号
                Console.WriteLine("门打开了");
            });
            thread.Start();    //启用线程
            Console.WriteLine("按下任意键开门");
            Console.ReadKey(true);    //获取按下的任意一个功能键,可以选择显示在控制台中
            Console.WriteLine("主线程结束");
            Console.ReadLine();
        }
    }

在这里插入图片描述
(4)AutoResetEvent

还有一个类 AutoResetEvent,他是在开门并且一个 WaitOne 通过后自动关门,因此命名为“AutoResetEvent”(Auto 自 动 -Reset 关 门 )

(5) 区别
ManualResetEvent就是学校的大门,开门大家都可以进,除非主动关门;
AutoResetEvent就是火车地铁的闸机口,过了一个后自动关门。

五:线程池

《1》:定义:
线程池:因为每次创建线程、销毁线程都比较消耗cpu资源,因此可以通过线程池进行优化。线程池是一组已经创建好的线程,随用随取,用完了不是销毁线程,然后放到线程池中,供其他人用。
用线程池之后就无法对线程进行精细化的控制了(线程启停、优先级控制等)
(1) ThreadPool类的一个重要方法:
static bool QueueUserWorkItem(WaitCallback callBack)
static bool QueueUserWorkItem(WaitCallback callBack, object state)
第二个重载是用来传递一个参数给线程代码的。
除非要对线程进行精细化的控制,否则建议使用线程池,因为又简单、性能调优又更好。

  public Form1()
        {
            InitializeComponent();
            CheckForIllegalCrossThreadCalls = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            ThreadPool.QueueUserWorkItem(StartDown);
        }

        void StartDown(object state)
        {
            WebClient web = new WebClient();
            string str = web.DownloadString("http://www.sina.com");
            textBox1.Text = str;
        }

这样写的话,会在 textBox1.Text = s;报异常,因为不能在其他线程中直接访问 UI 控件。需要把对 UI 控件的访问代码放到 BeginInvoke()中,BeginInvoke 的委托中的代码是运行在 UI 线程中的

 public Form1()
        {
            InitializeComponent();
            //CheckForIllegalCrossThreadCalls = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            ThreadPool.QueueUserWorkItem(StartDown);
        }

        void StartDown(object state)
        {
            WebClient web = new WebClient();
            string str = web.DownloadString("http://www.sina.com");
            this.BeginInvoke(new Action(() => {
                textBox1.Text = str;
            }) );
        }

但是也不能因此把DownloadString 等也放到 BeginInvoke 中,否则又会界面卡死了。
在任意一个UI控件上都可以调用BeginInvoke方法

五: 异步编程

.Net中很多的类接口设计的时候都考虑了多线程问题,简化了多线程程序的开发。不用自己去写WaitHandler等这些底层的代码。由于历史的发展,这些类的接口设计有着三种不同的风格:EAP()、APM()和 TPL。目前重点用 TPL。
《1》: EAP

EAP 是 Event-based Asynchronous Pattern( 基于事件的异步模型) 的简写, 类似于 Ajax 中的
XmlHttpRequest,send 之后并不是处理完成了,而是在 onreadystatechange 事件中再通知处理完成。

WebClient wc = new WebClient();
wc.DownloadStringCompleted += Wc_DownloadStringCompleted; wc.DownloadStringAsync(new Uri("http://www.baidu.com/"));
private void Wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
	MessageBox.Show(e.Result);
}

优点是简单,缺点是当实现复杂的业务的时候很麻烦,比如下载 A 成功后再下载 b,如果下载 b
成功再下载 c,否则就下载 d。
**EAP 的类的特点是:**一个异步方法配一个Completed 事件。.Net 中基于 EAP 的类比较少。也有更好的替代品,因此了解即可。
《2》:APM

APM(Asynchronous Programming Model)是.Net 旧版本中广泛使用的异步编程模型。使用了 APM 的异步方法会返回一个 IAsyncResult 对象,这个对象有一个重要的属性 AsyncWaitHandle,他是一个用来等待异步任务执行结束的一个同步信号。

FileStream fs = File.OpenRead("d:/1.txt"); 
byte[] buffer = new byte[16];
IAsyncResult aResult =fs.BeginRead(buffer, 0, buffer.Length, null, null);
aResult.AsyncWaitHandle.WaitOne();//等待任务执行结束
MessageBox.Show(Encoding.UTF8.GetString(buffer)); 
fs.EndRead(aResult);

如果不加 aResult.AsyncWaitHandle.WaitOne() 那么很有可能打印出空白,因为 BeginRead只是“开始读取”。调用完成一般要调用 EndXXX 来回收资源。
APM 的特点是:方法名字以 BeginXXX 开头,返回类型为 IAsyncResult,调用结束后需要EndXXX。
.Net 中有如下的常用类支持 APM:Stream、SqlCommand、Socket 等。
APM 还是太复杂,了解即可。

《3》TPL
TPL(Task Parallel Library)是.Net4.0之后带来的新特性,更简洁,更方便。现在在.Net平台下已经大面积使用。
TPL(Task Parallel Library)是.Net 4.0 之后带来的新特性,更简洁,更方便。现在在.Net 平台下已经大面积使用。

FileStream fs = File.OpenRead("d:/1.txt"); byte[] buffer = new byte[16];
Task<int> task = fs.ReadAsync(buffer, 0, buffer.Length); task.Wait();
MessageBox.Show("读取了"+task.Result+"个字节"); MessageBox.Show(Encoding.UTF8.GetString(buffer));

这样用和 APM 比起来的好处是:不需要 EndXXX。精彩不仅于此:

private async void button1_Click(object sender, EventArgs e)
{
FileStream fs = File.OpenRead("d:/1.txt"); byte[] buffer = new byte[16];
int len = await fs.ReadAsync(buffer, 0, buffer.Length);
MessageBox.Show("读取了" + len + "个字节"); MessageBox.Show(Encoding.UTF8.GetString(buffer));
}     

注意方法中如果有 await,则方法必须标记为 async,不是所有方法都可以被轻松的标记为 async。WinForm 中的事件处理方法都可以标记为 async、MVC 中的 Action 方法也可以标记为 async、控制台的 Main 方法不能标记为 async。
TPL 的特点是:方法都以XXXAsync 结尾,返回值类型是泛型的 Task。
TPL 让我们可以用线性的方式去编写异步程序,不再需要像 EAP 中那样搞一堆回调、逻辑跳来跳去了。await 现在已经被 JavaScript 借鉴走了!
用 await 实现“先下载 A,如果下载的内容长度大于 100 则下载 B,否则下载 C”就很容易了
再看看 WebClient 的 TPL 用法:

WebClient wc = new WebClient();
string html = await wc.DownloadStringTaskAsync("http://www.baidu.com/");//不要丢了 await MessageBox.Show(html);
WebClient wc = new WebClient();
var task = wc.DownloadStringTaskAsync("http://www.baidu.com/");
task. Wait();
MessageBox.Show(task.Result);

因为如果按照上面的写法,会卡死 UI 线程
而await 则不会 。。。 好像不是???那只是因为把 html 这么长的字符串
MessageBox.Show 很慢,MessageBox.Show(html.Substring(10));就证明了这一点
Task中的 T 是什么类型每个方法都不一样,要看文档。
WebClient、Stream、Socket 等这些“历史悠久”的类都同时提供了 APM、TPL 风格的
API,甚至有的还提供了 EAP 风格的 API。尽可能使用 TPL 风格的。
《4》 异步创建

返回值为Task,潜规则(不要求)是方法名字以 Async 结尾:

static Task<string> F2Async()
{
	return Task.Run(() => { System.Threading.Thread.Sleep(2000); return "F2";});
}

六: Tas任务

《1》:定义:
Task类的表示单个操作并返回一个值,通常以异步方式执行。Task对象是一个的中心思想基于任务的异步模式首次引入**.NET Framework 4中。 因为由执行工作Task对象通常以异步方式执行在线程池线程中**而不是以同步方式在主应用程序线程,您可以使用Status属性,以及 IsCanceled,,IsCompleted,和 IsFaulted 属性,以确定任务的状态。 大多数情况下,lambda表达式用于指定的任务是执行的工作。

《2》任务Task和线程Thread的区别

1.任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行。
2.任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制。

《3》:创建Task
Task 类还提供了构造函数对任务进行初始化。 出于性能原因,Task.Run或TaskFactory.StartNew(工厂创建) 方法是用于创建和计划计算的任务的首选的机制,但对于创建和计划必须分开的方案,您可以使用的构造函数(new一个出来),然后调用 Task.Start方法来计划任务,以在稍后某个时间执行。

    //方式1
        Task task = new Task(() =>
        {
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(1000);
                listBox1.Items.Add(DateTime.Now.ToLongTimeString());
            }
        });
        task.Start();
        //方式2
        Task task = Task.Factory.StartNew(() => {
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(1000);
                listBox1.Items.Add(DateTime.Now.ToLongTimeString());
            }
        });

《4》Task的简略生命周期

方法名说明
Created表示默认初始化任务,但是“工厂创建的”实例直接跳过。
WaitingToRun这种状态表示等待任务调度器分配线程给任务执行。
RanToCompletion任务执行完毕。

《5》Task的任务控制

方法名说明
Task.Waittask1.Wait();就是等待任务执行(task1)完成,task1的状态变为Completed。
Task.WaitAll待所有的任务都执行完成:
Task.WaitAny发同Task.WaitAll,就是等待任何一个任务完成就继续向下执行
Task.ContinueWith第一个Task完成后自动启动下一个Task,实现Task的延续
CancellationTokenSource通过cancellation的tokens来取消一个Task。

《6》任务的同步

            Task task3 = new Task(()=> {
                Task.WaitAny(task, task2);
                listBox1.Items.Add("任务一完成");
                listBox2.Items.Add("任务二完成");
            });
            task3.Start();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值