Thread
进程是执行的程序,是系统分配资源的基本单元,一个进程中有多个线程。
线程是cpu分派和调度的基本单元,共享进程资源。
多线程就好比在qq文字聊天的同时外面能发送文件,这两个可以同时进行。
创建线程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sTdKLGrV-1626439614472)(异步编程.assets/image-20210715195908308.png)]
官方文档可以看出线程的构造函数接受两种类型的委托
public Thread (System.Threading.ParameterizedThreadStart start);
public Thread (System.Threading.ThreadStart start);
ParameterizedThreadStart 委托
public delegate void ParameterizedThreadStart(object? obj);
ParameterizedThreadStart
委托在创建线程时可以接受带有一个参数Object 不带返回值
的方法。- 传入委托的方法参数一定是 Object 哦。
- 话说是只有一个参数,但是这可是Object类型啊,你想要传多个数据可以把数据封装成class,传个对象过去,
- 虽然不能ref 和 out ,但是你传class对象的话,改不了对象的引用但是可以改引用堆地址处的数据啊,所以可以通过另一种途径获取返回值。
- 另外有传递数据更简便的方法是传lambda表达式,不过其实lambda表达式
Example:
主线程和创建的线程异步遍历输出10次,myNum的数据在创建的线程中改变。
namespace Thread_CreateTest
{
class MyNum
{
public double length { set; get; }
public double width { set; get; }
public double area { get { return width * length; } }
}
class Program
{
public static void ThreadMethod1(Object obj)
{
if (obj is MyNum)
{
MyNum myNum = (MyNum)obj;
myNum.width = 10;
myNum.length = 4.5;
}
else
{
Console.WriteLine(obj);
}
for (int i = 1; i < 10; i++)
{
Console.WriteLine($"-- ThreadMethod1 -- {Thread.CurrentThread.Name}\t {Thread.CurrentThread.IsThreadPoolThread}\t ");
}
}
static void Main(string[] args)
{
Thread.CurrentThread.Name = "Main_name";
Thread myThread1 = new Thread(ThreadMethod1);
myThread1.Name = "myThread1_name";
MyNum myNum = new MyNum();
myThread1.Start(myNum);
for (int i = 1; i < 10; i++)
{
Console.WriteLine($"-- Main -- {Thread.CurrentThread.Name}\t {Thread.CurrentThread.IsThreadPoolThread}\t ");
}
myThread1.Join();
Console.WriteLine($"myNum length:{myNum.length}\t width:{myNum.width}\t area:{myNum.area}");
Console.ReadKey();
//--Main-- Main_name False
//--Main-- Main_name False
//--ThreadMethod1-- myThread1_name False
//--ThreadMethod1-- myThread1_name False
//--ThreadMethod1-- myThread1_name False
//--ThreadMethod1-- myThread1_name False
//--ThreadMethod1-- myThread1_name False
//--ThreadMethod1-- myThread1_name False
//--ThreadMethod1-- myThread1_name False
//--ThreadMethod1-- myThread1_name False
//--ThreadMethod1-- myThread1_name False
//--Main-- Main_name False
//--Main-- Main_name False
//--Main-- Main_name False
//--Main-- Main_name False
//--Main-- Main_name False
//--Main-- Main_name False
//--Main-- Main_name False
//myNum length:4.5 width: 10 area: 45
}
}
}
ThreadStart 委托
public delegate void ThreadStart();
ThreadStart委托在创建线程时 不接受有参数有返回值的方法
使用和ParameterizedThreadStart
差不多,只是没有参数而已,不赘述。
传递参数
在Thread构造函数中稍稍介绍了一下,这里简单举例:
class Program
{
static void Main(string[] args)
{
var sample = new ThreadArgs(10);
// plan1 将要传递的参数设为实例对象的字段或属性,为实例的无参方法开线程
var threadOne = new Thread(sample.CountNumbers);
threadOne.Name = "ThreadOne";
threadOne.Start();
threadOne.Join();
Console.WriteLine("--------------------------");
// plan2 带一个参数的ParameterizedThreadStart委托,线程Start时传参
var threadTwo = new Thread(Count);
threadTwo.Name = "ThreadTwo";
threadTwo.Start(8);
threadTwo.Join();
Console.WriteLine("--------------------------");
// plan3 lambda表达式
var threadThree = new Thread(() => CountNumbers(12));
threadThree.Name = "ThreadThree";
threadThree.Start();
threadThree.Join();
Console.WriteLine("--------------------------");
// lambda变量捕获有一个弊端就是 外部变量会被捕获它的所有方法共享,一处变处处变
int i = 10;
var threadFour = new Thread(() => PrintNumber(i));
i = 20;
var threadFive = new Thread(() => PrintNumber(i));
threadFour.Start();
// 20
threadFive.Start();
// 20
Console.ReadKey();
}
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 ThreadArgs
{
private readonly int _iterations;
public ThreadArgs(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);
}
}
}
}
相关方法
join()
在调用join()的实例线程终止前阻止调用当前线程,实现两个线程之间同步执行。
虽然加上了超时时间,但是该时间内没有终止也只是会返回false并不会去终止它。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lxxEdp5N-1626439614475)(多线程.assets/image-20210716094418352.png)]
public void Join ();
public bool Join (int millisecondsTimeout);// 毫秒数
public bool Join (TimeSpan timeout);
TimeSpan
:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hapMIwNQ-1626439614477)(多线程.assets/image-20210716094704343.png)]
public class Example
{
static Thread thread1, thread2;
public static void Main()
{
thread1 = new Thread(ThreadProc);
thread1.Name = "Thread1";
thread1.Start();
thread2 = new Thread(ThreadProc);
thread2.Name = "Thread2";
thread2.Start();
Console.ReadKey();
}
private static void ThreadProc()
{
Console.WriteLine("\nCurrent thread: {0}", Thread.CurrentThread.Name);
if (Thread.CurrentThread.Name == "Thread1" &&
thread2.ThreadState != ThreadState.Unstarted)
Console.WriteLine($"Is Thread2 finsh:{thread2.Join(new TimeSpan(0,0,1))}");
Thread.Sleep(4000);
Console.WriteLine("\nCurrent thread: {0}", Thread.CurrentThread.Name);
Console.WriteLine("Thread1: {0}", thread1.ThreadState);
Console.WriteLine("Thread2: {0}\n", thread2.ThreadState);
}
}
// The example displays output like the following:
//Current thread: Thread1
//Current thread: Thread2
//Is Thread2 finsh:False
//Current thread: Thread2
//Thread1: WaitSleepJoin
//Thread2: Running
//Current thread: Thread1
//Thread1: Running
//Thread2: Stopped
sleep()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3h56gFcz-1626439614479)(多线程.assets/image-20210716101012539.png)]
参数的值为零,则该线程会将其时间片的剩余部分让给任何已经准备好运行的、具有同等优先级的线程。 如果没有其他已经准备好运行的、具有同等优先级的线程,则不会挂起当前线程的执行。
前后台线程
- 平时我们显式创建的都是前台线程,ThreadPool和Task的都是后台线程,可以通过设置
IsBackground = true
来手动设置后台线程。 - 进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。
- 如果程序定义了一个不会完成的前台线程,主程序并不会正常结束。
线程安全
本地资源
共享资源
- 多个线程引用到同一个对象的实例,那么就会共享该实例的数据
- 静态字段也会在线程间共享
- 被lambda表达式或者匿名委托捕获的本地变量,也会被编译器转为字段,也会被共享。
有共享变量在多线程调度时就容易产生线程安全。
Example:
两个线程模拟两个窗口卖票,票是共享数据,这里用实例对象实现共享
namespace Thread_SafeTest
{
class Ticket
{
public int ticketNum{set;get;}
public Ticket(int maxnum)
{
ticketNum = maxnum;
}
}
class Program
{
static void Main(string[] args)
{
Ticket ticket = new Ticket(100);
Thread thread1 = new Thread(SaleTicket);
thread1.Name = "Thread1";
Thread thread2 = new Thread(SaleTicket);
thread2.Name = "Thread2";
thread1.Start(ticket);
thread2.Start(ticket);
Console.WriteLine("Main--");
Console.ReadKey();
}
public static void SaleTicket(Object ticketObj)
{
Ticket tickets = ticketObj as Ticket;
if (tickets != null)
{
while(tickets.ticketNum!=0)
Console.WriteLine($"{Thread.CurrentThread.Name} --- saled --- {tickets.ticketNum--}");
}
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oVu69PDK-1626439614481)(多线程.assets/image-20210716104413014.png)]
结果可以看出,多线程对共享资源的访问引发了安全问题,thread1 运行到 票数自减时cpu资源被抢占,Thread2在Thread1票减一前拿到了资源就造成这种卖同一张票。
如何解决
加锁,lock可以基于任何引用类型对象。
如果锁定了一个对象,需要访问该对象的所有其他线程则会处于阻塞状态,直到该对象解除锁定。不过这可能会导致严重的性能问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JKwEN366-1626439614482)(多线程.assets/image-20210716160457427.png)]
信号
ThreadPool
ThreadPool线程都是后台线程,前台线程结束不论后台线程执行完成与否都会结束,finally也不会执行。
ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务,任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用。当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行。线程池能减少线程的创建,节省开销。
Thread是基于操作系统级别的线程,而ThreadPool和Task不会创建自己的操作系统线程,二者是由任务调度器(TaskScheduler)执行,默认的调度程序仅仅在ThreadPool上运行,
ThreadPool也可以创建线程,把方法加入到执行队列中等待
ThreadPool.QueueUserWorkItem 方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B7tJH8zO-1626439614483)(多线程.assets/image-20210716160656800.png)]
class Program
{
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem((Object obj)=>Console.WriteLine($"Is ThreadPool :{Thread.CurrentThread.IsThreadPoolThread}"));
Console.WriteLine("Main");
Console.ReadKey();
}
}
需要传方法参数时可以用第二个方法第二个参数传。
Task
Task的线程也是ThreadPool里的线程,也是后台线程
与ThreadPool不同,Task可以在指定时间返回完成结果,并且还可以通过ContinueWith延续任务,以使得任务执行完毕后运行更多操作,如果已完成立即进行回调,也可以调用Wait来同步等待任务完成,如同Thread.Join一样阻塞线程执行,直到任务完成
Task创建线程三种方式
class Program
{
static void Main(string[] args)
{
//1.new方式实例化一个Task,需要通过Start方法启动
Task task = new Task(() =>
{
Thread.Sleep(100);
Console.WriteLine($"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId} 是线程池线程{Thread.CurrentThread.IsThreadPoolThread}");
});
task.Start();
//2.Task.Factory.StartNew(Action action)创建和启动一个Task
Task task2 = Task.Factory.StartNew(() =>
{
Thread.Sleep(100);
Console.WriteLine($"hello, task2的线程ID为{ Thread.CurrentThread.ManagedThreadId} 是线程池线程{Thread.CurrentThread.IsThreadPoolThread}");
});
//3.Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
Task task3 = Task.Run(() =>
{
Thread.Sleep(100);
Console.WriteLine($"hello, task3的线程ID为{ Thread.CurrentThread.ManagedThreadId} 是线程池线程{Thread.CurrentThread.IsThreadPoolThread}");
});
Console.WriteLine("执行主线程!");
Console.ReadKey();
//执行主线程!
//hello, task1的线程ID为3 是线程池线程True
//hello, task2的线程ID为4 是线程池线程True
//hello, task3的线程ID为5 是线程池线程True
}
}
Task阻塞
Task延续操作
async/await
C# 5.0
之后引入了async
和await
关键字,在语言层面给予了并发更好的支持。
async
用于标记异步方法:
async
关键字是上下文关键字,只有在修饰方法与Lambda时才会被当作关键字处理,在其它区域将被作为标识符处理。async
关键字可以标记静态方法,但不能标记入口点(Main()
方法)。async
标记的方法返回值必须为Task
、Task<TResult>
、void
其中之一。
await
用于等待异步方法的结果:
await
关键字同样是上下文关键字,只有在async
标记的方法中才被视为关键字。await
关键字可以用在async
方法和Task
、Task<TResult>
之前,用于等待异步任务执行结束。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9lxpev6n-1626439614484)(多线程.assets/2a0638cddd6442f3a93d387706fa87d.png)]
并不是方法使用async
关键字标记了就是异步方法,直接出现在async
方法内部的语句也是同步执行的,异步执行的内容需要使用Task
类执行。
事实上,一个不包含任何await
语句的async
方法将是同步执行的,此时编译器会给出警告。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-znelSwEP-1626439614485)(多线程.assets/image-20210715103210425.png)]
在上图中main输出结束了还等待了await完成,是不是说明主线程会等待异步完成再结束呢??
这就要分情况讨论啦,上例中异步调用返回值是void,主线程是并不会等待它异步完成再结束的
异步方法返回类型
-
void
-
Task
-
Task< T>
-
await
关键字同样是上下文关键字,只有在async
标记的方法中才被视为关键字。 -
await
关键字可以用在async
方法和Task
、Task<TResult>
之前,用于等待异步任务执行结束。
[外链图片转存中…(img-9lxpev6n-1626439614484)]
并不是方法使用async
关键字标记了就是异步方法,直接出现在async
方法内部的语句也是同步执行的,异步执行的内容需要使用Task
类执行。
事实上,一个不包含任何await
语句的async
方法将是同步执行的,此时编译器会给出警告。
[外链图片转存中…(img-znelSwEP-1626439614485)]
在上图中main输出结束了还等待了await完成,是不是说明主线程会等待异步完成再结束呢??
这就要分情况讨论啦,上例中异步调用返回值是void,主线程是并不会等待它异步完成再结束的
异步方法返回类型
- void
- Task
- Task< T>