线程、内存、锁定和阻塞(Threads, Memory, Locking, and Blocking)

如果你真的想进行并行编程的话,花点时间理解线程和内存的概念是完全值得的。在这一节,我们将学习如何显式地创建线程,并控制对共享资源,比如内存的访问。我的忠告是,应该避免你这样显式创建和管理线程,然而,在使用其他的并行编程方法时,理解底层的线程概念是需要的。

程序运行时,操作系统会创建一个进程(process)来运行,这个进程代表分配给这个程序的资源,最常见的是分配给它的内存。进程可以有一个或多个线程,负责运行程序的指令,共享进行的内存。在 .NEt 中,程序是以运行这个程序代码的线程开始的,在 F# 中,创建一个额外的线程,使用System.Threading.Thread 类。线程类的构造函数有一个代理参数,表示这个线程将要开始运行的函数;线程类构造以后,并不自动运行,必须调用它的 Start 方法。下面的示例演示了如何创建并启动一个新线程:

open System.Threading

let main() =

//create a new thread passing it a lambda function

letthread = new Thread(fun () ->

//print a message on the newly created thread

printfn"Created thread: %i" Thread.CurrentThread.ManagedThreadId)

//start the new thread

thread.Start()

//print an message on the original thread

printfn"Orginal thread: %i" Thread.CurrentThread.ManagedThreadId

//wait of the created thread to exit

thread.Join()

do main()

前面程序的运行结果你这样:

Orginal thread: 1

Created thread: 3

在这个示例中,你应该看两个重要的内容:第一,原始线程打印的消息在第二个线程打印之前,这是因为调用线程的 Start 方法并不立即启动这个线程,相反,把新线程的运行和操作系统选择何时运行它列入任务计划。通常,这个延迟很短,但是,原始线程会继续运行,因此,原始线程在新线程开始运行之前就运行了几条指令是完全可能的;第二,注意一下如何使用线程的 Jion 函数等待线程的退出。如果不这样做,最大的可能是原始线程可能已经运行结束,第二个线程才有机会启动。原始线程等待创建线程来完成工作,被称为阻塞。线程被阻塞有多种原因,例如,它可等待锁(lock),可能是等待输入输出的完成。当线程被锁定,操作系统会切换到下一个可运行的线程,这称为上下文切换(context
switch)。我们将在下一节学习有关锁定的内容,在这一节,我们看一下异步编程中阻塞输入输出的操作。

任何资源被两个不同的线程同时修改,是有被破坏的风险的,这是因为线程可能在任何时候进行上下文切换,而遗留的操作可能只完成了原子操作(atomic,不应该再分)的一半,要想避免这种破坏,就要用到锁。锁,有时也称为监视器(monitor),这是一段代码,一次只能有一个线程通过。在 F# 中,我们使用 lock(锁)函数创建和控制锁,是通过锁定对象来实现的,其思想是这样的:一旦拿到锁,企图进入这段代码的任何线程都会被阻塞,直到由拿到这个锁的线程把这个锁释放为止。采用这种方式保护的代码有时也称为临界区(critical
section),通过在打算保护的代码开始时调用System.Threading.Monitor.Enter,在这段代码的结束时调用 System.Threading.Monitor.Exit实现;必须保证 Monitor.Exit 被调用,否则,可能导致线程一直被锁定;锁函数是确保如果调用Monitor.Enter 之后,Monitor.Exit 总是被调用的一个好办法。这个函数有两个参数:第一个是打算锁定的对象,第二个是包含了想要保护的代码;这个函数可能把空(unit)作为参数,返回可以是任意值。

下面的示例演示了涉及锁定的微妙问题[ 需要仔细体会],完成锁定的代码需要相当长的时间,因此,示例故意写成夸大上下文切换的问题。代码背后的思想是这样的:如果两个线程同时运行,且都尝试写到控制台;其目标是把字符串"One ... Two ... Three ... " 以原子方式写到控制台,就是说,一个线程能名在下一外委会线程启动之前,应该能够完成写消息。示例有一个函数makeUnsafeThread,它创建的线程不能原子地把字符串写到控制台;第二个函数[ 不应该是线程] makeSafeThread,通过使用锁,可以原子地写到控制台:

open System

open System.Threading

// function to print to the consolecharacter by character

// this increases the chance of there beinga context switch

// between threads.

let printSlowly (s : string) =

s.ToCharArray()

|>Array.iter (printf "%c")

printfn""

// create a thread that prints to the consolein an unsafe way

let makeUnsafeThread() =

newThread(fun () ->

forx in 1 .. 100 do

printSlowly"One ... Two ... Three ... ")

// the object that will be used as a lock

let lockObj = new Object()

// create a thread that prints to theconsole in a safe way

let makeSafeThread() =

newThread(fun () ->

forx in 1 .. 100 do

//use lock to ensure operation is atomic

locklockObj (fun () ->

printSlowly "One ... Two ... Three ... "))

// helper function to run the test to

let runTest (f: unit -> Thread) message=

printfn"%s" message

lett1 = f() in

lett2 = f() in

t1.Start()

t2.Start()

t1.Join()

t2.Join()

// runs the demonstrations

let main() =

runTest

makeUnsafeThread

"Runningtest without locking ..."

runTest

makeSafeThread

"Runningtest with locking ..."

do main()

为了突出重点,我们把示例中使用锁的部分再重复一下,应该注意两个重要的地方:第一,通过声明lockObj 创建一个临界区;第二,把我们的代码嵌入到makeSafeThread 函数中的锁函数中。需要注意的最重要部分,是怎样、何时把想要原子化的函数,放在内部函数传递给锁函数:

// the object that will be used as a lock

let lockObj = new Object()

// create a thread that prints to theconsole in a safe way

let makeSafeThread() =

newThread(fun () ->

forx in 1 .. 100 do

//use lock to ensure operation is atomic

locklockObj (fun () ->

printSlowly "One ... Two ... Three ... "))

第一部分代码每次运行结果不同,因为它取决于线程何时发生上下文切换;它还取决于处理器数量,因为如果机器在有两个或更多的处理器,同时可能有多个线程在运行,这样,消息将会更加紧密地挤在一起;而在单处理器的机器上,输出很少会挤在一起,因为,当上下文切换时,消息已经打印出去了[ 原文:On a single-processor machine, the output will be less tightlypacked together because printing a message will go wrong
only when a contentswitch takes place ]。下面是第一部分代码在双处理器机器运行的示意:

Running test without locking ...

...

One ... Two ... Three ...

One One ... Two ... Three ...

One ... Two ... Three ...

...

而锁定意谓着示例的第二部分的结果根本不会不同,因此,看到的问题这样:

Running test with locking ...

One ... Two ... Three ...

One ... Two ... Three ...

One ... Two ... Three ...

...

锁定是并发的一个重要方面,所有需要在线程之间进行写入或共享的资源都应该锁定。资源通常是可变的,可能是文件,甚至是控制台,正如示例所演示的。虽然锁能够解决并发性,但它也有产生问题,因为会产生死锁(deadlock)。当两个或更多的线程锁定了其他线程需要的资源,而谁也不会先释放,于是就发生了死锁。解决并发性的最简单办法是避免共享可能发生写入的资源。在本章的后面,我们将会看到创建的并行程序并不显式依赖锁。

注意

本书介绍的线程内容非常有限,如果想进行并行编程还需要更多地了解线程。在 MSDN 上的托管线程处理基本知识是一个好去处:http://msdn.microsoft.com/zh-cn/library/hyz69czz.aspx[ 已经改成中文站点了。];另外,也可以在http://www.albahari.com/threading/找到有用的教程。

线程、内存、锁定和阻塞(Threads, Memory, Locking, and Blocking),布布扣,bubuko.com

时间: 2024-10-11 04:48:43

线程、内存、锁定和阻塞(Threads, Memory, Locking, and Blocking)的相关文章

java线程内存模型,线程、工作内存、主内存

转自:http://rainyear.iteye.com/blog/1734311 java线程内存模型 线程.工作内存.主内存三者之间的交互关系图: key edeas 所有线程共享主内存 每个线程有自己的工作内存 refreshing local memory to/from main memory must  comply to JMM rules 产生线程安全的原因 线程的working memory是cpu的寄存器和高速缓存的抽象描述:现在的计算机,cpu在计算的时候,并不总是从内存读

Android系统匿名共享内存Ashmem(Anonymous Shared Memory)驱动程序源代码分析

文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6664554 在上一文章Android系统匿名共享内存Ashmem(Anonymous Shared Memory)简要介绍和学习计划中, 我们简要介绍了Android系统的匿名共享内存机制,其中,简要提到了它具有辅助内存管理系统来有效地管理内存的特点,但是没有进一步去了解它是如何实 现的.在本文中,我们将通过分析Android系统的匿名共享内存

Android加载图片导致内存溢出(Out of Memory异常)

Android在加载大背景图或者大量图片时,经常导致内存溢出(Out of Memory  Error),本文根据我处理这些问题的经历及其它开发者的经验,整理解决方案如下(部分代码及文字出处无法考证):  方案一.读取图片时注意方法的调用,适当压缩  尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗

图片--Android加载图片导致内存溢出(Out of Memory异常)

Android在加载大背景图或者大量图片时,经常导致内存溢出(Out of Memory  Error),本文根据我处理这些问题的经历及其它开发者的经验,整理解决方案如下(部分代码及文字出处无法考证):  方案一.读取图片时注意方法的调用,适当压缩  尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗

SQL Server 的锁定和阻塞

本帖提供两种做法,可避免在 SQL Server 事务锁定时产生的不正常或长时间阻塞,让用户和程序也无限期等待,甚至引起 connection pooling 连接数超过容量. 所谓的「阻塞」,是指当一个数据库会话中的事务,正在锁定其他会话事务想要读取或修改的资源,造成这些会话发出的请求进入等待的状态.SQL Server 默认会让被阻塞的请求无限期地一直等待,直到原来的事务释放相关的锁,或直到它超时 (根据 SET LOCK_TIMEOUT,本文后续会提到).服务器关闭.进程被杀死.一般的系统

一个线程内存泄漏问题定位过程

关键词:meminfo.slabinfo.top.pthread_join.thread stack等等. 记录一个关于线程内存泄漏问题的定位过程,以及过程中的收获. 1. 初步定位 是否存在内存泄漏:想到内存泄漏,首先查看/proc/meminfo,通过/proc/meminfo可以看出总体内存在下降.确定内存泄漏确实存在.top中可以显示多种形式内存,进而可以判断是那种泄漏.比如vss/rss/pss等. 确定哪个进程内存泄漏:通过top即可查看到是哪个进程在泄漏.至此基本可以确定到哪个进程

java进阶07 线程的让步与阻塞与同步

前面介绍了线程的一些基本知识,现在来说下线程的让步,阻塞,和同步 先说说让步 所谓让步,就是让调用者的线程暂停,让其他线程重新竞争CPU,包括调用者. 先看看代码 package Thread; public class ThreadYield { public static void main(String[] args){ MyThread5 rthread=new MyThread5(); Thread thread1=new Thread(rthread); Thread thread2

如何判断C#的Finalizer线程有没有被阻塞

博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:如何判断C#的Finalizer线程有没有被阻塞.

MySQL中内存分为全局内存和线程内存

首先我们来看一个公式,MySQL中内存分为全局内存和线程内存两大部分(其实并不全部,只是影响比较大的 部分): 复制代码 代码如下: per_thread_buffers=(read_buffer_size+read_rnd_buffer_size+sort_buffer_size+thread_stack+join_buffer_size+binlog_cache_size+tmp_table_size)*max_connectionsglobal_buffers=innodb_buffer_