C#多线程编程

相关问题

什么是进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

什么是线程

线程(Thread)是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。简单来说,不同的线程可以执行相同的代码。

什么是多线程

多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

多线程的优点

可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。

多线程的缺点

  • 线程需要占用内存,线程越多占用内存也越多;
  • 多线程需要协调和管理,所以需要CPU时间跟踪线程;
  • 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
  • 线程太多会导致控制太复杂,最终可能造成很多Bug。

C#实现多线程的方法

Thread类

Thread类通过委托传递,执行线程的函数可以不带参数,也可以带有一个参数,但是参数的类型需要为object类型,在需要的时候进行类型转换。如果要传递多个参数,可以通过一个类或者结构体来封装需要传递的参数。

Thread类C#代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
using System.Threading;

namespace ThreadSample
{
class Program
{
static void Main(string[] args)
{

Thread t1 = new Thread(new ThreadStart(TestMethod));
Thread t2 = new Thread(new ParameterizedThreadStart(TestMethod));
t1.IsBackground = true;
t2.IsBackground = true;
t1.Start();
t2.Start("Hello");
Console.ReadKey();
}

public static void TestMethod()
{

Console.WriteLine("不带参数的线程函数");
}

//参数的类型必须为object类型
public static void TestMethod(object data)
{

string datastr = (string) data;
Console.WriteLine("带参数的线程函数,参数为:{0}", datastr);
}
}
}

关于多个参数的线程创建,请参照C#创建带参数的线程

下面给出了两种方法的示例,首先是使用类作为参数的传递,此种方式比较常用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System;
using System.Threading;

namespace ThreadSample
{
class Program
{
static void Main(string[] args)
{

MyThread mt = new MyThread(100);
Thread thread = new Thread(new ThreadStart(mt.Calculate));
thread.Start();
//等待线程结束
while (thread.ThreadState != ThreadState.Stopped)
{
Thread.Sleep(10);
}
Console.WriteLine(mt.Result); //打印返回值
Console.Read();
}
}

public class MyThread//线程类
{
public int Parame { set; get; } //参数
public int Result { set; get; } //返回值
//构造函数
public MyThread(int parame)
{

this.Parame = parame;
}
//线程执行方法
public void Calculate()
{

Random ra = new Random(); //随机数对象
Thread.Sleep(ra.Next(10, 100)); //随机休眠一段时间
Console.WriteLine(this.Parame);
this.Result = this.Parame * ra.Next(10, 100);
}
}
}

下面是使用匿名方法作为参数,这种方法十分的灵活:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;
using System.Threading;

namespace ThreadSample
{
class Program
{
static void Main(string[] args)
{

int Parame = 100; //当做参数
int Result = 0; //当做返回值
//匿名方法
ThreadStart threadStart = new ThreadStart(delegate()
{
Random ra = new Random(); //随机数对象
Thread.Sleep(ra.Next(10, 100)); //随机休眠一段时间
Console.WriteLine(Parame); //输出参数
Result = Parame * ra.Next(10, 100); //计算返回值
});
Thread thread = new Thread(threadStart);
thread.Start(); //多线程启动匿名方法
//等待线程结束
while (thread.ThreadState != ThreadState.Stopped)
{
Thread.Sleep(10);
}
Console.WriteLine(Result); //打印返回值
Console.Read();
}
}
}

线程池

由于线程的创建和销毁需要耗费一定的开销,过多的使用线程会造成内存资源的浪费,出于对性能的考虑,于是引入了线程池(ThreadPool)的概念。线程池维护一个请求队列,线程池的代码从队列提取任务,然后委派给线程池的一个线程执行,线程执行完不会被立即销毁,这样既可以在后台执行任务,又可以减少线程创建和销毁所带来的开销。

线程池线程默认为后台线程(IsBackground),示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Threading;

namespace ThreadSample
{
class Program
{
static void Main(string[] args)
{

//将工作项加入到线程池队列中,这里可以传递一个线程参数
ThreadPool.QueueUserWorkItem(Test, "Hello");
ThreadPool.QueueUserWorkItem(Test, "World");
Console.ReadKey();
}

public static void Test(object data)
{

string datastr = (string) data;
Console.WriteLine(datastr);
}
}
}

Task类

使用ThreadPoolQueueUserWorkItem()方法发起一次异步的线程执行很简单,但是该方法最大的问题是没有一个内建的机制让你知道操作什么时候完成,有没有一个内建的机制在操作完成后获得一个返回值。为此,可以使用System.Threading.Tasks中的Task类。

构造一个Task<TResult>对象,并为泛型TResult参数传递一个操作的返回类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
using System.Threading;

namespace ThreadSample
{
class Program
{
private static void Main(string[] args)
{

Task<Int32> t = new Task<Int32>(n => Sum((Int32) n), 1000);
t.Start();
t.Wait();
Console.WriteLine(t.Result);
Console.ReadKey();
}

private static Int32 Sum(Int32 n)
{

Int32 sum = 0;
for (; n > 0; --n)
{
checked
{
sum += n;
} //结果太大,抛出异常
}
return sum;
}
}
}

一个任务完成时,自动启动一个新任务;一个任务完成后,它可以启动另一个任务,下面重写了前面的代码,不阻塞任何线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
using System.Threading.Tasks;

namespace ThreadSample
{
class Program
{
static void Main(string[] args)
{

Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 1000);
t.Start();
//t.Wait();
Task cwt = t.ContinueWith(task => Console.WriteLine("The result is {0}", t.Result));
Console.ReadKey();
}

private static Int32 Sum(Int32 n)
{

Int32 sum = 0;
for (; n > 0; --n)
{
checked
{
sum += n;
} //结果溢出,抛出异常
}
return sum;
}
}
}

使用委托开启多线程

用委托(Delegate)的BeginInvokeEndInvoke方法操作线程,BeginInvoke方法可以使用线程异步地执行委托所指向的方法。然后通过EndInvoke方法获得方法的返回值(EndInvoke方法的返回值就是被调用方法的返回值),或是确定方法已经被成功调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;
using System.Threading;

namespace ThreadSample
{
class Program
{
private delegate int NewTaskDelegate(int ms);
private static int newTask(int ms)
{
Console.WriteLine("任务开始");
Thread.Sleep(ms);
Random random = new Random();
int n = random.Next(10000);
Console.WriteLine("任务完成");
return n;
}

static void Main(string[] args)
{
NewTaskDelegate task = newTask;
IAsyncResult asyncResult = task.BeginInvoke(2000, null, null);
//EndInvoke方法将被阻塞2
int result = task.EndInvoke(asyncResult);
Console.WriteLine(result);
Console.Read();
}
}
}

同时,我们也可以使用IAsyncResult.IsCompleted属性来判断异步调用是否完成,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Threading;

namespace ThreadSample
{
class Program
{
private delegate int NewTaskDelegate(int ms);
private static int newTask(int ms)
{
Console.WriteLine("任务开始");
Thread.Sleep(ms);
Random random = new Random();
int n = random.Next(10000);
Console.WriteLine("任务完成");
return n;
}

static void Main(string[] args)
{
NewTaskDelegate task = newTask;
IAsyncResult asyncResult = task.BeginInvoke(2000, null, null);
//等待异步执行完成
while (!asyncResult.IsCompleted)
{
Console.Write("*");
Thread.Sleep(100);
}
// 由于异步调用已经完成,因此, EndInvoke会立刻返回结果
int result = task.EndInvoke(asyncResult);
Console.WriteLine(result);
Console.Read();
}
}
}

我们也可以使用WaitOne方法等待异步方法执行完成。WaitOne的第一个参数表示要等待的毫秒数,在指定时间之内,WaitOne方法将一直等待,直到异步调用完成,并发出通知,WaitOne方法才返回true。当等待指定时间之后,异步调用仍未完成,WaitOne方法返回false,如果指定时间为0,表示不等待,如果为-1,表示永远等待,直到异步调用完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System;
using System.Threading;

namespace ThreadSample
{
class Program
{
private delegate int NewTaskDelegate(int ms);
private static int newTask(int ms)
{
Console.WriteLine("任务开始");
Thread.Sleep(ms);
Random random = new Random();
int n = random.Next(10000);
Console.WriteLine("任务完成");
return n;
}

static void Main(string[] args)
{
NewTaskDelegate task = newTask;
IAsyncResult asyncResult = task.BeginInvoke(2000, null, null);
//等待异步执行完成
while (!asyncResult.AsyncWaitHandle.WaitOne(100, false))
{
Console.Write("*");
}
int result = task.EndInvoke(asyncResult);
Console.WriteLine(result);
Console.Read();
}
}
}

同样也可以使用使用回调方式返回结果。要注意的是my.BeginInvoke(3, 300, MethodCompleted, my)BeginInvoke方法的参数传递方式:

  • 前面一部分(3, 300)是其委托本身的参数;
  • 参数MethodCompleted是回调方法委托类型,他是回调方法的委托,此委托没有返回值,有一个IAsyncResult类型的参数,当method方法执行完后,系统会自动调用MethodCompleted方法。
  • 最后一个参数my需要向MethodCompleted方法中传递一些值,一般可以传递被调用方法的委托,这个值可以使用IAsyncResult.AsyncState属性获得。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;
using System.Threading;

namespace ThreadSample
{
class Program
{
private delegate int MyMethod(int second, int millisecond);
//线程执行方法
private static int method(int second, int millisecond)
{
Console.WriteLine("线程休眠" + (second * 1000 + millisecond) + "毫秒");
Thread.Sleep(second * 1000 + millisecond);
Random random = new Random();
return random.Next(10000);
}

//回调方法
private static void MethodCompleted(IAsyncResult asyncResult)
{
if (asyncResult == null || asyncResult.AsyncState == null)
{
Console.WriteLine("回调失败!!!");
return;
}
int result = (asyncResult.AsyncState as MyMethod).EndInvoke(asyncResult);
Console.WriteLine("任务完成,结果:" + result);
}

static void Main(string[] args)
{
MyMethod my = method;
IAsyncResult asyncResult = my.BeginInvoke(3, 300, MethodCompleted, my);
Console.WriteLine("任务开始");
Console.Read();
}
}
}

使用BackgroundWorker组件来实现

当在设计WinForm应用程序的时候,如果有一段代码非常的耗费时间,并且我们需要很好的控制代码的执行,或者是返回代码的执行进度,这时候就可以使BackgroundWorker组件了。如果是做WinForm程序开发,可以从工具箱中拖放BackgroundWorker组件到应用程序,也可以在应用程序中添加一个BackgroundWorker实例:

1
BackgroundWorker bgw = new BackgroundWorker();

BackgroundWorker组件常用的操作:
bgw.RunWorkerAsync():
开始后台运行执行,该函数后将触发bgw.DoWorker事件,需要执行的操作写在DoWorker事件响应函数里。该函数也可以加参数,参数从DoWorker事件处理函数的e.Arguement里获取;

bgw.CancelAsync():
申请后台程序停止,注意该函数不能实际停止后台程序,只能将bgwCancellationPending值设为true,需要自己在后台运行的程序中判断这一值,进而停止后台程序的运行。注意本方法使用前,需要将bgwWorkerSupportsCancellation值设为true,否则将不起作用。

bgw.ReportProgress():
在后台程序中调用,向主线程传送进度信息。可以带一个或两个参数,一个为int类型的进度(0 ~ 100),一个为自定义类型的参数,可以传任意信息。调用后,将触发bgw.ProgressChanged事件,可以将界面变化的代码写在该事件响应函数中,之前提到的两个参数均可从bgw.ProgressChanged事件响应函数的参数e中获取,分别为e.ProgressPercentagee.UserState。本方法使用前,需要将bgwWorkerReportsProgress值设为true,否则将不会触发事件。

完整的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
BackgroundWorker bgw = new BackgroundWorker();
//用于显示后台进度
bgw.WorkerReportsProgress = true;
//支持取消后台正在执行的操作
bgw.WorkerSupportsCancellation = true;

//在另一个线程里开始操作(btnStart是一个按钮控件)
//也可以利用RunWokerAsync()方法传递参数,
private void btnStart_Click(object sender, EventArgs e)
{

bgw.RunWokerAsync(2000/*参数是可选的*/);
}

//支持取消后台正在执行的操作
private void btnCancel_Click(object sender, EventArgs e)
{

bgw.CancelAsync();
}

//DoWork事件在另一个线程里执行
private void bgw_DoWork(objectsender, DoWorkeventArgs e)
{

for (int i = 1; i < 11; i++)
{
//允许长时间的操作
int input = (int)e.Argument;
Thread.Sleep(input);
//报告后台执行的进度
bgw.ReportProgress(i * 10);
//判断是否发出了取消的指令
if (bgw.CancellationPending)
{
e.Cancel = true;
return;
}
}
//在此处设置返回值
e.Result = 10;
}

//用来接收报告回来的进度
private void bgw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{

//progressBar1是ProgressBar控件
progressBar1.Value = e.ProgressPercentage;
}

//将后台操作的结果反馈给用户
private void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{

if(e.Cancelled)
{
MessageBox.Show("Operation Cancelled");
}
else
{
MessageBox.Show("OperationCompleted");
}
//在此处接收传递回来的值
int returnValue = (int)e.Result;
}

相关资料

[1] C#多线程编程 By 阿凡卢

[2] C#多线程学习(一) 多线程的相关概念 By xugang

[3] C#多线程的应用全面解析

[4] C#创建带参数的线程 By 熊猫大叔

[5] 使用BackgroundWorker组件 By inforasc

[6] BackgroundWorker的使用 By rrrrssss00