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("\n" + h); printList t
| [] -> ed.WriteMessage("\n")
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("\n" + h); printList t
| [] -> ed.WriteMessage("\n")
// 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("\nElapsed 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("\nElapsed 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.