多线程学习一(多线程基础)

前言

多线程、单线程、进程、任务、线程池...等等一些术语到底是什么意思呢?到底什么是多线程?它到底怎么用?我们一起来学习一下多线程的处理

如何理解

进程:进程是给定程序当前正在执行的实例(操作系统的一个基本功能就是管理进程)

线程:线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位

单线程程序是仅包含一个线程的进程。多线程程序的进程则包含两个或更多的线程

线程安全:在多线程程序中运行时具有正确的表现,就说代码是线程安全的

任务:任务是可能有高延迟的工作单元,目的是生成一个结果值,或者产生想要的效果

线程池:线程池是多个线程的集合,也是决定如何向线程分配工作的逻辑

多线程处理的目的和方式

 多线程处理主要用于两个方面:

1、实现多任务

2、解决延迟

其中主要还是解决延迟问题:例如导入一个大文件的时候需要较长的时间,为了允许用户随时点击取消,开发者创建一个额外的线程来执行导入,这样就可以随时点击取消,而不是直接冻结UI直至导入完成。

当然,如果有足够的内核使得每一个线程都能分配到一个内核的话,那么每个线程就都使用自己各自的CPU。但是如今虽然有了多核机器,但是线程数任然大于内核的数量。

为了解决这一粥(CPU内核)少僧(线程)多的矛盾,操作系统通过称为时间分片的机制来模拟多个线程并发运行。操作系统以极快的速度从一个线程切换到另一个线程,给人的感觉就是所有的线程都在同时执行

时间片:处理器在切换到下一个线程之前,执行一个特定的线程的时间周期称之为时间片或量子

上下文切换:在一个给定的内核中改换执行线程的动作称为上下文切换

不管是真正的多核并行运行还是使用时间分片的机制来模拟,我们说“一起”进行的两个操作是并发的。并行编程是指将一个问题分解成较小的部分,并异步的发起对每个部分的处理,使它们能并发地得到处理。

其中我们也需要考虑的是性能问题,不要产生一种误导就是多线程的代码会更快,多线程知识解决处理器受限的问题。同时我们需要注意性能问题

多线程处理遇到的问题

写一个多线程程序既复杂又困难,因为在单线程程序中许多成立的假设在多线程中变得不成立了,其中包括原子性、竞态条件、复杂的内存模型以及死锁

1、大多数操作不是原子性的

int Balance=10;
int Money=6;
if(Balance>Money)
 {
     Balance-=Money;
 }    

在这段代码中,如果出现两个线程都拿到Balance(当前余额)并且都进入了if中,第一个拿走了Money(取走的金额),然后第二个没有经过验证继续执行了Balance-=Money的操作,最后得出的结果是Balance剩下-2。这就导致了出现错误。

2、竞态条件造成的不确定性

什么是竞态条件

官方的定义是如果程序运行顺序的改变会影响最终结果,这就是一个竞态条件(race condition).

Runnable r1 = () -> { // do something };
Runnable1 r2 = () -> { // do another thing };
Thread producer = new Thread(new ThreadStart(Runnable));
Thread producer1 = new Thread(new ThreadStart(Runnable1));

producer.Start();
producer1.Start();

两个线程同时把一个类的静态成员做50词自增加1的操作,即

SomeClass.someMember++;

写在两个线程中,都运行50次,运行结束以后用主线程去取这个变量的值几乎不可能是100. 有的时候是97,有的时候是98,这是用来说明竞态条件的最有效例子。

3、内存模型的复杂性

假设两个线程在两个不同的进程中运行,但要访问同一个对象中的字段,目前的处理器不会每次都去访问主内存,相反访问的是处理的“高速缓存”中生成的一个本地副本,这个缓存会定时的与主内存同步,这就意味着这两个不同进程中的线程以为自己读取到的是相同的位置,实际读取到的不是那个字段实时更新的,造成两个线程获取的字段结果不一致。

4、锁定造成死锁

当然肯定有办法解决非原子性,防止竞态条件,并且确保处理器的高速缓存在必要时进行同步的。解决这些问题的主要机制是lock语句,这个语句就是将一部分代码设置为“关键”代码,一次只有一个线程能执行它,如果多个线程需要访问它,操作系统只允许进入一个,其他的将被挂起。

当然锁也有问题,加入不同的线程以不同的顺序获取锁,就可能造成死锁,这样的结果就是你等着我释放锁,我等着你释放锁。此时只有对方释放了锁之后才能继续运行,线程阻塞,造成了这段代码的彻底死锁

既然锁可以解决前三个问题,但是可能会出现死锁的问题。那么我们改如何避免或解决死锁的问题呢?

如何避免死锁

既然加入不同的线程以不同的顺序获取锁可能造成死锁,那么我们只有确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。

class Program
    {
        private static object objA = new object();
        private static object objB = new object();  
        static void Main()
        {
            Program a = new Program();
            Thread th = new Thread(new ThreadStart(a.Lock1));
            th.Start();

            lock (objB)
            {

                Console.WriteLine("我是objB,想获取objA");
                lock (objA)
                {
                    Console.WriteLine("死锁了"); 
                }
            }

            Console.WriteLine("死锁了");
            Console.WriteLine();
        }
        
        public void Lock1()
        {

            lock (objA)
            {
                Thread.Sleep(500);
                Console.WriteLine("我是objA,想获取objB");
                lock (objB)
                {
                    Console.WriteLine("死锁了"); 
                }
            }
        }
    } 

上面是获取锁的顺序不恰当而导致死锁的例子。如果把其中一个获取锁的位置改变一下就不会造成死锁了,例如在Lock1中先获取objB再获取objA的话就不会造成死锁了。所有我们平时在使用lock时一定得确保锁的顺序,不然很容易造成死锁的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券