明经CAD社区

 找回密码
 注册

QQ登录

只需一步,快速开始

搜索
查看: 4073|回复: 6

[Kean专集] Kean专题(4)—Concurrent_Programming

 关闭 [复制链接]
发表于 2009-5-17 09:40 | 显示全部楼层 |阅读模式
本帖最后由 作者 于 2009-5-17 12:35:37 编辑

一、在AutoCAD使用异步工作流简化并发程序设计(F#)
January 25, 2008
Using F# Asynchronous Workflows to simplify concurrent programming in AutoCAD
In the last post we saw some code that downloaded data - serially - from a number of websites via RSS and created AutoCAD entities linking to the various posts.
As promised, in today's post we take that code and enable it to query the same data in parallel by using Asynchronous Workflows in F#. Asynchronous Workflows are an easy-to-use yet powerful mechanism for enabling concurrent programming in F#.
Firstly, a little background as to why this type of technique is important. As many - if not all - of you are aware, the days of raw processor speed doubling every couple of years are over. The technical innovations that enabled  Moore's Law to hold true for half a century are - at least in the area of silicon-based microprocessor design - hitting a wall (it's apparently called the Laws of Physics :-). Barring some disruptive technological development, the future gains in computing performance are to be found in the use of parallel processing, whether via multiple cores, processors or distributed clouds of computing resources.
Additionally, with an increasing focus on distributed computing and information resources, managing tasks asynchronously becomes more important, as information requests across a network inevitably introduce a latency that can be mitigated by the tasks being run in parallel.
The big problem is that concurrent programming is - for the most-part - extremely difficult to do, and even harder to retro-fit into existing applications. Traditional lock-based parallelism (where locks are used to control access to shared computing resources) is both unwieldy and prone to blocking. New technologies, such as Asynchronous Workflows and Software Transactional Memory, provide considerable hope (and this is a topic I have on my list to cover at some future point).
Today's post looks at a relatively simple scenario, in the sense that we want to perform a set of discrete tasks in parallel, harnessing those fancy multi-core systems for those of you lucky enough to have them (I'm hoping to get one when I next replace my notebook, sometime in March), but that these tasks are indeed independent: we want to wait until they are all complete, but we do not have the additional burden of them communicating amongst themselves or using shared resources (e.g. accessing shared memory) during their execution.
We are also going to be very careful only to run parallel tasks unrelated to AutoCAD. Any access made into AutoCAD's database, for instance, needs to be performed in series: AutoCAD is not thread-safe when it comes to the vast majority of its programmatically-accessible functionality. So we're going to run a set of asynchronous, parallel tasks to query our various RSS feeds, and combine the results before creating the corresponding geometry in AutoCAD. This all sounds very complex, but the good (actually great) news is that Asynchronous Workflows does all the heavy lifting. Phew.
Here's the modified F# code
  1. // Use lightweight F# syntax
  2. #light
  3. // Declare a specific namespace and module name
  4. module MyNamespace.MyApplication
  5. // Import managed assemblies
  6. #I @"C:\Program Files\Autodesk\AutoCAD 2008"
  7. #r "acdbmgd.dll"
  8. #r "acmgd.dll"
  9. open Autodesk.AutoCAD.Runtime
  10. open Autodesk.AutoCAD.ApplicationServices
  11. open Autodesk.AutoCAD.DatabaseServices
  12. open Autodesk.AutoCAD.Geometry
  13. open System.Xml
  14. open System.Collections
  15. open System.Collections.Generic
  16. open System.IO
  17. open System.Net
  18. open Microsoft.FSharp.Control.CommonExtensions
  19. // The RSS feeds we wish to get. The first two values are
  20. // only used if our code is not able to parse the feed's XML
  21. let feeds =
  22.   [ ("Through the Interface",
  23.      "http://blogs.autodesk.com/through-the-interface",
  24.      "http://through-the-interface.typepad.com/through_the_interface/rss.xml");
  25.      
  26.     ("Don Syme's F# blog",
  27.      "http://blogs.msdn.com/dsyme/",
  28.      "http://blogs.msdn.com/dsyme/rss.xml");
  29.    
  30.     ("Shaan Hurley's Between the Lines",
  31.      "http://autodesk.blogs.com/between_the_lines",
  32.      "http://autodesk.blogs.com/between_the_lines/rss.xml");
  33.    
  34.     ("Scott Sheppard's It's Alive in the Lab",
  35.      "http://blogs.autodesk.com/labs",
  36.      "http://labs.blogs.com/its_alive_in_the_lab/rss.xml");
  37.    
  38.     ("Lynn Allen's Blog",
  39.      "http://blogs.autodesk.com/lynn",
  40.      "http://lynn.blogs.com/lynn_allens_blog/index.rdf");
  41.     ("Heidi Hewett's AutoCAD Insider",
  42.      "http://blogs.autodesk.com/autocadinsider",
  43.      "http://heidihewett.blogs.com/my_weblog/index.rdf") ]
  44. // Fetch the contents of a web page, asynchronously
  45. let httpAsync(url:string) =
  46.   async { let req = WebRequest.Create(url)
  47.           use! resp = req.GetResponseAsync()
  48.           use stream = resp.GetResponseStream()
  49.           use reader = new StreamReader(stream)
  50.           return reader.ReadToEnd() }
  51. // Load an RSS feed's contents into an XML document object
  52. // and use it to extract the titles and their links
  53. // Hopefully these always match (this could be coded more
  54. // defensively)
  55. let titlesAndLinks (name, url, xml) =
  56.   let xdoc = new XmlDocument()
  57.   xdoc.LoadXml(xml)
  58.   let titles =
  59.     [ for n in xdoc.SelectNodes("//*[name()='title']")
  60.         -> n.InnerText ]
  61.   let links =
  62.     [ for n in xdoc.SelectNodes("//*[name()='link']") ->
  63.         let inn = n.InnerText
  64.         if  inn.Length > 0 then
  65.           inn
  66.         else
  67.           let href = n.Attributes.GetNamedItem("href").Value
  68.           let rel = n.Attributes.GetNamedItem("rel").Value
  69.           if href.Contains("feedburner") then
  70.               ""
  71.           else
  72.             href ]
  73.          
  74.   let descs =
  75.     [ for n in xdoc.SelectNodes
  76.         ("//*[name()='description' or name()='content' or name()='subtitle']")
  77.           -> n.InnerText ]
  78.   // A local function to filter out duplicate entries in
  79.   // a list, maintaining their current order.
  80.   // Another way would be to use:
  81.   //    Set.of_list lst |> Set.to_list
  82.   // but that results in a sorted (probably reordered) list.
  83.   let rec nub lst =
  84.     match lst with
  85.     | a::[] -> [a]
  86.     | a::b ->
  87.       if a = List.hd b then
  88.         nub b
  89.       else
  90.         a::nub b
  91.     | [] -> []
  92.   // Filter the links to get (hopefully) the same number
  93.   // and order as the titles and descriptions
  94.   let real = List.filter (fun (x:string) -> x.Length > 0)  
  95.   let lnks = real links |> nub
  96.   // Return a link to the overall blog, if we don't have
  97.   // the same numbers of titles, links and descriptions
  98.   let lnum = List.length lnks
  99.   let tnum = List.length titles
  100.   let dnum = List.length descs
  101.   
  102.   if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then
  103.     [(name,url,url)]
  104.   else
  105.     List.zip3 titles lnks descs
  106. // For a particular (name,url) pair,
  107. // create an AutoCAD HyperLink object
  108. let hyperlink (name,url,desc) =
  109.   let hl = new HyperLink()
  110.   hl.Name <- url
  111.   hl.Description <- desc
  112.   (name, hl)
  113. // Use asynchronous workflows in F# to download
  114. // an RSS feed and return AutoCAD HyperLinks
  115. // corresponding to its posts
  116. let hyperlinksAsync (name, url, feed) =
  117.   async { let! xml = httpAsync feed
  118.           let tl = titlesAndLinks (name, url, xml)
  119.           return List.map hyperlink tl }
  120. // Now we declare our command
  121. [<CommandMethod("rss")>]
  122. let createHyperlinksFromRss() =
  123.   
  124.   // Let's get the usual helpful AutoCAD objects
  125.   
  126.   let doc =
  127.     Application.DocumentManager.MdiActiveDocument
  128.   let db = doc.Database
  129.   // "use" has the same effect as "using" in C#
  130.   use tr =
  131.     db.TransactionManager.StartTransaction()
  132.   // Get appropriately-typed BlockTable and BTRs
  133.   let bt =
  134.     tr.GetObject
  135.       (db.BlockTableId,OpenMode.ForRead)
  136.     :?> BlockTable
  137.   let ms =
  138.     tr.GetObject
  139.       (bt.[BlockTableRecord.ModelSpace],
  140.        OpenMode.ForWrite)
  141.     :?> BlockTableRecord
  142.   
  143.   // Add text objects linking to the provided list of
  144.   // HyperLinks, starting at the specified location
  145.   
  146.   // Note the valid use of tr and ms, as they are in scope
  147.   let addTextObjects pt lst =
  148.     // Use a for loop, as we care about the index to
  149.     // position the various text items
  150.     let len = List.length lst
  151.     for index = 0 to len - 1 do
  152.       let txt = new DBText()
  153.       let (name:string,hl:HyperLink) = List.nth lst index
  154.       txt.TextString <- name
  155.       let offset =
  156.         if index = 0 then
  157.           0.0
  158.         else
  159.           1.0
  160.       // This is where you can adjust:
  161.       //  the initial outdent (x value)
  162.       //  and the line spacing (y value)
  163.       let vec =
  164.         new Vector3d
  165.           (1.0 * offset,
  166.            -0.5 * (Int32.to_float index),
  167.            0.0)
  168.       let pt2 = pt + vec
  169.       txt.Position <- pt2
  170.       ms.AppendEntity(txt) |> ignore
  171.       tr.AddNewlyCreatedDBObject(txt,true)
  172.       txt.Hyperlinks.Add(hl) |> ignore
  173.   // Here's where we do the real work, by firing
  174.   // off - and coordinating - asynchronous tasks
  175.   // to create HyperLink objects for all our posts
  176.   let links =
  177.     Async.Run
  178.       (Async.Parallel
  179.         [ for (name,url,feed) in feeds ->
  180.           hyperlinksAsync (name,url,feed) ])
  181.   // Add the resulting objects to the model-space  
  182.   let len = Array.length links
  183.   for index = 0 to len - 1 do
  184.     // This is where you can adjust:
  185.     //  the column spacing (x value)
  186.     //  the vertical offset from origin (y axis)
  187.     let pt =
  188.       new Point3d
  189.         (15.0 * (Int32.to_float index),
  190.          30.0,
  191.          0.0)
  192.     addTextObjects pt (Array.get links index)
  193.   tr.Commit()
A few comments on the changes:
Lines 57-62 define our new httpAsync() function, which uses GetResponseAsync() - a function exposed in F# 1.9.3.7 - to download the contents of a web-page asynchronously [and which I stole shamelessly from Don Syme, who presented the code last summer at Microsoft's TechEd].
Lines 141-144 define another asynchronous function, hyperlinksAsync(), which calls httpAsync() and then - as before - extracts the feed information and creates a corresponding list of HyperLinks. This is significant: creation of AutoCAD HyperLink objects will be done on parallel; it is the addition of these objects to the drawing database that needs to be performed serially.
Lines 214-217 replace our very simple "map" with something slightly more complex: this code runs a list of tasks in parallel and waits for them all to complete before continuing. What is especially cool about this implementation is the fact that exceptions in individual tasks result in the overall task failing (a good thing, believe it or not :-), and the remaining tasks being terminated gracefully.
Lines 221 and 233 change our code to handle an array, rather than a list (while "map" previously returned a list, Async.Run returns an array).
When run, the code creates exactly the same thing as last time (although there are a few more posts in some of the blogs ;-)


A quick word on timing: I used "F# Interactive" to do a little benchmarking on my system, and even though it's single-core, single-processor, there was a considerable difference between the two implementations. I'll talk more about F# Interactive at some point, but think of it to F# in Visual Studio as the command-line is to LISP in AutoCAD: you can very easily test out fragments of F#, either by entering them directly into the F# Interactive window or highlighting them in Visual Studio's text editor and hitting Alt-Enter.
To enable function timing I entered "#time;;" (without the quotations marks) in the F# Interactive window. I then selected and loaded the supporting functions needed for each test - not including the code that adds the DBText objects with their HyperLinks to the database, as we're only in Visual Studio, not inside AutoCAD - and executed the "let links = ..." assignment in our two implementations of the createHyperlinksFromRss() function (i.e. the RSS command). These functions do create lists of AutoCAD HyperLinks, but that's OK: this is something works even outside AutoCAD, although we wouldn't be able to do anything much with them. Also, the fact we're not including the addition of the entities to the AutoCAD database is not relevant: by then we should have identical data in both versions, which would be added in exactly the same way.
Here are the results:
  1. I executed the code for serial querying and parallel querying twice (to make sure there were no effects from page caching on the measurement):
  2. val links : (string * HyperLink) list list
  3. Real: 00:00:14.636, CPU: 00:00:00.15, GC gen0: 5, gen1: 1, gen2: 0
  4. val links : (string * HyperLink) list array
  5. Real: 00:00:06.245, CPU: 00:00:00.31, GC gen0: 3, gen1: 0, gen2: 0
  6. val links : (string * HyperLink) list list
  7. Real: 00:00:15.45, CPU: 00:00:00.46, GC gen0: 5, gen1: 1, gen2: 0
  8. val links : (string * HyperLink) list array
  9. Real: 00:00:03.832, CPU: 00:00:00.62, GC gen0: 2, gen1: 1, gen2: 0
  10. So the serial execution took 14.5 to 15.5 seconds, while the parallel execution took 3.8 to 6.3 seconds.
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
 楼主| 发表于 2009-5-17 10:15 | 显示全部楼层

二、用多义线路径模拟Hatch

February 21, 2008
Parallelizing robotic AutoCAD hatching with F# and .NET
In the last post we saw some code combining F# with C# to create a random "hatch" - a polyline path that bounces around within a boundary.

This post extends the code to make use of Asynchronous Workflows in F# to parallelize the testing of points along a segment. In the initial design of the application I decided to test 10 points along each segment, to see whether it remained entirely within our boundary: the idea being that this granularity makes it very likely the segment will fail the test, should it happen to leave the boundary at any point. Not 100% guaranteed, but a high probability event. What this code does is take the 10 tests and queue them up for concurrent processing (where the system is capable of it).

Asynchronous Workflows - as suggested by the name - were intended to fire off and manage asynchronous tasks (ones that access network resources, for instance). The segment testing activity is actually very local and processor-bound, so it's not really what the mechanism was intended for, but I thought it would be interesting to try. One interesting point: while testing this code I noticed that it actually ran slower on a single processor machine, which is actually quite logical: only one core is available for processing, so the amount of sequential processing is not reduced but the overhead of synchronizing the various tasks is added. So it was fairly inevitable it would take longer. In the post I first talked about Asynchronous Workflows I showed a sample that queried multiple RSS sites for data: even on a single processor machine this was significantly quicker, as parallelizing the network latency led to a clear gain.

Anyway, as it was slower on this machine I decided only to enable the parallel version of the code in cases where the computer's NUMBER_OF_PROCESSORS environment variable is greater than 1. I checked quickly on a colleague's dual-core machine, and sure enough this variable is set to 2 on his system. I haven't, however, tested the code on a dual- or multi-core system, but I'll be getting a new system in a matter of weeks, which will give me the chance to test it out.

Here's the the complete project which defines both FB and FBA commands for simple side-by-side comparison.

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
 楼主| 发表于 2009-5-17 10:24 | 显示全部楼层

三、编程语言分类

March 17, 2008
A simple taxonomy of programming languages
Someone asked me recently how I categorize different programming paradigms. I thought it was a very interesting question, so here's what I responded. Please bear in mind that this is very much the way I see things, and is neither an exhaustive nor a formally-ratified taxonomy.

One way to look at languages is whether they're declarative or imperative:

 Declarative programming languages map the way things are by building up “truths”: this category includes  functional programming languages (such as  Miranda,  Haskell and  Erlang) which tend to be mathematical in nature (you define equations) and start with  lambda calculus as a foundation. The other main set of declarative languages are  logic programming languages (such as  Prolog), which start with  propositional calculus as a foundation (you declare axioms that build up to a system against which you can run queries). Declarative languages tend to focus on describing the problem to solve, rather than how to solve it.

 Imperative programming languages, on the other hand, are lists of instructions of what to do: I tend to consider  procedural programming languages (such as  C,  COBOL,  Fortran and  Pascal) as a sub-category which focus on the definition and execution of sub-routines, while some people treat the terms imperative and procedural as synonyms.

Considering these definitions,  object-oriented programming languages (such as  Smalltalk and  Eiffel) should probably be considered declarative, as conceptually they map real-world objects, but the truth is that the most popular OO languages (such as  C++) are impure, and so most OO systems combine big chunks of procedural (i.e. imperative) code. Many people who think they’re doing OOP are actually packaging up procedures.

Note that I've tried not to list multi-paradigm languages such as  Ada,  C++ and  F# in the above categorisation. It's possible that some of the languages I've listed are also multi-paradigm, but anyway.

One other way to think about languages is whether they’re top-down or bottom-up:

Bottom-up languages are ultimately layered on how a processor works (from machine code to assembly language to C & C++), while top-down languages start from the world of mathematics and logic and add language features that allow them to be used for programming (i.e. declarative languages are therefore top-down). This latter set of languages are starting to see increased adoption, as they assume much less (even nothing) about the underlying machinery, in which big changes are occurring with multiple processing cores being introduced (which essentially invalidate the assumptions of previous generations of programmers, who have been conditioned to think in terms of the processor's ability to store and access state).

Many popular - or soon to be popular - programming environments are pragmatic in nature: C++ allows OOP but can also be used for procedural programming, VB.NET now allows you to define and access objects while coming from a long line of procedural languages, F# is multi-paradigm, combining OO with functional and imperative programming.

There are bound to be people with differing views on this subject (and many of them are no doubt more intelligent and experienced in these matters than I), but this is how I would answer the question of how to categorise programming languages.

For those of you with an interest in the future of programming languages, I can strongly recommend the following Channel 9 episodes. If you're not aware of Channel 9, then prepare to be impressed: Microsoft has given a fantastic gift to the development community with this resource.

Burton Smith: On General Purpose Super Computing and the History and Future of Parallelism
Erik Meijer: Functional Programming
Anders Hejlsberg, Herb Sutter, Erik Meijer, Brian Beckman: Software Composability and the Future of Languages
Brian Beckman: Don't fear the Monads
Joe Armstrong - On Erlang, OO, Concurrency, Shared State and the Future, Part 1
Joe Armstrong - On Erlang, OO, Concurrency, Shared State and the Future, Part 2

Enjoy! :-)

 楼主| 发表于 2009-5-17 10:31 | 显示全部楼层

四、销毁AutoCAD对象的时机和方法

June 16, 2008
Cleaning up after yourself: how and when to dispose of AutoCAD objects in .NET
A question came up recently in an internal discussion and I thought I'd share it as it proved so illuminating.

If I have an object of a type which implements IDisposable, is it good practice to explicitly dispose it (whether via the using statement or calling Dispose() explicitly)?

The quick(ish) answer is:

Yes it is, but sometimes you might choose not to as the increase in code simplicity outweighs the benefits derived from manually disposing of the objects.

So, naturally, the devil is in the detail. Let's take a look at the three scenarios where you're likely to be working with IDisposable objects inside AutoCAD:

Temporary objects - such as those provided by Autodesk.AutoCAD.Geometry - which are never Database-resident
Temporary objects with the potential to be database-resident but which never actually get added to a Database
Database-resident objects added/accessed via a Transaction
Below follows the details on each of these categories.

Temporary objects of types not derived from DBObject
The first category of temporary objects, such as Geometry.Line, are safe to be disposed of either "manually" (by your own code) or "automatically" (by the .NET garbage collector).

Temporary objects of DBObject-derived types
The second category of temporary objects, which are of a type derived from DBObject, must be disposed of manually. It is absolutely unsafe not to dispose of objects of DBObject-derived classes from the main execution thread in AutoCAD.

Why is this the case? Firstly, the majority of the classes available through AutoCAD's .NET API are currently unmanaged, with a relatively thin wrapper exposing them to the .NET world. Inside AutoCAD, all Database-resident objects are managed by a single-threaded runtime component, AcDb (which, along with some other components, is productized as Autodesk RealDWG). A side note: if you're using ObjectARX or RealDWG from C++, don't be confused by the fact your project's C-runtime memory management is likely to be "Multi-threaded DLL", RealDWG is not thread-aware and so must be treated as single-threaded, for all intents and purposes.

And - secondly - on to the reason that automatic garbage collection is not to be trusted on DBObject-derived types: the .NET garbage collector runs on a separate, typically low-priority - unless memory is running short - thread. So if a DBObject-derived type is garbage-collected inside AutoCAD, a separate thread will essentially call into a non thread-safe component (RealDWG), which is very, very likely to cause AutoCAD to crash.

So you must always call Dispose() on temporary objects of DBObject-derived classes in your .NET code - you cannot rely on the CLR to manage your objects'  lifetimes for you.

Interestingly, this is also the reason why the F# code I posted in this prior post will not effectively leverage multiple cores (something I've just tested since getting a multi-core machine). We are using a Ray (which is a DBObject-derived class) to get intersection points with our path, and then disposing of this temporary object from an arbitrary thread (farmed off via F# Asynchronous Workflows). So this is unsafe, and won't run for long before crashing. At least now I understand why.

Database-resident, Transaction-managed objects
The third category of objects are those we use a Transaction either to add to the Database or to open for access. The good news is that - as the Transaction is aware of the objects it manages (via calls to AddNewlyCreatedDBObject() or to GetObject()), it is able to dispose of them automatically when it, itself, is disposed. So the key here is to make sure you wrap your use of Transactions in using blocks or call Dispose() on them when you're done. There is no need to explicitly dispose of the objects managed by a Transaction (unless there is a failure between the time the object is created and when it is added to the transaction via AddNewlyCreatedDBObject(), of course).

 楼主| 发表于 2009-5-17 10:37 | 显示全部楼层
 楼主| 发表于 2009-5-17 10:55 | 显示全部楼层
六、一个简单的注标工具(?)
January 28, 2009
Implementing a simple graphing tool inside AutoCAD using F#
Well, I couldn't resist... as I mentioned in the last post - where we looked at creating a simple graph inside AutoCAD as an example of modifying objects inside nested transactions - the idea of graphing inside AutoCAD is a good fit for F#. This is for a number of reasons: F# is very mathematical in nature and excels at processing lists of data. I also spiced it up a bit by adding some code to parallelise some of the mathematical operations, but that didn't turn out to be especially compelling with my dual-core laptop. More on that later.
Here's the F# code:
  1. // Use lightweight F# syntax
  2. #light
  3. // Declare a specific namespace and module name
  4. module Grapher.Commands
  5. // Import managed assemblies
  6. open Autodesk.AutoCAD.Runtime
  7. open Autodesk.AutoCAD.ApplicationServices
  8. open Autodesk.AutoCAD.DatabaseServices
  9. open Autodesk.AutoCAD.Geometry
  10. // Define a common normalization function which makes sure
  11. // our graph gets mapped to our grid
  12. let normalize fn normFn x minInp maxInp maxOut =
  13.   let res =
  14.     fn ((maxInp - minInp) * x / maxOut)
  15.   let normRes = normFn res
  16.   if normRes >= 0.0 && normRes <= 1.0 then
  17.     normRes * (maxOut - 1.0)
  18.   else
  19.     -1.0
  20. // Define some shortcuts to the .NET Math library
  21. // trigonometry functions
  22. let sin x = System.Math.Sin x
  23. let cos x = System.Math.Cos x        
  24. let tan x = System.Math.Tan x        
  25. // Implement our own normalized trig functions
  26. // which each map to the size of the grid passed in
  27. let normSin max x =
  28.   let nf a = (a + 1.0) / 2.0 // Normalise to 0-1
  29.   let res =
  30.     normalize
  31.       sin nf (Int32.to_float x)
  32.       0.0 (2.0 * System.Math.PI) (Int32.to_float max)
  33.   Int32.of_float res
  34. let normCos max x =
  35.   let nf a = (a + 1.0) / 2.0 // Normalise to 0-1
  36.   let res =
  37.     normalize
  38.       cos nf (Int32.to_float x)
  39.       0.0 (2.0 * System.Math.PI) (Int32.to_float max)
  40.   Int32.of_float res
  41. let normTan max x =
  42.   let nf a = (a + 3.0) / 6.0 // Normalise differently for tan
  43.   let res =
  44.     normalize
  45.       tan nf (Int32.to_float x)
  46.       0.0 (2.0 * System.Math.PI) (Int32.to_float max)
  47.   Int32.of_float res
  48. // Now we declare our command
  49. [<CommandMethod("graph")>]
  50. let gridCommand() =
  51.   // We'll time the command, so we can check the
  52.   // sync vs. async efficiency
  53.   let starttime = System.DateTime.Now
  54.   // Let's get the usual helpful AutoCAD objects
  55.   let doc =
  56.     Application.DocumentManager.MdiActiveDocument
  57.   let ed = doc.Editor
  58.   let db = doc.Database
  59.   // "use" has the same effect as "using" in C#
  60.   use tr =
  61.     db.TransactionManager.StartTransaction()
  62.   // Get appropriately-typed BlockTable and BTRs
  63.   let bt =
  64.     tr.GetObject
  65.       (db.BlockTableId,OpenMode.ForRead)
  66.     :?> BlockTable
  67.   let ms =
  68.     tr.GetObject
  69.       (bt.[BlockTableRecord.ModelSpace],
  70.        OpenMode.ForWrite)
  71.     :?> BlockTableRecord
  72.   // Function to create a filled circle (hatch) at a
  73.   // specific location
  74.   // Note the valid use of tr and ms, as they are in scope
  75.   let createCircle pt rad =
  76.     let hat = new Hatch()
  77.     hat.SetDatabaseDefaults();
  78.     hat.SetHatchPattern
  79.       (HatchPatternType.PreDefined,
  80.        "SOLID")
  81.     let id = ms.AppendEntity(hat)
  82.     tr.AddNewlyCreatedDBObject(hat, true)
  83.     // Now we create the loop, which we make db-resident
  84.     // (appending a transient loop caused problems, so
  85.     // we're going to use the circle and then erase it)
  86.     let cir = new Circle()
  87.     cir.Radius <- rad
  88.     cir.Center <- pt
  89.     let lid = ms.AppendEntity(cir)
  90.     tr.AddNewlyCreatedDBObject(cir, true)
  91.     // Have the hatch use the loop we created
  92.     let loops = new ObjectIdCollection()
  93.     loops.Add(lid) |> ignore
  94.     hat.AppendLoop(HatchLoopTypes.Default, loops)
  95.     hat.EvaluateHatch(true)
  96.     // Now we erase the loop
  97.     cir.Erase()
  98.     id
  99.   // Function to create our grid of circles
  100.   let createGrid size rad offset =
  101.     let ids = new ObjectIdCollection()
  102.     for i = 0 to size - 1 do
  103.       for j = 0 to size - 1 do
  104.         let pt =
  105.           new Point3d
  106.             (offset * (Int32.to_float i),
  107.             offset * (Int32.to_float j),
  108.             0.0)
  109.         let id = createCircle pt rad
  110.         ids.Add(id) |> ignore
  111.     ids
  112.   // Function to change the colour of an entity
  113.   let changeColour col (id : ObjectId) =
  114.     if id.IsValid then
  115.       let ent =
  116.         tr.GetObject(id, OpenMode.ForWrite) :?> Entity
  117.       ent.ColorIndex <- col
  118.   // Shortcuts to make objects red and yellow
  119.   let makeRed = changeColour 1
  120.   let makeYellow = changeColour 2
  121.   // Function to retrieve the contents of our
  122.   // array of object IDs - this just calculates
  123.   // the index based on the x & y values
  124.   let getIndex fn size i =
  125.     let res = fn size i
  126.     if res >= 0 then
  127.         (i * size) + res
  128.     else
  129.         -1
  130.   // Apply our function synchronously for each value of x
  131.   let applySyncBelowMax size fn =
  132.     [| for i in [0..size-1] ->
  133.        getIndex fn size i |]
  134.   // Apply our function asynchronously for each value of x
  135.   let applyAsyncBelowMax size fn =
  136.     Async.Run
  137.       (Async.Parallel
  138.         [ for i in [0..size-1] ->
  139.           async { return getIndex fn size i } ])
  140.   // Hardcode the size of the grid and create it
  141.   let size = 50
  142.   let ids = createGrid size 0.5 1.2
  143.   // Make the circles all red to start with
  144.   Seq.iter makeRed (Seq.cast ids)
  145.   // From a certain index in the list, get an object ID
  146.   let getId i =
  147.     if i >= 0 then
  148.       ids.[i]
  149.     else
  150.       ObjectId.Null
  151.   // Apply one of our trig functions, synchronously or
  152.   // otherwise, to our grid
  153.   applySyncBelowMax size normSin |>
  154.     Array.map getId |>
  155.       Array.iter makeYellow
  156.   // Commit the transaction
  157.   tr.Commit()
  158.   // Check how long it took
  159.   let elapsed =
  160.       System.DateTime.op_Subtraction
  161.         (System.DateTime.Now, starttime)
  162.   ed.WriteMessage
  163.     ("\nElapsed time: " +
  164.     elapsed.ToString())
Here's what you see on AutoCAD's drawing canvas when you run the GRAPH command as it stands:

If you want to play around with other functions, you can edit the call to applySyncBelowMax to pass normCos or normTan instead of normSin.


As I mentioned earlier, if you swap the call to be applyAsyncBelowMax instead of applySyncBelowMax you will actually run the mathematics piece as asynchronous tasks. These are CPU-bound operations - they don't call across the network or write to a hard-drive, which might have increased the benefit of calling them asynchronously - so right now the async version actually runs more slowly than the sync version. If I were to have more processing cores available to me, it might also give us more benefit, but right now with my dual-core machine there's more effort spent coordinating the tasks than you gain from the parallelism. But I'll let you play around with that yourselves... you may get better results. One other note on that piece of the code: at some point I'd like to make use of the Parallel Extensions for .NET (in particular the Task Parallel Library (TPL)), but for now I've continued with what I know, the asynchronous worklows capability which is now standard in F#.
I'm travelling in India this week (and working from our Bangalore office next week), so this is likely to be my last post of the week.

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
 楼主| 发表于 2009-5-17 11:01 | 显示全部楼层
本帖最后由 作者 于 2009-9-9 8:51:21 编辑

七、并行像素化
February 02, 2009
Parallelized pixelization inside AutoCAD using F#
As promised in the last post, we're now going to look at how to change the code to make the colour averaging routine work in parallel. The overall performance is marginally better on my dual-core machine, but I fully expect it to get quicker and quicker as the number of cores multiply.
To start with, though, here's the modified "synchronous" version of the code - as I went through making the code work in parallel, I noticed a bunch of general enhancements that were applicable to both versions. Here's the updated F# code:
  1. // Use lightweight F# syntax
  2. #light
  3. // Declare a specific namespace and module name
  4. module SyncPixelizer.Commands
  5. // Import managed assemblies
  6. #nowarn "9" // ... because we're using NativePtr
  7. open Autodesk.AutoCAD.Runtime
  8. open Autodesk.AutoCAD.ApplicationServices
  9. open Autodesk.AutoCAD.DatabaseServices
  10. open Autodesk.AutoCAD.EditorInput
  11. open Autodesk.AutoCAD.Geometry
  12. open Autodesk.AutoCAD.Colors
  13. open System.Drawing.Imaging
  14. open Microsoft.FSharp.NativeInterop
  15. // Add up the RGB values of a list of pixels
  16. // We use a recursive function with an accumulator argument,
  17. // (rt, gt, bt), to allow tail call optimization
  18. let rec sumColors (pix : List<(byte*byte*byte)>) (rt,gt,bt) =
  19.   match pix with
  20.     | [] -> (rt, gt, bt)
  21.     | (r, g, b) :: tl ->
  22.         sumColors tl
  23.           (rt + Byte.to_int r,
  24.           gt + Byte.to_int g,
  25.           bt + Byte.to_int b)
  26. // Average out the RGB values of a list of pixels
  27. let getAverageColour (pixels : List<(byte*byte*byte)>) =
  28.   let (rsum, gsum, bsum) =
  29.     sumColors pixels (0, 0, 0)
  30.   let count = pixels.Length
  31.   let ravg = Byte.of_int (rsum / count)
  32.   let gavg = Byte.of_int (gsum / count)
  33.   let bavg = Byte.of_int (bsum / count)
  34.   // For some reason the pixel needs ro be reversed - probably
  35.   // because of the bitmap format (needs investigation)
  36.   Color.FromRgb(bavg, gavg, ravg)
  37. // Function to get an index into our flat array
  38. // from an x,y pair
  39. let getIndexFromXY ysize x y =
  40.   (x * ysize) + y
  41. //  Get a chunk of pixels to average from one row
  42. // We use a recursive function with an accumulator argument
  43. // to allow tail call optimization
  44. let rec getChunkRowPixels p xsamp acc =
  45.   if xsamp = 0 then
  46.     acc
  47.   else
  48.     let pix =
  49.       [(NativePtr.get p 0,
  50.         NativePtr.get p 1,
  51.         NativePtr.get p 2)]
  52.     let p = NativePtr.add p 3 // We do *not* mutate here
  53.     getChunkRowPixels p (xsamp-1) (pix @ acc)
  54. // Get a chunk of pixels to average from multiple rows
  55. // We use a recursive function with an accumulator argument
  56. // to allow tail call optimization
  57. let rec getChunkPixels p stride xsamp ysamp acc =
  58.   if ysamp = 0 then
  59.     acc
  60.   else
  61.     let pix = getChunkRowPixels p xsamp []
  62.     let p = NativePtr.add p stride  // We do *not* mutate here
  63.     getChunkPixels p stride xsamp (ysamp-1) (pix @ acc)
  64. // Get the various chunks of pixels to average across
  65. // a complete bitmap image
  66. let pixelizeBitmap (image:System.Drawing.Bitmap) xsize ysize =
  67.   // Create a 1-dimensional array of pixel lists (one list,
  68.   // which then needs averaging, per final pixel)
  69.   let (arr : List<(byte*byte*byte)>[]) =
  70.     Array.create (xsize * ysize) []
  71.   // Lock the entire memory block related to our image
  72.   let bd =
  73.     image.LockBits
  74.       (System.Drawing.Rectangle
  75.         (0, 0, image.Width ,image.Height),
  76.       ImageLockMode.ReadOnly, image.PixelFormat)
  77.   // Establish the number of pixels to sample per chunk
  78.   // in each of the x and y directions
  79.   let xsamp = image.Width / xsize
  80.   let ysamp = image.Height / ysize
  81.   // We have a mutable pointer to step through the image
  82.   let mutable (p:nativeptr<byte>) =
  83.     NativePtr.of_nativeint (bd.Scan0)
  84.   // Loop through the various chunks
  85.   for i = 0 to ysize - 1 do
  86.     // We take a copy of the current value of p, as we
  87.     // don't want to mutate p while extracting the pixels
  88.     // within a row
  89.     let mutable xp = p
  90.     for j = 0 to xsize - 1 do
  91.       // Get the square chunk of pixels starting at
  92.       // this x,y position
  93.       let chk =
  94.         getChunkPixels xp bd.Stride xsamp ysamp []
  95.       // Add it into our array
  96.       let idx = getIndexFromXY ysize j (ysize-1-i)
  97.       arr.[idx] <- chk
  98.       // Mutate the pointer to move along to the right
  99.       // by a value of 3 (our RGB value) times the
  100.       // number of pixels we're sampling in x
  101.       xp <- NativePtr.add xp (xsamp * 3)
  102.     done
  103.     // Mutate the original p pointer to move on one row
  104.     p <- NativePtr.add p (bd.Stride * ysamp)
  105.   done
  106.   // Finally unlock the bitmap data and return the array
  107.   image.UnlockBits(bd)
  108.   arr
  109. // Create an array of ObjectIds from a collection
  110. let getIdArray (ids : ObjectIdCollection) =
  111.   [| for i in [0..ids.Count-1] -> ids.[i] |]
  112. // Declare our command
  113. [<CommandMethod("pix")>]
  114. let pixelize() =
  115.   // Let's get the usual helpful AutoCAD objects
  116.   let doc =
  117.     Application.DocumentManager.MdiActiveDocument
  118.   let ed = doc.Editor
  119.   let db = doc.Database
  120.   // Prompt the user for the file and the width of the image
  121.   let pofo =
  122.     new PromptOpenFileOptions
  123.       ("Select an image to import and pixelize")
  124.   pofo.Filter <-
  125.     "Jpeg Image (*.jpg)|*.jpg|All files (*.*)|*.*"
  126.   let pfnr = ed.GetFileNameForOpen(pofo)
  127.   let file =
  128.     match pfnr.Status with
  129.     | PromptStatus.OK ->
  130.         pfnr.StringResult
  131.     | _ ->
  132.         ""
  133.   if System.IO.File.Exists(file) then
  134.     let img = System.Drawing.Image.FromFile(file)
  135.     let pio =
  136.       new PromptIntegerOptions
  137.         ("\nEnter number of horizontal pixels: ")
  138.     pio.AllowNone <- true
  139.     pio.UseDefaultValue <- true
  140.     pio.LowerLimit <- 1
  141.     pio.UpperLimit <- img.Width
  142.     pio.DefaultValue <- 100
  143.     let pir = ed.GetInteger(pio)
  144.     let xsize =
  145.       match pir.Status with
  146.         | PromptStatus.None ->
  147.             img.Width
  148.         | PromptStatus.OK ->
  149.             pir.Value
  150.         | _ -> -1
  151.     if xsize > 0 then
  152.       // Calculate the vertical size from the horizontal
  153.       let ysize = img.Height * xsize / img.Width
  154.       if ysize > 0 then
  155.         // We'll time the command, so we can check the
  156.         // sync vs. async efficiency
  157.         let starttime = System.DateTime.Now
  158.         // "use" has the same effect as "using" in C#
  159.         use tr =
  160.           db.TransactionManager.StartTransaction()
  161.         // Get appropriately-typed BlockTable and BTRs
  162.         let bt =
  163.           tr.GetObject
  164.             (db.BlockTableId,OpenMode.ForRead)
  165.           :?> BlockTable
  166.         let ms =
  167.           tr.GetObject
  168.             (bt.[BlockTableRecord.ModelSpace],
  169.             OpenMode.ForWrite)
  170.           :?> BlockTableRecord
  171.         // Function to create a filled circle (hatch) at a
  172.         // specific location
  173.         // Note the valid use of tr and ms, as they are in scope
  174.         let createCircle pt rad =
  175.           let hat = new Hatch()
  176.           hat.SetDatabaseDefaults()
  177.           hat.SetHatchPattern
  178.             (HatchPatternType.PreDefined,
  179.             "SOLID")
  180.           let id = ms.AppendEntity(hat)
  181.           tr.AddNewlyCreatedDBObject(hat, true)
  182.           // Now we create the loop, which we make db-resident
  183.           // (appending a transient loop caused problems, so
  184.           // we're going to use the circle and then erase it)
  185.           let cir = new Circle()
  186.           cir.Radius <- rad
  187.           cir.Center <- pt
  188.           let lid = ms.AppendEntity(cir)
  189.           tr.AddNewlyCreatedDBObject(cir, true)
  190.           // Have the hatch use the loop we created
  191.           let loops = new ObjectIdCollection()
  192.           loops.Add(lid) |> ignore
  193.           hat.AppendLoop(HatchLoopTypes.Default, loops)
  194.           hat.EvaluateHatch(true)
  195.           // Now we erase the loop
  196.           cir.Erase()
  197.           id
  198.         // Function to create our grid of circles
  199.         let createGrid xsize ysize rad offset =
  200.           let ids = new ObjectIdCollection()
  201.           for i = 0 to xsize - 1 do
  202.             for j = 0 to ysize - 1 do
  203.               let pt =
  204.                 new Point3d
  205.                   (offset * (Int32.to_float i),
  206.                    offset * (Int32.to_float j),
  207.                    0.0)
  208.               let id = createCircle pt rad
  209.               ids.Add(id) |> ignore
  210.           ids
  211.         // Function to change the colour of an entity
  212.         let changeColour (id : ObjectId) (col : Color) =
  213.           if id.IsValid then
  214.             let ent =
  215.               tr.GetObject(id, OpenMode.ForWrite) :?> Entity
  216.             ent.Color <- col
  217.         // Create our basic grid
  218.         let ids = createGrid xsize ysize 0.5 1.2
  219.         // Cast our image to a bitmap and then
  220.         // get the chunked pixels
  221.         let bmp = img :?> System.Drawing.Bitmap
  222.         let arr = pixelizeBitmap bmp xsize ysize
  223.         // Loop through the pixel list and average them out
  224.         // (which could be parallelized), using the results
  225.         // to change the colour of the circles in our grid
  226.         Array.map getAverageColour arr |>
  227.           Array.iter2 changeColour (getIdArray ids)
  228.         // Commit the transaction
  229.         tr.Commit()
  230.         // Check how long it took
  231.         let elapsed =
  232.           System.DateTime.op_Subtraction
  233.             (System.DateTime.Now, starttime)
  234.         ed.WriteMessage
  235.           ("\nElapsed time: " + elapsed.ToString())
To change this to run the colour averaging asynchronously (in parallel, if you have the cores) is really simple. We replace one line of code "Array.map getAverageColour arr" with the following:
Async.Run
  (Async.Parallel
    [ for a in arr ->
        async { return getAverageColour a }])
This essentially performs a parallel array map (albeit a somewhat naive one), returning basically the same results as the previous line - just hopefully a little more quickly. In case you want to build the two files into one project to test them side-by-side, here they are, the synchronous and asynchronous versions, with the changes needed to allow them to live in and execute from the same assembly.
Here's one more image that's been processed by the PIX command:

In case you're interested, the original image can be found here.

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋|手机版|CAD论坛|CAD教程|CAD下载|联系我们|关于明经|明经通道 ( 粤ICP备05003914号 )  
©2000-2023 明经通道 版权所有 本站代码,在未取得本站及作者授权的情况下,不得用于商业用途

GMT+8, 2024-4-19 23:25 , Processed in 0.259372 second(s), 23 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表