并发编程基本概念

引言:挂起与阻塞

纠结了好几年的sleep()到底是阻塞还是挂起的问题,又引申出为什么编程时没见过挂起相关的方法,因此查阅大量资料写下这篇文章。
学习过程中发现,之所以自己有这样的疑惑,是因为在我的学习过程中,java多线程编程 和 操作系统原理 这两门学问的学习时完全分割的,导致有种割裂感,不知道怎么把操作系统的线程模型和高级编程语言的线程API统一起来。

原子性:通过中间不会插入指令的原子指令,实现任务中不被打断。
通过关中断实现。
数据库事务原理?数据库并发?

辞典

  • pthread: IEEE1003.1c提出的通用线程标准,其对应的C语言库文件为pthread.h,该库是对unix、GNU/linux内核系统调用的封装。早期c++程序员直接使用<pthread.h>库进行多线程编程,

    • 而c++11之后也使用类似java等高级语言中的封装线程库了。一来更方便,二来<pthread.h>不便跨平台(win有另一套线程库,想使用phtread需要安装专门的库)
    • 前面这个"p"就是POSIX的意思;
    • 虽然源自POSIX,但实现大部分POSIX标准的操作系统(unix和linux家族)们的内核甚至发行版并不自带pthread.h文件,该文件是在安装GNU编译器套件时安装的;对大多数使用者简单来说,就是装gcc的时候顺便装的。
    • 为什么又是ieee的标准又是POSIX的?这和GNU标准又是什么关系?
  • 挂起的案例:浏览器,游戏卡死?

挂起和阻塞的区别:挂起时进程放在外存了,而阻塞时是放到内存的阻塞队列里。

为什么已经有了阻塞,还要引入挂起?主要是为了利用交换技术来省内存;

  • 要是用了虚拟内存还需要交换技术嘛?

线程状态转换

我们常见的五状态模型(及对应的java方法):

而引入挂起状态后,我们可以有七甚至六状态模型!

在这里插入图片描述

很多朋友可能感到疑惑:为什么很少讨论“挂起”状态呢?在java等高级语言学习中,我们讨论的是sleep():阻塞的一种;在很多操作系统书籍中,我们学习的是五状态模型;在NIO网络编程中,我们讨论的线程何时阻塞,却不提挂起;

终于在《操作系统-精髓与设计原理》一书中3.2.4我找到了对“挂起”状态的讨论。

原来,“挂起”这一将进程保存在外存以高效利用cpu的操作,要基于“交换”,讨论时要在没有虚拟内存功能的操作系统中使用“交换”技术来实现。

  • 虚拟内存真的使交换技术没意义了么?
  • 现代操作系统还有交换技术吗?有没有对应的系统调用?

原书作者引入“挂起”操作,主要是为了告诉大家“五状态模型”不是教条的,因此以“挂起”作为例子来说明引入新状态的好处。原书也说明了,是在没有虚拟内存功能的操作系统中讨论挂起的。

但是,我们可以证明向模型中增加其他状态也是合理的。为了说明加入新状态的好处,考虑个未使用虚存的系统,每个被执行的进程必须完全载入内存,因此在图 3.8(b)中,所有队列中的所有进程必须驻留在内存中。

使用虚存的情况下,可能会执行只有部分内容在内存中的进程,若访问的进程地址不在内存中,则将进程的相应部分调入内存。使用虚存看上去不需要显式交换,因为通过处理器中的存储管理硬件,任何进程中的任何地址都可移入或移出内存。然而,如第8章所述,若活动进程很多,且所有的进程都有一部分在内存中时,则可能会导致虚存系统崩溃。因此,即使是在虚存系统中,操作系统也需要不时地根据执行情况完全显式地换出进程。

-----------《操作系统-精髓与设计原理》

关于“虚拟内存”,需要注意英文中virtual既有“虚拟”之意,又有与之相反的“实际”之意。在程序员的学习过程中,私以为"virtual *** "可以理解为“逻辑上的某某某”;

引入sleep:更细致的阻塞状态

sleep()和Wait()都是阻塞!!他们可以理解为阻塞下更细致的功能性分支。

java文档:

sleep(long millis)

Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers.

sleep(n)调用后当前线程让出cpu,之后的n毫秒内不再参与cpu竞争

join(long millis)

Waits at most millis milliseconds for this thread to die.

join()方法:能够使当前执行的线程停下来等待,直至join()方法所调用的那个线程结束,再恢复执行。

在一些c++使用pthread多线程编程的教程中,join()法如其名地被称为“连接”线程,比较好理解:让一个线程再另一线程结束后再运行,好像是两个线程连接在一起运行一样。

而与之对应的,pthread线程标准中还有pthread_detach (threadid) 用于“分离”线程。

wait(long timeout)

Causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed.

  • 怎么等待?空转?

wait()与notify()方法:wait()不用指定时间,让线程一直等待下去,知道其他线程用notify()指定该线程唤醒它;

@Deprecated
public final void suspend()

已弃用 这种方法已被弃用,因为它本身就是死锁的。 如果目标线程在挂起时锁定了在关键资源上的monitor,则在目标线程恢复之前,线程不能访问该资源。 如果要恢复目标线程的线程在调用resume之前也需要尝试锁定此监视器, resume将导致死锁。

Why are Thread.suspend and Thread.resume deprecated?

Thread.suspend is inherently deadlock-prone. If the target thread holds a lock on the monitor protecting a critical system resource when it is suspended, no thread can access this resource until the target thread is resumed. If the thread that would resume the target thread attempts to lock this monitor prior to calling resume, deadlock results. Such deadlocks typically manifest themselves as “frozen” processes.

这段是说,挂起(suspend)容易引发死锁问题,如果一个被挂起的线程锁住了某个重要资源的monitor,。

  • 那为什么wait()就不会死锁?
    • 是不是因为,“挂起”这一状态,要求保留线程状态,因此锁也被保留了;而wait()底层JNI源码添加了释放锁的过程?
  • 这,,还需要看源码嘛?看文档也只是功能性的描述 ,没说是怎么系统调用的,,
  • 话说,为什么宁愿空转也不愿阻塞?宁愿阻塞也不挂起?
  • 底层线程库分为两大类:POSIX的Pthread,windows API
  • Pthread不能跨平台,c++11推出了thread
  • 五种模型和七种模型谁提出的?ieee?
  • 新一代的线程方法呢?某一语言提出然后引入的?

线程池中的核心线程数设置问题

之前看过哪吒的这篇大批量数据库导入文章。不过友友们似乎标识可以直接easyExcel或者线下写sql处理。或者fork、join。这么大规模导入的需求有些伪了。
使用双异步后,从 191s 优化到 2s
线程池中的核心线程数CorePoolSize、最大线程数MaxPoolSize,设置成多少,最合适,效率最高。

CPU的处理器数量

// 获取CPU的处理器数量
int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;

Runtime.getRuntime().availableProcessors()获取的是CPU核心线程数,也就是计算资源。

  • CPU密集型,线程池大小设置为N,也就是和cpu的线程数相同,可以尽可能地避免线程间上下文切换,但在实际开发中,一般会设置为N+1,为了防止意外情况出现线程阻塞,如果出现阻塞,多出来的线程会继续执行任务,保证CPU的利用效率。
  • IO密集型,线程池大小设置为2N,这个数是根据业务压测出来的,如果不涉及业务就使用推荐。
  • 看不懂,既然是业务压测的,那为什恶魔不涉及业务就使用推荐呢?

在实际中,需要对具体的线程池大小进行调整,可以通过压测及机器设备现状,进行调整大小。

如果线程池太大,则会造成CPU不断的切换,对整个系统性能也不会有太大的提升,反而会导致系统缓慢。

  • 不断切换?为什么?难道不是时间片算法吗?还

知道了线程数来反推业务量:
我的电脑的CPU的处理器数量是24。

那么一次读取多少行最合适呢?

测试的Excel中含有10万条数据,10万/24 = 4166,那么我设置成4200,是不是效率最佳呢?

测试的过程中发现,好像真的是这样的。

  • 每次读取和入库的数量是关键,不能太多,因为每次入库会变慢;
  • 也不能太少,如果太少,超过了150个线程,就会造成线程阻塞,也会变慢;

Go 的并发模型

Go 的并发模型基于goroutine 和channel,这是其最大的亮点之一。Goroutine 是一种轻量级的线程,可以在单个进程中同时运行多个任务。每个 goroutine 只占用少量的内存(约 2KB),并且可以根据需要动态调整其栈大小。Go 的调度器会自动管理 goroutine 的创建和销毁,使得开发者无需担心底层的线程管理细节。Channel 是 Go 中用于 goroutine 之间通信的机制。它提供了一种安全的方式在不同 goroutine 之间传递数据,避免了传统的锁机制带来的复杂性和潜在的死锁问题。Go 的标准库提供了丰富的 channel 操作,如发送、接收、选择等,使得并发编程变得更加直观和易用。Go 的并发模型非常适合构建高并发的网络服务和 Web 应用。例如,在处理 HTTP 请求时,每个请求都可以作为一个独立的 goroutine 来处理,充分利用多核 CPU 的计算能力。此外,Go 还提供了sync.WaitGroup 等工具,可以帮助开发者等待多个 goroutine 完成后再继续执行后续操作。

Rust 的并发模型

Rust 的并发模型同样强大,但与 Go 不同的是,Rust 更加注重内存安全和细粒度的控制。Rust 引入了所有权、借用 和生命周期 的概念,确保在多线程环境中不会发生数据竞争。Rust 的编译器会在编译阶段进行严格的静态检查,确保每个线程对共享资源的访问都是安全的。Rust 的并发编程主要依赖于std::thread 模块,该模块提供了创建和管理线程的功能。Rust 还支持async/await 语法,使得异步编程变得更加简洁和直观。此外,Rust 提供了多种并发原语,如Mutex、RwLock 和Arc,帮助开发者实现线程安全的数据共享。Rust 的Rayon 库是一个非常强大的并行计算库,它可以通过简单的 API 实现容器迭代的并行化。Rayon 的设计目标是让开发者能够轻松地利用多核 CPU 的计算能力,而无需手动管理线程。通过 Rayon,开发者可以编写高效的并行代码,而无需担心底层的线程调度和同步问题。

use std::thread;
use std::time::Duration;
fn main(){
    // 1. create a new thread
        for i in 1..10 {
           thread::spawn(move|| {
           println!("thread: number {}!",i);
           thread::sleep(Duration::from_millis(100));
           });    
        }    
        println!("hi from the main thread!");
}

并发模型的对比

虽然 Go 和 Rust 都提供了强大的并发支持,但在开发者体验方面有所不同。Go 的并发模型更加直观和易用,适合那些希望快速开发并行应用程序的开发者。Go 的 goroutine 和 channel 机制使得并发编程变得简单明了,减少了学习曲线。然而,Go 的并发模型相对较为宽松,可能会导致一些潜在的并发问题,如竞态条件(race condition)和死锁(deadlock)。为此,Go 提供了一个内置的竞态检测器(race detector),可以帮助开发者识别并修复这些问题。相比之下,Rust 的并发模型更加严格和安全。Rust 的所有权和借用系统确保了在多线程环境中不会发生数据竞争,从而避免了常见的并发问题。Rust 的编译器会在编译阶段进行严格的静态检查,确保每个线程对共享资源的访问都是安全的。这种设计虽然增加了开发的复杂性,但也带来了更高的代码质量和可靠性。

参考

The Single UNIX Specification, Version 2 (opengroup.org)

Linux下调用pthread库创建的线程是属于用户级线程还是内核级线程?求大神指教? - 知乎 (zhihu.com)

怎样理解线程的睡眠,挂起,和阻塞? - 王小C的回答 - 知乎

Q: pthread到底是POSIX操作系统提供的,还是GNU套件提供的?

A: 虽然大量linux系操作系统都实现了不少的POSIX标准,但大多数情况下,pthread是在安装GNU编译器套件(如用包管理器安装gcc)时,作为C语言标准库安装的。linux下会把这些标准库到/usr/include目录下

如果了解C语言标准库就知道了,C语言标准库都是安装GNU的时候装的。

  • 看来这并不是直接使用系统调用了,或许可以称他们为系统调用的“入口”,,那么系统调用函数定义在了那里?

在这里插入图片描述

吐槽下:家人们谁懂啊,人工智障是真下头,之前问的两个问题也是一整个无语住了,一问就说操作系统提供的,再问就是操作系统默认没有

在这里插入图片描述

  • linux不安装gcc的话,有pthread.h么

  • 人工智障答:在大多数Linux发行版中,安装了gcc(GNU编译器套件)通常会默认包含pthread.h头文件。pthread.h是POSIX线程标准的头文件,用于在C或C++程序中进行多线程编程。它定义了与线程相关的函数、宏和数据类型。

    但是,如果你在Linux系统上没有安装gcc或者只安装了C编译器而没有安装C++编译器,那么你可能无法直接使用pthread.h。这是因为pthread.h是C/C++编译器的一部分,它是用来编译和链接多线程程序的。

    如果你想在Linux上进行多线程编程而没有安装gcc,你可以尝试使用其他编译器,如Clang。Clang也支持POSIX线程库,并提供了对pthread.h的支持。

    总结起来,大多数情况下,在Linux上安装了gcc后会默认包含pthread.h头文件。如果没有安装gcc或者只安装了C编译器而没有安装C++编译器,你可能需要安装相应的工具链或使用其他编译器来使用pthread.h头文件。

  • windows下pthtread.h在哪里?

    • 答在Windows操作系统下,并不直接提供pthread.h头文件和对应的线程库。Windows使用不同的线程模型,它提供了自己的线程相关函数和头文件。如果你想在Windows下进行多线程编程,可以使用Windows API中的线程函数,例如CreateThread()、WaitForSingleObject()等。相应地,需要包含windows.h头文件来使用这些函数。如果你希望在Windows下使用类似于POSIX线程的API和头文件,可以考虑使用第三方库,例如pthread-win32或Winpthreads。这些库提供了对pthread.h的实现,并允许在Windows上进行跨平台的多线程编程。你可以从它们的官方网站上下载并使用这些库。总结起来,在Windows操作系统下,如果你想使用类似于pthread.h的头文件和API,你可以选择使用Windows API中的线程函数,或者使用第三方库来实现跨平台的多线程编程。

我在DevC中include<pthread.h>,再ctrl点进去->查看文件夹,原来安装DevC开发环境的时候,它给我自动安装了x86_64-w64-mingw32文件夹,这个库里包含pthread.h。

因此才有了我前面的“纠正”

为此我专门新装了台centos最小化虚拟机上亲手实验了下,默认没有的,,安装gcc才有
在这里插入图片描述

安装后:
在这里插入图片描述

  • POSIX标准哪里看?为什么pthread要单独拿出来而不是作为系统调用?系统调用函数都在哪儿?
  • [ ]

业务并发笔记

同步是指控制顺序。之前我总是误以为“同步”是说两个线程同时做同样的工作(类比形容两个人动作“同步”了),但这种说法其实类似“并发”。同步其实是指控制顺序的同步,比如两个人不知道搭档的工作进度,便“同步一下工作进度”,这是指控制顺序到“一条线”上,避免各干各的。导致冲突等。
加锁同步,除了线程同步,还有人的操作同步吧,
除了为了同步顺序,也有打退另一方的情况。比如拍卖

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值