多线程编程的难点以及问题

多线程编程的难点

  • 数据安全:同一个对象被多个线程同时操作,这样有可能导致数据的读写错误,所以有些任务是需要保序的。在C++中,为了保证同一阶段任务按序执行,可以使用boost::asio中的strand,即ot、rdt、rst等。
  • 优先级:多线程访问同一个对象时,需要考虑线程的不同优先级。

在计算机多线程编程中,线程之间的安全问题是很重要的,它不仅关系到所需要的功能能否正确地实现,还关系到算法运行结果的稳定性等问题。当在多线程编程时,或者使用到的软件框架是具有多线程运行功能的时候,一名训练有素且技术过硬的合格程序员是会考虑程序在多线程环境下运行时的线程安全问题的,尤其是在多个线程间存在共享的资源的情况下。本文主要介绍两种实现多线程之间线程安全的方案,同步和加锁。

什么是多线程

  • 在单线程计算系统中,一次执行一条指令,并且一次产生一个结果。加载和完成程序的时间取决于 CPU 需要完成的工作量。
  • 多线程是一种编程方式,利用了 CPU 在多个核心上同时处理多个线程的能力。这种情况下不是逐个执行任务或指令,而是同时运行任务或指令。
  • 默认情况下,在程序的开头会运行一个线程。这就是“主线程”。主线程会创建新线程来处理任务。这些新线程并行运行,通常在完成后将其结果与主线程同步。
  • 如果有一些长时间运行的任务,这种多线程方法很有效。但是,游戏开发代码通常包含许多一次执行的小指令。如果为每个指令创建一个线程,最终可能会有许多线程,每个线程的生命周期都很短。这种情况下可能会挑战CPU 和操作系统处理能力的极限。
  • 设置一个线程池可以缓解线程生命周期的问题。但是,即使使用线程池,也可能会同时激活大量线程。线程数多于 CPU 核心数会导致线程相互竞争CPU资源,进而造成频繁的上下文切换。上下文切换是这样一个过程:在执行过程的中途保存一个线程的状态,然后处理另一个线程,再然后重建第一个线程以继续处理该线程。上下文切换是资源密集型的过程,因此应尽可能避免。

同步

同步的方案是将一段代码声明为原子操作,只有执行完毕才可以由其他线程执行,执行过程中不可中断,即使出现不可屏蔽的中断,也必须进行回退。这一种方案Java编程中用的较多,主要原因是Java语法原生支持“synchronized”关键字,可以使得某一段代码块的原子性和可见性。这里的锁对象可以是任意对象。在该代码块中,对指定的对象进行的操作具有原子性,其他线程在执行调用该对象的这段代码时,会被阻塞,保证只有一个线程能够处于执行这段代码的状态中。

加锁

加锁则是另一种技术了,但是二者有关联,可以认为上述的同步是锁的一种,即互斥锁。加锁一般分为互斥锁,可重入锁和读写锁。

可重入锁可以用来解决当代码块是递归调用时,普通的互斥锁会产生死锁的问题。因为某个线程已经获得锁,当它递归调用自己的时候,会再次申请加锁,此时就会死锁,而可重入锁可以在线程已经获取到锁的情况下,递归调用自己时,直接再次使用自己已经获得的锁。

读写锁的特点是,多个读者线程可以同时读,但是写者线程是互斥的,也就是同一时间最多只能有一个写者,而且也与读者线程优先,也就是当有写者时,优先调度写者线程运行,并将后续的读者线程调度为阻塞态。

同步和加锁的对比

从性能上来说,在资源的竞争不激烈的情况下,两中方式的性能接近,而在有大量线程同时激烈地竞争资源时,此时Lock的性能要远远优于synchronized。同步的互斥锁可重入但不可中断,排斥所有其他线程,即使二者都是读者也不行,此时读写锁更好。但同步的互斥锁在synchronized块结束时,会自动释放锁,在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;lock一般需要在finally中自己释放,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要手动在finally块中释放锁。我们在具体使用时,要根据实际情况做最合适的选择。

优先级反转(造成死锁)

假设有两个线程A和B,B的优先级低于A,B占有资源M运行着,接着优先级高的线程A开始运行,由于优先级高,所以此时线程B被挂起,但是资源M没有释放,所以A就会一直等待,从而造成线程A和线程B的死锁。

原文地址:通过同步和加锁解决多线程的线程安全问题

参考文章:
线程同步(重难点)
多线程的难点在哪里

猜你喜欢

转载自blog.csdn.net/qq_41841073/article/details/127838830