This post continues on from Part 1 of this series. You'll find much of this content has been used before in these previous posts, although post does include content updated for F# 1.9.6.2 (the September 2008 CTP).

The first thing we need to do is – as with any AutoCAD .NET project – add project references to AutoCAD’s managed assemblies, acmgd.dll and acdbmgd.dll. With F#’s integration into Visual Studio 2008 you do this in exactly the same way as you would for a C# or VB.NET project, by selecting Project -> Add Reference... from the pull-down menu or right-clicking the project inside the Solution Explorer and selecting Add Reference... from the context menu.

Here you then browse to the AutoCAD 2009 folder and filter for *mgd* files (at least this is the way I do it), and select the two we want:





Figure 10 – Adding project references to AutoCAD’s managed assemblies

Now we need to make sure AutoCAD recognizes a module within a namespace, from which it is able to load commands. I found – by using .NET Reflector – that the appropriate structure is to declare your functions as the contents of a module (this needs to come after the #light directive):

module MyNamespace.MyApplication

Next we’re going to specify the .NET namespaces we’ll be using inside this application:

open Autodesk.AutoCAD.Runtime open Autodesk.AutoCAD.ApplicationServices open Autodesk.AutoCAD.DatabaseServices

We’ll then skip past our definitions of words and sortedWords, and define our command function:

[<CommandMethod("Words")>] let listWords () = // Let's get the usual helpful AutoCAD objects let doc = Application.DocumentManager.MdiActiveDocument let ed = doc.Editor let db = doc.Database // "use" has the same effect as "using" in C# use tr = db.TransactionManager.StartTransaction(); // Get appropriately-typed BlockTable and BTRs let bt = tr.GetObject (db.BlockTableId,OpenMode.ForRead) :?> BlockTable let ms = tr.GetObject (bt.[BlockTableRecord.ModelSpace], OpenMode.ForRead) :?> BlockTableRecord let ps = tr.GetObject (bt.[BlockTableRecord.PaperSpace], OpenMode.ForRead) :?> BlockTableRecord

Most of this section should be familiar to anyone who has used the .NET API to AutoCAD – there are really only a couple of ideas that may need explanation:

The use keyword is just like C#’s using – but we don’t use curly braces to define scope. The scope gets defined as the remainder of the function in which the use statement has been used. Once the function completes the used object will be disposed of automatically. We’re using the dynamic cast operator (:?>) to specify the type of object we’re opening with GetObject(). This operator involves a query to check whether this is valid – if we wanted to do a static cast we could use :> instead.

Now we have opened our modelspace and paperspace objects (we could go further and open other layouts, but – once again – I’ll leave that as a follow-on exercise for those who feel the need to do it :-) we can look at the code we need to extract the text from our database-resident objects.

Let’s start by defining a local function which takes an ObjectId and uses it to open an object, and for a textual object (DBText or MText) it will return its contents:

// A function that accepts an ObjectId and returns // a list of the text contents, or an empty list. // Note the valid use of tr, as it is in scope let extractText x = let obj = tr.GetObject(x,OpenMode.ForRead) match obj with | :? MText as m -> m.Contents | :? DBText as d -> d.TextString | _ -> ""

Once again we haven’t specified the type of the argument – this will be inferred by the system – but we could very easily do so. We’re using the transaction previously started in the listWords function – the reason for defining extractText local to it – which is quite valid, as it’s in scope.

After opening the object for read from its ID we’re using pattern-matching – a technique that is a huge timesaver for functional programmers – to check on the type of the object and return the appropriate property of it. This is just like a much cleaner switch statement in C#.

We could choose to match against any property of the object, but in our case we want to check the type, so use this operator: :?. The as keyword is a syntactic shortcut that defines a value we can then use to easily dereference the object and get at its properties and methods.

The final clause of the three is a wildcard: it will match all object that are not DBText or MText objects and return an empty string.

Now that we can get at the contents of our text objects, let’s write a quick recursive function to display the contents of the final list of words inside AutoCAD:

// A recursive function to print the contents of a list let rec printList x = match x with | h :: t -> ed.WriteMessage("

" + h); printList t | [] -> ed.WriteMessage("

")

Once again, this is a local function, so using our Editor (accessed via the ed value) is quite valid. We’re using pattern-matching again to create a recursive function (indicated via the rec keyword and then the recursive call to printList). When we find an empty list ([]) we simply print a newline, but when we find a list with a head (h) and a tail (t – which may well end up being empty, by the way, we’ll find out the next time we recurse into printList), we print the head and recurse with the tail.

One thing to look out for when defining recursive functions: they really need to be defined as tail-recursive, which means that the recursive call should be the last operation. This allows the compiler to perform tail call optimization, which replaces the declared recursion with a simply while loop inside the generated code.

Why does this matter? Well, calling a function does have some overhead, as stack space is required to store information about the function and its arguments, so if we have a list of 10,000 words to print and the function hasn’t been optimized, the recursion could cause problems. (The number could be 100,000 or 1,000,000, but the point is there is a number).

The above code does get optimized properly (even if the pattern for the empty list comes after the recursive call – it’s really about the position of the recursive call in the clause that recurses, rather than the overall program), and this is easy to check with .NET Reflector. In fact there’s an article on my blog covering just this:

http://through-the-interface.typepad.com/through_the_interface/2008/02/using-reflector.html

Seq.to_list (Seq.cast ms) @ // Create a list of modelspace ids, Seq.to_list (Seq.cast ps) |> // appending those from paperspace List.map extractText |> // Extract the text from each object sortedWords |> // Get a sorted, canonical list of words printList // Print the resultant words

A couple of comments on the above plumbing: ms and ps are both IEnumerable types (which correspond to the Seq class in F#), but are both untyped. This means we have to cast them, to be able to access them properly from F#, and then we can simply call Seq.to_list to get the contents into a list. The @ operator appends the list of ObjectIds of objects in modelspace with those in paperspace, and we then pipe the list into a call to List.map which runs our extractText function on all the objects in the combined list. The results get piped into our sortedWords function, and we finally print them to the command-line using our recursive printList function.

Finally, we’re just going to call commit on our transaction object, as for performance reasons this is currently best practice:

// As usual, committing is cheaper than aborting tr.Commit()

That’s it for our first AutoCAD application. Let’s see the entire listing:

#light // Declare a specific namespace and module name module MyNamespace.MyApplication open Autodesk.AutoCAD.Runtime open Autodesk.AutoCAD.ApplicationServices open Autodesk.AutoCAD.DatabaseServices // Partial application of split which can then be // applied to a string to retrieve the contained words let words = let seps = " \t~`!@#$%^&*()-=_+{}|[]\\;':\"<>?,./" seps.ToCharArray() |> Array.to_list |> String.split let sortedWords x = List.map words x |> // Get the words from each string List.concat |> // No need for the outer list Set.of_list |> // Create a set from the list Set.to_list // Create a list from the set // Now we define our command [<CommandMethod("Words")>] let listWords () = // Let's get the usual helpful AutoCAD objects let doc = Application.DocumentManager.MdiActiveDocument let ed = doc.Editor let db = doc.Database // "use" has the same effect as "using" in C# use tr = db.TransactionManager.StartTransaction(); // Get appropriately-typed BlockTable and BTRs let bt = tr.GetObject (db.BlockTableId,OpenMode.ForRead) :?> BlockTable let ms = tr.GetObject (bt.[BlockTableRecord.ModelSpace], OpenMode.ForRead) :?> BlockTableRecord let ps = tr.GetObject (bt.[BlockTableRecord.PaperSpace], OpenMode.ForRead) :?> BlockTableRecord // Now the fun starts... // A function that accepts an ObjectId and returns // a list of the text contents, or an empty list. // Note the valid use of tr, as it is in scope let extractText x = let obj = tr.GetObject(x,OpenMode.ForRead) match obj with | :? MText as m -> m.Contents | :? DBText as d -> d.TextString | _ -> "" // A recursive function to print the contents of a list let rec printList x = match x with | h :: t -> ed.WriteMessage("

" + h); printList t | [] -> ed.WriteMessage("

") // And here's where we plug everything together... Seq.to_list (Seq.cast ms) @ // Create a list of modelspace ids, Seq.to_list (Seq.cast ps) |> // appending those from paperspace List.map extractText |> // Extract the text from each object sortedWords |> // Get a sorted, canonical list of words printList // Print the resultant words // As usual, committing is cheaper than aborting tr.Commit()

Introducing parallel processing in AutoCAD via F# Asynchronous Workflows

As mentioned previously, pure functional code lends itself to be run on multiple computing cores in parallel. While the tools aren’t yet there to make this happen automatically – via implicit parallelization – this is a likely outcome, over the coming years. For now we have the possibility of writing code that uses explicit parallelization – where we specify the tasks we know can be executed at the same time and leave the language and runtime to take care of the coordination.

There are a couple of ways to do this, right now: the Parallel Extensions to .NET (also in CTP stage and due for inclusion in Visual Studio 2010) provide a number of parallel constructs, such as parallel versions of for and while loops. F# currently provides the capability to define and execute Asynchronous Workflows, which is what we’re going to look at now.

First, let’s take a look at a sample application that we’re going to parallelize. This sample goes through and queries, via RSS, the latest posts on a number of different blogs. It then generates AutoCAD geometry – text with a hyperlink – for each of these posts. So we turn AutoCAD into an RSS reader, for all intents and purposes.

1 // Use lightweight F# syntax 2 3 #light 4 5 // Declare a specific namespace and module name 6 7 module MyNamespace.MyApplication 8 9 // Import managed assemblies 10 11 open Autodesk.AutoCAD.Runtime 12 open Autodesk.AutoCAD.ApplicationServices 13 open Autodesk.AutoCAD.DatabaseServices 14 open Autodesk.AutoCAD.Geometry 15 open System.Xml 16 open System.IO 17 open System.Net 18 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 22 let feeds = 23 [ ("Through the Interface", 24 "http://blogs.autodesk.com/through-the-interface", 25 "http://through-the-interface.typepad.com/through_the_interface/atom.xml"); 26 27 ("Don Syme's F# blog", 28 "http://blogs.msdn.com/dsyme/", 29 "http://blogs.msdn.com/dsyme/rss.xml"); 30 31 ("Shaan Hurley's Between the Lines", 32 "http://autodesk.blogs.com/between_the_lines", 33 "http://autodesk.blogs.com/between_the_lines/rss.xml"); 34 35 ("Scott Sheppard's It's Alive in the Lab", 36 "http://blogs.autodesk.com/labs", 37 "http://labs.blogs.com/its_alive_in_the_lab/rss.xml"); 38 39 ("Volker Joseph's Beyond the Paper", 40 "http://blogs.autodesk.com/beyond_the_paper", 41 "http://dwf.blogs.com/beyond_the_paper/atom.xml") ] 42 43 // Fetch the contents of a web page, synchronously 44 45 let httpSync (url:string) = 46 let req = WebRequest.Create(url) 47 use resp = req.GetResponse() 48 use stream = resp.GetResponseStream() 49 use reader = new StreamReader(stream) 50 reader.ReadToEnd() 51 52 // Load an RSS feed's contents into an XML document object 53 // and use it to extract the titles and their links 54 // Hopefully these always match (this could be coded more 55 // defensively) 56 57 let titlesAndLinks (name, url, xml) = 58 try 59 let xdoc = new XmlDocument() 60 xdoc.LoadXml(xml) 61 62 let titles = 63 [ for n in xdoc.SelectNodes("//*[name()='title']") 64 -> n.InnerText ] 65 let links = 66 [ for n in xdoc.SelectNodes("//*[name()='link']") -> 67 let inn = n.InnerText 68 if inn.Length > 0 then 69 inn 70 else 71 let href = n.Attributes.GetNamedItem("href").Value 72 let rel = n.Attributes.GetNamedItem("rel").Value 73 if href.Contains("feedburner") or rel.Contains("enclosure") then 74 "" 75 else 76 href ] 77 78 let descs = 79 [ for n in xdoc.SelectNodes 80 ("//*[name()='description' or name()='subtitle' or name()='summary']") 81 -> n.InnerText ] 82 83 // A local function to filter out duplicate entries in 84 // a list, maintaining their current order. 85 // Another way would be to use: 86 // Set.of_list lst |> Set.to_list 87 // but that results in a sorted (probably reordered) list. 88 89 let rec nub lst = 90 match lst with 91 | a::[] -> [a] 92 | a::b -> 93 if a = List.hd b then 94 nub b 95 else 96 a::nub b 97 | [] -> [] 98 99 // Filter the links to get (hopefully) the same number 100 // and order as the titles and descriptions 101 102 let real = List.filter (fun (x:string) -> x.Length > 0) 103 let lnks = real links |> nub 104 105 // Return a link to the overall blog, if we don't have 106 // the same numbers of titles, links and descriptions 107 108 let lnum = List.length lnks 109 let tnum = List.length titles 110 let dnum = List.length descs 111 112 if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then 113 [(name,url,url)] 114 else 115 List.zip3 titles lnks descs 116 with _ -> [] 117 118 // For a particular (name,url) pair, 119 // create an AutoCAD HyperLink object 120 121 let hyperlink (name,url,desc) = 122 let hl = new HyperLink() 123 hl.Name <- url 124 hl.Description <- desc 125 (name, hl) 126 127 // Download an RSS feed and return AutoCAD HyperLinks for its posts 128 129 let hyperlinksSync (name, url, feed) = 130 let xml = httpSync feed 131 let tl = titlesAndLinks (name, url, xml) 132 List.map hyperlink tl 133 134 // Now we declare our command 135 136 [<CommandMethod("rss")>] 137 let createHyperlinksFromRss() = 138 139 let starttime = System.DateTime.Now 140 141 // Let's get the usual helpful AutoCAD objects 142 143 let doc = 144 Application.DocumentManager.MdiActiveDocument 145 let ed = doc.Editor 146 let db = doc.Database 147 148 // "use" has the same effect as "using" in C# 149 150 use tr = 151 db.TransactionManager.StartTransaction() 152 153 // Get appropriately-typed BlockTable and BTRs 154 155 let bt = 156 tr.GetObject 157 (db.BlockTableId,OpenMode.ForRead) 158 :?> BlockTable 159 let ms = 160 tr.GetObject 161 (bt.[BlockTableRecord.ModelSpace], 162 OpenMode.ForWrite) 163 :?> BlockTableRecord 164 165 // Add text objects linking to the provided list of 166 // HyperLinks, starting at the specified location 167 168 // Note the valid use of tr and ms, as they are in scope 169 170 let addTextObjects (pt : Point3d) lst = 171 // Use a for loop, as we care about the index to 172 // position the various text items 173 174 let len = List.length lst 175 for index = 0 to len - 1 do 176 let txt = new DBText() 177 let (name:string,hl:HyperLink) = List.nth lst index 178 txt.TextString <- name 179 let offset = 180 if index = 0 then 181 0.0 182 else 183 1.0 184 185 // This is where you can adjust: 186 // the initial outdent (x value) 187 // and the line spacing (y value) 188 189 let vec = 190 new Vector3d 191 (1.0 * offset, 192 -0.5 * (Int32.to_float index), 193 0.0) 194 let pt2 = pt + vec 195 txt.Position <- pt2 196 ms.AppendEntity(txt) |> ignore 197 tr.AddNewlyCreatedDBObject(txt,true) 198 txt.Hyperlinks.Add(hl) |> ignore 199 200 // Here's where we use the varous functions 201 // we've defined 202 203 let links = 204 List.map hyperlinksSync feeds 205 206 // Add the resulting objects to the model-space 207 208 let len = List.length links 209 for index = 0 to len - 1 do 210 211 // This is where you can adjust: 212 // the column spacing (x value) 213 // the vertical offset from origin (y axis) 214 215 let pt = 216 new Point3d 217 (15.0 * (Int32.to_float index), 218 30.0, 219 0.0) 220 addTextObjects pt (List.nth links index) 221 222 tr.Commit() 223 224 let elapsed = 225 System.DateTime.op_Subtraction(System.DateTime.Now, starttime) 226 227 ed.WriteMessage("

Elapsed time: " + elapsed.ToString())

I have numbered the lines, to make it easier for us to talk about the changes that are needed to introduce parallelism into this sample. Both synchronous and asynchronous versions of this application are available on my blog.

I won’t go through the above code in detail, here: firstly, it’s not intended as a perfect implementation of an RSS consumer – there are too many variations in the way RSS is implemented by different sites, so I know for a fact that this code will not work for certain blogs – it’s really intended to be an example of a – potentially time-consuming – asynchronous (in this case network-based) activity that is easy to run in parallel.

A word of caution: AutoCAD is not thread-safe – it is very much a single-threaded application – so we need to coordinate the results of these tasks prior to making the changes to the AutoCAD database. Luckily F# makes this very easy for us to do, so that’s really not a problem.

Here is the updated source that makes use of Asynchronous Workflows, with the modified/new lines highlighted in red (with a grey background for those reading this in black & white :-):

1 // Use lightweight F# syntax 2 3 #light 4 5 // Declare a specific namespace and module name 6 7 module MyNamespace.MyApplicationAsync 8 9 // Import managed assemblies 10 11 open Autodesk.AutoCAD.Runtime 12 open Autodesk.AutoCAD.ApplicationServices 13 open Autodesk.AutoCAD.DatabaseServices 14 open Autodesk.AutoCAD.Geometry 15 open System.Xml 16 open System.IO 17 open System.Net 18 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 22 let feeds = 23 [ ("Through the Interface", 24 "http://blogs.autodesk.com/through-the-interface", 25 "http://through-the-interface.typepad.com/through_the_interface/atom.xml"); 26 27 ("Don Syme's F# blog", 28 "http://blogs.msdn.com/dsyme/", 29 "http://blogs.msdn.com/dsyme/rss.xml"); 30 31 ("Shaan Hurley's Between the Lines", 32 "http://autodesk.blogs.com/between_the_lines", 33 "http://autodesk.blogs.com/between_the_lines/rss.xml"); 34 35 ("Scott Sheppard's It's Alive in the Lab", 36 "http://blogs.autodesk.com/labs", 37 "http://labs.blogs.com/its_alive_in_the_lab/rss.xml"); 38 39 ("Volker Joseph's Beyond the Paper", 40 "http://blogs.autodesk.com/beyond_the_paper", 41 "http://dwf.blogs.com/beyond_the_paper/atom.xml") ] 42 43 // Fetch the contents of a web page, asynchronously 44 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 52 // Load an RSS feed's contents into an XML document object 53 // and use it to extract the titles and their links 54 // Hopefully these always match (this could be coded more 55 // defensively) 56 57 let titlesAndLinks (name, url, xml) = 58 try 59 let xdoc = new XmlDocument() 60 xdoc.LoadXml(xml) 61 62 let titles = 63 [ for n in xdoc.SelectNodes("//*[name()='title']") 64 -> n.InnerText ] 65 let links = 66 [ for n in xdoc.SelectNodes("//*[name()='link']") -> 67 let inn = n.InnerText 68 if inn.Length > 0 then 69 inn 70 else 71 let href = n.Attributes.GetNamedItem("href").Value 72 let rel = n.Attributes.GetNamedItem("rel").Value 73 if href.Contains("feedburner") or rel.Contains("enclosure") then 74 "" 75 else 76 href ] 77 78 let descs = 79 [ for n in xdoc.SelectNodes 80 ("//*[name()='description' or name()='subtitle' or name()='summary']") 81 -> n.InnerText ] 82 83 // A local function to filter out duplicate entries in 84 // a list, maintaining their current order. 85 // Another way would be to use: 86 // Set.of_list lst |> Set.to_list 87 // but that results in a sorted (probably reordered) list. 88 89 let rec nub lst = 90 match lst with 91 | a::[] -> [a] 92 | a::b -> 93 if a = List.hd b then 94 nub b 95 else 96 a::nub b 97 | [] -> [] 98 99 // Filter the links to get (hopefully) the same number 100 // and order as the titles and descriptions 101 102 let real = List.filter (fun (x:string) -> x.Length > 0) 103 let lnks = real links |> nub 104 105 // Return a link to the overall blog, if we don't have 106 // the same numbers of titles, links and descriptions 107 108 let lnum = List.length lnks 109 let tnum = List.length titles 110 let dnum = List.length descs 111 112 if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then 113 [(name,url,url)] 114 else 115 List.zip3 titles lnks descs 116 with _ -> [] 117 118 // For a particular (name,url) pair, 119 // create an AutoCAD HyperLink object 120 121 let hyperlink (name,url,desc) = 122 let hl = new HyperLink() 123 hl.Name <- url 124 hl.Description <- desc 125 (name, hl) 126 127 // Use asynchronous workflows in F# to download 128 // an RSS feed and return AutoCAD HyperLinks 129 // corresponding to its posts 130 131 let hyperlinksAsync (name, url, feed) = 132 async { let! xml = httpAsync feed 133 let tl = titlesAndLinks (name, url, xml) 134 return List.map hyperlink tl } 135 136 // Now we declare our command 137 138 [<CommandMethod("arss")>] 139 let createHyperlinksFromRssAsync() = 140 141 let starttime = System.DateTime.Now 142 143 // Let's get the usual helpful AutoCAD objects 144 145 let doc = 146 Application.DocumentManager.MdiActiveDocument 147 let ed = doc.Editor 148 let db = doc.Database 149 150 // "use" has the same effect as "using" in C# 151 152 use tr = 153 db.TransactionManager.StartTransaction() 154 155 // Get appropriately-typed BlockTable and BTRs 156 157 let bt = 158 tr.GetObject 159 (db.BlockTableId,OpenMode.ForRead) 160 :?> BlockTable 161 let ms = 162 tr.GetObject 163 (bt.[BlockTableRecord.ModelSpace], 164 OpenMode.ForWrite) 165 :?> BlockTableRecord 166 167 // Add text objects linking to the provided list of 168 // HyperLinks, starting at the specified location 169 170 // Note the valid use of tr and ms, as they are in scope 171 172 let addTextObjects (pt : Point3d) lst = 173 // Use a for loop, as we care about the index to 174 // position the various text items 175 176 let len = List.length lst 177 for index = 0 to len - 1 do 178 let txt = new DBText() 179 let (name:string,hl:HyperLink) = List.nth lst index 180 txt.TextString <- name 181 let offset = 182 if index = 0 then 183 0.0 184 else 185 1.0 186 187 // This is where you can adjust: 188 // the initial outdent (x value) 189 // and the line spacing (y value) 190 191 let vec = 192 new Vector3d 193 (1.0 * offset, 194 -0.5 * (Int32.to_float index), 195 0.0) 196 let pt2 = pt + vec 197 txt.Position <- pt2 198 ms.AppendEntity(txt) |> ignore 199 tr.AddNewlyCreatedDBObject(txt,true) 200 txt.Hyperlinks.Add(hl) |> ignore 201 202 // Here's where we do the real work, by firing 203 // off - and coordinating - asynchronous tasks 204 // to create HyperLink objects for all our posts 205 206 let links = 207 Async.Run 208 (Async.Parallel 209 [ for (name,url,feed) in feeds -> 210 hyperlinksAsync (name,url,feed) ]) 211 |> Array.to_list 212 213 // Add the resulting objects to the model-space 214 215 let len = List.length links 216 for index = 0 to len - 1 do 217 218 // This is where you can adjust: 219 // the column spacing (x value) 220 // the vertical offset from origin (y axis) 221 222 let pt = 223 new Point3d 224 (15.0 * (Int32.to_float index), 225 30.0, 226 0.0) 227 addTextObjects pt (List.nth links index) 228 229 tr.Commit() 230 231 let elapsed = 232 System.DateTime.op_Subtraction(System.DateTime.Now, starttime) 233 234 ed.WriteMessage("

Elapsed time: " + elapsed.ToString())

Let's look at the specific changes:

Line 7 has been changed to allow both files to be part of the same project.

Lines 45-50 implement a new, asynchronous function to download content from a URL. The async primitive coordinates a set of activities, while the let! and use! statements indicate that these right-hand side of the operation will be run asynchronously and that the results should be bound to the left. So here we're only getting the HTTP content asynchronously - the reading is to occur synchronously.

Lines 131-134 implement an asynchronous task that not only calls our asynchronous HTTP request function but coordinates the creation of AutoCAD geometry based on the contents received.

Lines 207-211 are where we make use of these newly-defined functions by firing them off in parallel (the framework will use the processing capabilities available to it to execute the tasks as efficiently as possible) and coordinating the results into a single array, which we convert to a list to maintain our previous processing code.

When we run either the RSS or ARSS (its asynchronous version), we should see this kind of result:

Figure 11 – AutoCAD geometry created from our RSS feeds

Now let’s see how they compare in terms of performance. I executed the RSS and ARSS commands a number of times in sequence to get a feel for relative performance.

Command: rss Elapsed time: 00:00:08.1958195 Command: arss Elapsed time: 00:00:02.2802280 Command: rss Elapsed time: 00:00:04.1264126 Command: arss Elapsed time: 00:00:03.6343634 Command: rss Elapsed time: 00:00:03.6563656 Command: arss Elapsed time: 00:00:01.9891989 Command: rss Elapsed time: 00:00:03.1673167 Command: arss Elapsed time: 00:00:03.1223122 Command: rss Elapsed time: 00:00:05.7375737 Command: arss Elapsed time: 00:00:01.9391939

The first execution time is much higher due to an initial startup penalty or the need to fill some page cache with the content. On average, though, the asynchronous code runs in 60-70% of the time needed by the synchronous version. The code was run a dual-core notebook: some of the performance will be related to using both cores, but most will be due to the parallelization of asynchronous tasks that have some latency due to use of the network. With more accesses in parallel you would see this performance difference become increasingly exaggerated.