服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C# - 大家应该掌握的多线程编程

大家应该掌握的多线程编程

2022-02-17 16:08Helius-黑牛 C#

这篇文章主要介绍了大家应该掌握的多线程编程,具有一定借鉴价值,需要的朋友可以参考下

毫无疑问,多线程在各种编程语言中都占有比较重要的一个席位。不管你是初学者,还是资深的老司机,多线程是在学习,面试和工作中都要经常被提及的一个话题,下面我们就来看一看具体的相关内容。

1、多线程编程必备知识

1.1 进程与线程的概念

当我们打开一个应用程序后,操作系统就会为该应用程序分配一个进程id,例如打开qq,你将在任务管理器的进程选项卡看到qq.exe进程,如下图:

大家应该掌握的多线程编程

进程可以理解为一块包含了某些资源的内存区域,操作系统通过进程这一方式把它的工作划分为不同的单元。一个应用程序可以对应于多个进程。

线程是进程中的独立执行单元,对于操作系统而言,它通过调度线程来使应用程序工作,一个进程中至少包含一个线程,我们把该线程成为主线程。线程与进程之间的关系可以理解为:线程是进程的执行单元,操作系统通过调度线程来使应用程序工作;而进程则是线程的容器,它由操作系统创建,又在具体的执行过程中创建了线程。

1.2线程的调度

在操作系统的书中貌似有提过,“windows是抢占式多线程操作系统”。之所以这么说它是抢占式的,是因为线程可以在任意时间里被抢占,来调度另一个线程。操作系统为每个线程分配了0-31中的某一级优先级,而且会把优先级高的线程优先分配给cpu执行。

windows支持7个相对线程优先级:idle、lowest、belownormal、normal、abovenormal、highest和time-critical。其中,normal是默认的线程优先级。程序可以通过设置thread的priority属性来改变线程的优先级,该属性的类型为threadpriority枚举类型,其成员包括lowest、belownormal、normal、abovenormal和highest。clr为自己保留了idle和time-critical两个优先级。

1.3线程也分前后台

线程有前台线程和后台线程之分。在一个进程中,当所有前台线程停止运行后,clr会强制结束所有仍在运行的后台线程,这些后台线程被直接终止,却不会抛出任何异常。主线程将一直是前台线程。我们可以使用tread类来创建前台线程。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using system;
using system.threading;
 
namespace 多线程1
{
  internal class program
  {
    private static void main(string[] args)
    {
      var backthread = new thread(worker);
      backthread.isbackground = true;
      backthread.start();
      console.writeline("从主线程退出");
      console.readkey();
    }
 
    private static void worker()
    {
      thread.sleep(1000);
      console.writeline("从后台线程退出");
    }
  }
}

以上代码先通过thread类创建了一个线程对象,然后通过设置isbackground属性来指明该线程为后台线程。如果不设置这个属性,则默认为前台线程。接着调用了start的方法,此时后台线程会执行worker函数的代码。所以在这个程序中有两个线程,一个是运行main函数的主线程,一个是运行worker线程的后台线程。由于前台线程执行完毕后clr会无条件地终止后台线程的运行,所以在前面的代码中,若启动了后台线程,则主线程将会继续运行。主线程执行完后,clr发现主线程结束,会终止后台线程,然后使整个应用程序结束运行,所以worker函数中的console语句将不会执行。所以上面代码的结果是不会运行worker函数中的console语句的。

可以使用join函数的方法,确保主线程会在后台线程执行结束后才开始运行。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using system;
using system.threading;
 
namespace 多线程1
{
  internal class program
  {
    private static void main(string[] args)
    {
      var backthread = new thread(worker);
      backthread.isbackground = true;
      backthread.start();
      backthread.join();
      console.writeline("从主线程退出");
      console.readkey();
    }
 
    private static void worker()
    {
      thread.sleep(1000);
      console.writeline("从后台线程退出");
    }
  }
}

以上代码调用join函数来确保主线程会在后台线程结束后再运行。

如果你线程执行的方法需要参数,则就需要使用new thread的重载构造函数thread(parameterizedthreadstart).

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using system;
using system.threading;
 
namespace 多线程1
{
  internal class program
  {
    private static void main(string[] args)
    {
      var backthread = new thread(new parameterizedthreadstart(worker));
      backthread.isbackground = true;
      backthread.start("helius");
      backthread.join();
      console.writeline("从主线程退出");
      console.readkey();
    }
 
    private static void worker(object data)
    {
      thread.sleep(1000);
      console.writeline($"传入的参数为{data.tostring()}");
    }
  }
}

执行结果为:

大家应该掌握的多线程编程

2、线程的容器——线程池

前面我们都是通过thead类来手动创建线程的,然而线程的创建和销毁会耗费大量时间,这样的手动操作将造成性能损失。因此,为了避免因通过thread手动创建线程而造成的损失,.net引入了线程池机制。

2.1 线程池

线程池是指用来存放应用程序中要使用的线程集合,可以将它理解为一个存放线程的地方,这种集中存放的方式有利于对线程进行管理。

clr初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列,当应用程序想要执行一个异步操作时,需要调用queueuserworkitem方法来将对应的任务添加到线程池的请求队列中。线程池实现的代码会从队列中提取,并将其委派给线程池中的线程去执行。如果线程池没有空闲的线程,则线程池也会创建一个新线程去执行提取的任务。而当线程池线程完成某个任务时,线程不会被销毁,而是返回到线程池中,等待响应另一个请求。由于线程不会被销毁,所以也就避免了性能损失。记住,线程池里的线程都是后台线程,默认级别是normal。

 

2.2 通过线程池来实现多线程

要使用线程池的线程,需要调用静态方法threadpool.queueuserworkitem,以指定线程要调用的方法,该静态方法有两个重载版本:

public static bool queueuserworkitem(waitcallback callback);

public static bool queueuserworkitem(waitcallback callback,object state)

这两个方法用于向线程池队列添加一个工作先以及一个可选的状态数据。然后,这两个方法就会立即返回。下面通过实例来演示如何使用线程池来实现多线程编程。

?
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 多线程2
{
  class program
  {
    static void main(string[] args)
    {
      console.writeline($"主线程id={thread.currentthread.managedthreadid}");
      threadpool.queueuserworkitem(callbackworkitem);
      threadpool.queueuserworkitem(callbackworkitem,"work");
      thread.sleep(3000);
      console.writeline("主线程退出");
      console.readkey();
    }
 
    private static void callbackworkitem(object state)
    {
      console.writeline("线程池线程开始执行");
      if (state != null)
      {
        console.writeline($"线程池线程id={thread.currentthread.managedthreadid},传入的参数为{state.tostring()}");
      }
      else
      {
        console.writeline($"线程池线程id={thread.currentthread.managedthreadid}");
      }
    }
  }
}

结果为:

大家应该掌握的多线程编程

2.3 协作式取消线程池线程

.net framework提供了取消操作的模式,这个模式是协作式的。为了取消一个操作,必须创建一个system.threading.cancellationtokensource对象。下面还是使用代码来演示一下:

?
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 多线程3
{
  internal class program
  {
    private static void main(string[] args)
    {
      console.writeline("主线程运行");
      var cts = new cancellationtokensource();
      threadpool.queueuserworkitem(callback, cts.token);
      console.writeline("按下回车键来取消操作");
      console.read();
      cts.cancel();
      console.readkey();
    }
 
    private static void callback(object state)
    {
      var token = (cancellationtoken) state;
      console.writeline("开始计数");
      count(token, 1000);
    }
 
    private static void count(cancellationtoken token, int count)
    {
      for (var i = 0; i < count; i++)
      {
        if (token.iscancellationrequested)
        {
          console.writeline("计数取消");
          return;
        }
        console.writeline($"计数为:{i}");
        thread.sleep(300);
      }
      console.writeline("计数完成");
    }
  }
}

结果为:

大家应该掌握的多线程编程

3、线程同步

线程同步计数是指多线程程序中,为了保证后者线程,只有等待前者线程完成之后才能继续执行。这就好比生活中排队买票,在前面的人没买到票之前,后面的人必须等待。

3.1 多线程程序中存在的隐患

多线程可能同时去访问一个共享资源,这将损坏资源中所保存的数据。这种情况下,只能采用线程同步技术。

3.2 使用监视器对象实现线程同步

监视器对象(monitor)能够确保线程拥有对共享资源的互斥访问权,c#通过lock关键字来提供简化的语法。

?
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
using system;
using system.collections.generic;
using system.linq;
using system.text;
using system.threading;
using system.threading.tasks;
 
namespace 线程同步
{
  class program
  {
    private static int tickets = 100;
    static object globalobj=new object();
    static void main(string[] args)
    {
      thread thread1=new thread(saleticketthread1);
      thread thread2=new thread(saleticketthread2);
      thread1.start();
      thread2.start();
      console.readkey();
    }
 
    private static void saleticketthread2()
    {
      while (true)
      {
        try
        {
          monitor.enter(globalobj);
          thread.sleep(1);
          if (tickets > 0)
          {
            console.writeline($"线程2出票:{tickets--}");
          }
          else
          {
            break;
          }
        }
        catch (exception)
        {
          throw;
        }
        finally
        {
          monitor.exit(globalobj);
        }
      }
    }
 
    private static void saleticketthread1()
    {
      while (true)
      {
        try
        {
          monitor.enter(globalobj);
          thread.sleep(1);
          if (tickets > 0)
          {
            console.writeline($"线程1出票:{tickets--}");
          }
          else
          {
            break;
          }
        }
        catch (exception)
        {
          throw;
        }
        finally
        {
          monitor.exit(globalobj);
        }
      }
    }
  }
}

在以上代码中,首先额外定义了一个静态全局变量globalobj,并将其作为参数传递给enter方法。使用了monitor锁定的对象需要为引用类型,而不能为值类型。因为在将值类型传递给enter时,它将被先装箱为一个单独的毒香,之后再传递给enter方法;而在将变量传递给exit方法时,也会创建一个单独的引用对象。此时,传递给enter方法的对象和传递给exit方法的对象不同,monitor将会引发synchronizationlockexception异常。

3.3线程同步技术存在的问题

(1)使用比较繁琐。要用额外的代码把多个线程同时访问的数据包围起来,还并不能遗漏。

(2)使用线程同步会影响程序性能。因为获取和释放同步锁是需要时间的;并且决定那个线程先获得锁的时候,cpu也要进行协调。这些额外的工作都会对性能造成影响。

(3)线程同步每次只允许一个线程访问资源,这会导致线程堵塞。继而系统会创建更多的线程,cpu也就要负担更繁重的调度工作。这个过程会对性能造成影响。

下面就由代码来解释一下性能的差距:

?
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
using system;
using system.collections.generic;
using system.diagnostics;
using system.linq;
using system.text;
using system.threading;
using system.threading.tasks;
 
namespace 线程同步2
{
  class program
  {
    static void main(string[] args)
    {
      int x = 0;
      const int iterationnumber = 5000000;
      stopwatch stopwatch=stopwatch.startnew();
      for (int i = 0; i < iterationnumber; i++)
      {
        x++;
      }
      console.writeline($"不使用锁的情况下花费的时间:{stopwatch.elapsedmilliseconds}ms");
      stopwatch.restart();
      for (int i = 0; i < iterationnumber; i++)
      {
        interlocked.increment(ref x);
      }
      console.writeline($"使用锁的情况下花费的时间:{stopwatch.elapsedmilliseconds}ms");
      console.readkey();
    }
  }
}

执行结果:

大家应该掌握的多线程编程

总结

以上就是本文关于大家应该掌握的多线程编程的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

原文链接:http://www.cnblogs.com/Helius/archive/2016/08/22/5793838.html

延伸 · 阅读

精彩推荐
  • C#如何使用C#将Tensorflow训练的.pb文件用在生产环境详解

    如何使用C#将Tensorflow训练的.pb文件用在生产环境详解

    这篇文章主要给大家介绍了关于如何使用C#将Tensorflow训练的.pb文件用在生产环境的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴...

    bbird201811792022-03-05
  • C#C#微信公众号与订阅号接口开发示例代码

    C#微信公众号与订阅号接口开发示例代码

    这篇文章主要介绍了C#微信公众号与订阅号接口开发示例代码,结合实例形式简单分析了C#针对微信接口的调用与处理技巧,需要的朋友可以参考下...

    smartsmile20127762021-11-25
  • C#C#设计模式之Strategy策略模式解决007大破密码危机问题示例

    C#设计模式之Strategy策略模式解决007大破密码危机问题示例

    这篇文章主要介绍了C#设计模式之Strategy策略模式解决007大破密码危机问题,简单描述了策略模式的定义并结合加密解密算法实例分析了C#策略模式的具体使用...

    GhostRider10972022-01-21
  • C#利用C#实现网络爬虫

    利用C#实现网络爬虫

    这篇文章主要介绍了利用C#实现网络爬虫,完整的介绍了C#实现网络爬虫详细过程,感兴趣的小伙伴们可以参考一下...

    C#教程网11852021-11-16
  • C#SQLite在C#中的安装与操作技巧

    SQLite在C#中的安装与操作技巧

    SQLite,是一款轻型的数据库,用于本地的数据储存。其优点有很多,下面通过本文给大家介绍SQLite在C#中的安装与操作技巧,感兴趣的的朋友参考下吧...

    蓝曈魅11162022-01-20
  • C#三十分钟快速掌握C# 6.0知识点

    三十分钟快速掌握C# 6.0知识点

    这篇文章主要介绍了C# 6.0的相关知识点,文中介绍的非常详细,通过这篇文字可以让大家在三十分钟内快速的掌握C# 6.0,需要的朋友可以参考借鉴,下面来...

    雨夜潇湘8272021-12-28
  • C#深入理解C#的数组

    深入理解C#的数组

    本篇文章主要介绍了C#的数组,数组是一种数据结构,详细的介绍了数组的声明和访问等,有兴趣的可以了解一下。...

    佳园9492021-12-10
  • C#VS2012 程序打包部署图文详解

    VS2012 程序打包部署图文详解

    VS2012虽然没有集成打包工具,但它为我们提供了下载的端口,需要我们手动安装一个插件InstallShield。网上有很多第三方的打包工具,但为什么偏要使用微软...

    张信秀7712021-12-15