43、Ruby 线程与并发编程全解析

Ruby 线程与并发编程全解析

1. 线程与未处理异常

在 Ruby 中,线程的异常处理机制与主线程有所不同。当主线程抛出未处理的异常时,Ruby 解释器会打印消息并退出。而对于非主线程,未处理的异常只会使该线程停止运行,默认情况下不会导致解释器打印消息或退出。

若线程 t 因未处理的异常退出,而另一个线程 s 调用 t.join t.value ,则 t 中发生的异常会在 s 线程中抛出。

如果希望任何线程中的未处理异常都能导致解释器退出,可以使用类方法 Thread.abort_on_exception = true 。若仅希望某个特定线程的未处理异常导致解释器退出,则使用同名的实例方法:

t = Thread.new { ... }
t.abort_on_exception = true

2. 线程与变量

2.1 线程共享变量

线程的一个关键特性是它们可以共享对变量的访问。由于线程由代码块定义,它们可以访问代码块作用域内的任何变量,包括局部变量、实例变量和全局变量等。示例如下:

x = 0
t1 = Thread.new do
  # 此线程可以查询和设置变量 x
end
t2 = Thread.new do
  # 此线程也可以查询和设置 x
  # 并且它还可以查询和设置 t1 和 t2
end

当两个或多个线程同时读写相同的变量时,必须确保操作的正确性,这将在后续的线程同步部分详细讨论。

2.2 线程私有变量

在线程代码块内定义的变量是该线程私有的,其他线程不可见,这是 Ruby 变量作用域规则的结果。

例如,以下代码尝试创建三个线程,分别打印数字 1、2 和 3:

n = 1
while n <= 3
  Thread.new { puts n }
  n += 1
end 

在某些情况下,这段代码可能按预期工作并打印 1、2 和 3,但在其他情况下可能不会。因为每个线程读取的是变量 n 的共享副本,而该变量的值会随着循环的执行而改变,线程打印的值取决于该线程相对于父线程的运行时间。

为了解决这个问题,可以将 n 的当前值传递给 Thread.new 方法,并将该变量的当前值赋给一个块参数:

n = 1
while n <= 3
  # 获取 n 当前值的私有副本到 x
  Thread.new(n) {|x| puts x }
  n += 1
end 

另一种解决方法是使用迭代器而不是 while 循环:

1.upto(3) {|n| Thread.new { puts n }}

2.3 线程局部变量

Ruby 的某些特殊全局变量是线程局部的,不同线程中它们可能有不同的值,例如 $SAFE $~ 。这意味着如果两个线程同时进行正则表达式匹配,它们将看到不同的 $~ 值,一个线程的匹配操作不会干扰另一个线程的匹配结果。

Thread 类提供了类似哈希的行为,定义了 [] []= 实例方法,允许将任意值与任何符号关联。以下是一个示例,假设创建了一些线程从 Web 服务器下载文件,主线程需要监控下载进度:

# 每个线程设置进度
Thread.current[:progress] = bytes_received
# 主线程计算总下载字节数
total = 0
download_threads.each {|t| total += t[:progress] if t.key?(:progress)}

3. 线程调度

3.1 线程优先级

影响线程调度的第一个因素是线程优先级,高优先级线程会在低优先级线程之前被调度。更准确地说,只有在没有更高优先级线程等待运行时,线程才能获得 CPU 时间。

可以使用 priority= priority 方法来设置和查询 Ruby Thread 对象的优先级,但在线程启动之前无法设置其优先级。新创建的线程的优先级与创建它的线程相同,主线程的初始优先级为 0。

线程优先级依赖于 Ruby 的实现和底层操作系统,例如在 Linux 上,非特权线程无法提高或降低其优先级,因此在使用原生线程的 Ruby 1.9 中,线程优先级设置会被忽略。

3.2 线程抢占和 Thread.pass

当多个相同优先级的线程需要共享 CPU 时,由线程调度器决定每个线程何时运行以及运行多长时间。有些调度器是抢占式的,即允许线程运行一段固定时间后,再让另一个相同优先级的线程运行;而其他调度器是非抢占式的,一旦线程开始运行,它将继续运行,除非它进入睡眠状态、进行 I/O 阻塞或有更高优先级的线程唤醒。

如果一个长时间运行的计算密集型线程在非抢占式调度器上运行,它可能会“饿死”其他相同优先级的线程。为避免这种情况,长时间运行的计算密集型线程应定期调用 Thread.pass 方法,请求调度器将 CPU 让给其他线程。

4. 线程状态

4.1 线程的五种状态

Ruby 线程可能处于以下五种状态之一:
| 线程状态 | 返回值 |
| ---- | ---- |
| 可运行 | “run” |
| 睡眠 | “sleep” |
| 中止 | “aborting” |
| 正常终止 | false |
| 异常终止 | nil |

4.2 查询线程状态

Thread 类定义了几个实例方法来测试线程的状态:
- alive? :如果线程可运行或睡眠,则返回 true
- stop? :如果线程处于除可运行之外的任何状态,则返回 true
- status :返回线程的状态。

4.3 改变线程状态

线程创建时处于可运行状态,可通过调用 Thread.stop 方法暂停自身,进入睡眠状态。调用 Kernel.sleep 方法并传入参数也会使线程暂时进入睡眠状态,经过指定的秒数后自动唤醒并重新进入可运行状态。

可以使用 wakeup run 方法将暂停的线程重新启动, run 方法还会调用线程调度器,可能使新唤醒的线程立即开始运行,而 wakeup 方法唤醒指定线程但不放弃 CPU。

线程可以通过退出其代码块或抛出异常从可运行状态转换到终止状态,也可以调用 Thread.exit 方法正常终止。此外,一个线程可以通过调用另一个线程的 kill terminate exit 方法强制终止它, kill! terminate! exit! 方法会终止线程但不允许执行 ensure 子句。还可以使用 raise 方法在另一个线程中抛出异常。

以下是线程状态转换的 mermaid 流程图:

graph LR
    classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;

    A([创建线程]):::startend --> B(可运行状态):::process
    B --> C{操作}:::decision
    C -->|Thread.stop| D(睡眠状态):::process
    C -->|Kernel.sleep| D
    C -->|退出代码块/抛出异常| E(终止状态):::process
    C -->|Thread.exit| E
    D -->|wakeup/run| B
    B -->|kill/terminate/exit| E
    B -->|raise 异常| E

5. 线程列表和线程组

5.1 列出所有活动线程

Thread.list 方法返回一个表示所有活动(运行或睡眠)线程的 Thread 对象数组,当线程退出时,它将从该数组中移除。

5.2 线程组

可以创建 ThreadGroup 对象并将线程添加到其中,对线程进行分组管理。新线程最初会被放置在其父线程所属的组中,可以使用 group 方法查询线程所属的 ThreadGroup ,使用 list 方法获取组中的线程数组。

ThreadGroup 类的 enclose 方法使其比简单的线程数组更有用,一旦线程组被封闭,就不能从中移除线程,也不能添加新线程,但组内的线程可以创建新线程,新线程将成为该组的成员。以下是一个示例:

group = ThreadGroup.new
3.times {|n| group.add(Thread.new { do_task(n) }}

6. 线程应用示例

6.1 并发读取文件

在处理 IO 密集型任务时,Ruby 的线程可以让程序在等待用户输入、文件系统或网络数据时保持忙碌。下面的 conread 方法可以并发读取多个文件,并返回一个将文件名映射到文件内容的哈希表:

# Read files concurrently. Use with the "open-uri" module to fetch URLs.
# Pass an array of filenames. Returns a hash mapping filenames to content.
def conread(filenames)
  h = {}                            # Empty hash of results
  # Create one thread for each file
  filenames.each do |filename|      # For each named file
    h[filename] = Thread.new do     # Create a thread, map to filename
      open(filename) {|f| f.read }  # Open and read the file
    end                             # Thread value is file contents
  end
  # Iterate through the hash, waiting for each thread to complete.
  # Replace the thread in the hash with its value (the file contents)
  h.each_pair do |filename, thread| 
    begin
      h[filename] = thread.value    # Map filename to file contents
    rescue
      h[filename] = $!              # Or to the exception raised
    end
  end
end

操作步骤如下:
1. 初始化一个空的哈希表 h 用于存储结果。
2. 遍历文件名数组,为每个文件创建一个线程,线程的任务是打开并读取文件,线程的值即为文件内容。
3. 再次遍历哈希表,等待每个线程完成,并将线程对象替换为其值(文件内容),若线程抛出异常,则将异常信息存储在哈希表中。

6.2 多线程服务器

线程的另一个常见应用是编写可以同时与多个客户端通信的服务器。以下是一个简单的多线程服务器示例:

require 'socket'
# This method expects a socket connected to a client.
# It reads lines from the client, reverses them and sends them back.
# Multiple threads may run this method at the same time.
def handle_client(c)
  while true
    input = c.gets.chop     # Read a line of input from the client
    break if !input         # Exit if no more input
    break if input=="quit"  # or if the client asks to.
    c.puts(input.reverse)   # Otherwise, respond to client.
    c.flush                 # Force our output out
  end
  c.close                   # Close the client socket
end
server = TCPServer.open(2000) # Listen on port 2000
while true                    # Servers loop forever
  client = server.accept      # Wait for a client to connect
  Thread.start(client) do |c| # Start a new thread 
    handle_client(c)          # And handle the client on that thread
  end
end

操作步骤如下:
1. 定义 handle_client 方法,用于处理与客户端的通信,读取客户端输入,反转字符串并发送回客户端,直到客户端输入 quit 或关闭连接。
2. 创建一个 TCP 服务器,监听端口 2000。
3. 进入无限循环,等待客户端连接,为每个客户端连接创建一个新线程,并在该线程中调用 handle_client 方法处理客户端请求。

6.3 并发迭代器

除了 IO 密集型任务,线程还可以用于并发处理数组元素。以下是为 Enumerable 模块添加的 conmap concurrently 方法:

module Enumerable           # Open the Enumerable module
  def conmap(&block)        # Define a new method that expects a block
    threads = []            # Start with an empty array of threads
    self.each do |item|     # For each enumerable item
      # Invoke the block in a new thread, and remember the thread
      threads << Thread.new { block.call(item) }
    end
    # Now map the array of threads to their values 
    threads.map {|t| t.value } # And return the array of values
  end
end

module Enumerable
  def concurrently
    map {|item| Thread.new { yield item }}.each {|t| t.join }
  end
end

操作步骤如下:
- conmap 方法
1. 初始化一个空的线程数组 threads
2. 遍历可枚举对象的每个元素,为每个元素创建一个新线程,并在该线程中调用传入的块。
3. 等待所有线程完成,并将线程对象映射为其值,返回结果数组。
- concurrently 方法
1. 为可枚举对象的每个元素创建一个新线程,并在该线程中调用传入的块。
2. 等待所有线程完成。

7. 线程互斥与死锁

当两个或多个线程共享对同一数据的访问,并且至少有一个线程修改该数据时,必须采取特殊措施确保没有线程会看到数据处于不一致的状态,这就是线程互斥。

7.1 线程互斥的必要性

以下是两个需要线程互斥的经典例子:
- 文件处理计数器 :两个线程处理文件并递增一个共享变量来跟踪处理的文件总数。由于递增操作不是原子操作,可能会导致计数器值错误。
- 电子银行应用 :一个线程处理从储蓄账户到支票账户的资金转移,另一个线程生成月度报告。如果没有适当的互斥机制,报告生成线程可能会读取到不一致的账户数据。

7.2 使用互斥锁(Mutex)

可以使用 Mutex 对象来实现线程互斥,以下是一个银行账户示例:

require 'thread'  # For Mutex class in Ruby 1.8
# A BankAccount has a name, a checking amount, and a savings amount.
class BankAccount
  def init(name, checking, savings)
    @name,@checking,@savings = name,checking,savings 
    @lock = Mutex.new         # For thread safety
  end
  # Lock account and transfer money from savings to checking
  def transfer_from_savings(x)
    @lock.synchronize {
      @savings -= x
      @checking += x
    }
  end
  # Lock account and report current balances
  def report
    @lock.synchronize {
      "#@name\nChecking: #@checking\nSavings: #@savings"
    }
  end
end

操作步骤如下:
1. 在 BankAccount 类中初始化一个 Mutex 对象 @lock
2. 在 transfer_from_savings 方法中,使用 @lock.synchronize 块来确保在资金转移过程中其他线程无法访问账户数据。
3. 在 report 方法中,同样使用 @lock.synchronize 块来确保在报告账户余额时数据的一致性。

通过以上内容,我们全面了解了 Ruby 线程的异常处理、变量共享、调度、状态管理、应用示例以及线程互斥等方面的知识,掌握这些知识可以帮助我们编写更高效、更可靠的并发程序。

这个是完整源码 python实现 Django 【python毕业设计】基于Python的天气预报(天气预测分析)(Django+sklearn机器学习+selenium爬虫)可视化系统.zip 源码+论文+sql脚本 完整版 数据库是mysql 本研究旨在开发一个基于Python的天气预报可视化系统,该系统结合了Django框架、sklearn机器学习库和Selenium爬虫技术,实现对天气数据的收集、分析和可视化。首先,我们使用Selenium爬虫技术从多个天气数据网站实时抓取气象数据,包括温度、湿度、气压、风速等多项指标。这些数据经过清洗和预处理后本研究旨在开发一个基于Python的天气预报可视化系统,该系统结合了Django框架、sklearn机器学习库和Selenium爬虫技术,实现对天气数据的收集、分析和可视化。首先,我们使用Selenium爬虫技术从多个天气数据网站实时抓取气象数据,包括温度、湿度、气压、风速等多项指标。这些数据经过清洗和预处理后,将其存储在后端数据库中,以供后续分析。 其次,采用s,将其存储在后端数据库中,以供后续分析。 其次,采用sklearn机器学习库构建预测模型,通过时间序列分析和回归方法,对未来天气情况进行预测。我们利用以往的数据训练模型,以提高预测的准确性。通过交叉验证和超参数优化等技术手段,我们优化了模型性能,确保其在实际应用中的有效性和可靠性。 最后,基于Django框架开发前端展示系统,实现天气预报的可视化。用户可以通过友好的界面查询实时天气信息和未来几天内的天气预测。系统还提供多种图表类型,包括折线图和柱状图,帮助用户直观理解天气变化趋势。 本研究的成果为天气预报领域提供了一种新的技术解决方案,不仅增强了数据获取和处理的效率,还提升了用户体验。未来,该系统能够扩展至其他气象相关的应用场景,为大众提供更加准确和及时的气象服务。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值