I hope you're ready for this. This is not only the most exciting part, but
at the same time also the most difficult part. So, in one way, we can be happy that we learned
something that is really, really interesting. But, on the other hand, it might be a little
bit tedious because these things don't become clear right away, especially if you're not
used to writing asynchronous code with multiple threads and are still taking your first steps
in those kinds of fields. I myself have not written quit
e a lot of code
in that department, so that's the reason why I literally sent the code snippet to the engineer
that has conceived TMS XData and said, 'Hey, is this approach for multi-threading the correct
approach? Is this something you would implement yourself?' And he agrees. In this case, that's how he would tackle it. The reason why it is so difficult will become
clear right away as soon as you look at the code, how it needs to be done according to
TMS FNC Maps. Just let me create another fo
rm in here. Form, and for whatever reason, this is the
best-hidden. The form nowadays is the best-hidden component
here. I think it's on Windows. And then I have to scroll down. There we go, VCL form. Let's drop a FNC Directions. Okay, poof, right here. And I have events here. For example, the getDirections, onGetDirections,
onGetDirectionsResult. That's the key here. This event is triggered when direction results
are retrieved. And that should ring a bell in your head because
if I implement som
ething here, let's call this component 'directions.' Directions. And I go here, directions.getDirections. Right here. I would have to call this. I can specify an origin, a destination, and
a callback. Or I don't specify it. I'll leave it nil. And then the component will call this event
right here. And that's exactly the difficulty. Imagine that you write a method in your web
service that calls getDirections, and then it has to wait until the result from the callback
is retrieved. So at this poin
t, if we look at the code,
we have, I mean, Wagner, the engineer that wrote XData, Wagner Longgraph, and I basically
said there are two real options on how to implement this. And we decided for one of them, obviously. Maybe you come up with more, but those are
the two that made the most sense to us. But looking at the implementation, we have
the method getDrivingDirections. We get an origin and a destination as a string,
and we need to return a route. That's what we need to use. By default, our
result is nil, meaning we
don't have anything from Google yet. That is literally something that can happen
if you specify, 'How can I go from Miami, Florida, to Seattle, Washington?' You might not get a reply in a certain amount
of time. That is something you also have to remember. You're working with a Google web service on
the backend of your backend, so to speak, and that might not deliver a reply. So nil is definitely a viable response for
this thing. In order to identify the request that we
sent
to Google, we need to generate some sort of ID. And in this case, I simply generated a UID,
a unique ID. That's, to me, the most the easiest approach,
so to speak. We do need this because there is no guarantee
that getDrivingDirections is not called multiple times at the same time because multiple people
can use your web application at the same time, and people can issue requests to your web
service at the same time. That means there are parallel runs of this
getDrivingDirections method. T
hat is just something that might happen. So you need to be able to distinguish later
on which request you're dealing with. And this is not only the instance if you look
at the multiple threads; this is also something that is built into the getDirections because
there, the third parameter actually right here, this origin is the first, destination
is the second, the callback is the third, the fourth is the ID of the request. So you can identify it where, well, if your
callback gets called here, th
is is your callback right here with the request and the request
result, there you also can read the ID. So you know which request it is. So if you have later on multiple requests
at the same time, each request to Google, those are the requests we're assigning an
ID to have a unique ID assigned to them. Everything separates into two tasks. The first task is to retrieve the information
from Google. That's task number one. Task number two is to wait for Google to deliver
this result, or if Google d
oes not deliver this result in a certain time, return 404
not found. That's the option we have here. Alternatively, you could also just shoot this
getDirections request off and then instead of using some memory data structure to store
the directions, you could also store this in a database or in a memory storage that
is housed inside of your DirectionsManager class. But that would require you just to be very
direct that this manager is always the same for all the different requests that you do,
that you do not use separate classes. And then you could have in your DirectionsManager
a data structure that holds all the direction requests and routes that have been requested. But again, you see I'm going for the approach
that is the least amount of work and is the easiest to understand at first. That's why we're not doing it. I'm not saying this is the most performant
one, but this is one that works and is straightforward and is also, if you want to believe Wagner
and me, the one that is re
liable. There's a lot of trust here, right? But in all seriousness, this has not been
conceived in 5 minutes, 10 minutes, 20 minutes, 30 minutes. This is something where I sat down and built
the structure of this method and thought, 'What could I use?' Because these tasks, of course, are a library
that is bundled with Delphi, now the parallel library from Embarcadero. And after that, I did the thing. And this is the exact point where it gets
a little bit tricky. Let's say you have this task to r
etrieve,
and you create this task, and this task does nothing else but—don't let yourself get
confused with the syntax. We just pass in a procedure inside of the
parameter of the constructor of the task. So the task will execute this procedure when
started. That's the content of it. And inside of the procedure, we call FDirections.getDirections. That is the component. Where does FDirections come from? It's defined in the class here. FDirections is the component TtmsFNCDirections. That's the comp
onent that I've been dropping
on the form each time, and I initialize it as follows: TtmsFNCDirections.Create(nil). In this case, again, I want to free it myself. And then FDirections.APIKey := TAPIKey. TAPIKey is Google. I use my Google API key coming from this unit
right here. And I am very much aware that I'm not showing
it here. And then there might be people looking at
the source code associated with this video and saying, 'Huh, he has the key in the history. He made this public.' And be as
sured, this key no longer exists
that I used. That I checked into the source repository
for my tests. I removed that key. I already replaced it with a different key. So even if I were to show it, I'm just not
showing it because I don't know how YouTube analyzes these videos, and it's like, 'Oh,
there's secret information in there.' So I'm not even going to show it. I'm saving myself some time to blur it. So that returns a string with the key, and
the service is Google. So with that, the directio
n service is initialized. It's the same thing as if you drop it on the
form and assign the properties. So this directions component, we call with
getDirections, the origin, the destination. They, of course, come from the parameters
of the method. And then, and this is where my head was completely
screwed the first time I looked at this, of course, that is a callback. So we call getDirections and tell Delphi,
'Call this method with these parameters and with this callback done.' So Delphi immediat
ely executes the next line. And that is the end of this procedure. So the task will be terminated and done with. So if we were to say LTaskRetrieve.Wait, basically
if we were to wait for this, it would finish right away. And it would be gone. LTaskRetrieve would be gone before the callback
can even be called. So it would be done. Google would most likely process the result
and then send it back on. There's nothing there anymore. If you're concerned about memory leaks, don't
be because it's an IT
ask. So as soon as it goes away, Delphi will also
handle the memory management. So there's nothing to be concerned about in
that regard. It's just not going to work. Okay, there's, uh, you wait for something
that concludes right away. So at that point, when you get after the waiting,
you still don't have the result from the web service. So what we need to do is we need a task to
wait. And this is, as I said, this is the one where
the web service really, really is going to return a result. Altern
atively, you could say, 'Start, start
this. Return nothing. Return the ID of this request and then build
a second endpoint into your web service where you say, 'Get the data for this request.' Of course, you don't know how long this takes. You can ask, 'How long are you going to wait? 1 second, 2 seconds, 3 seconds?' And at that point, you need some sort of mechanism
on the client where you ask for the same thing multiple times as long as the result is, 'Don't
have an answer yet.' And then the a
nswer is either could be retrieved,
could not be retrieved. And if it could be retrieved, you automatically
return the route as well. And if it could not be retrieved, that's also
an answer for the web client. However, that implies that your web client
needs to issue requests with a timer or something on a regular basis. I don't like that because we're looking here
at something of a maximum of 67-78 seconds to be retrieved. So it doesn't make any sense for me to build
a client that polls on a re
gular basis. Do you have an answer yet? Do you have an answer yet? Do you have an answer yet? If you can implement it like this without
doing that, basically this service is easier to consume than something like start the process
and then retrieve the process. One example for a service that would benefit
from having started off and then basically get regular updates is, for example, imagine
you have an accounting system and you do payroll. And you have to generate the PDF documents
for all your
employees as PDF documents. And then you have a web service which downloads
a zip file of all those PDF documents. Just assume. Obviously, to create all those PDF files if
you work in a company with 5,000 employees, it's not going to be 5, 10, 20 seconds. It's longer, most likely. And that means it does not make sense for
the web server to wait in order to return the result right away. So instead, you would basically trigger the
generation of the reports and then have a second endpoint that woul
d then basically
ask which reports are ready or retrieve these reports. You would have a completely different interface
to communicate with your web service than this. Okay, but we still don't have a result from
the web service, right? We just called getDirections and we haven't
looked at the callback yet. We also need a data structure that allows
me to transfer data from this callback to here, to the outside world, basically. So what we do, we don't declare something
locally. We say, 'FRoots is
a dictionary of roots identified
by ID,' as you can see it right here. So we have a dictionary. The string is the ID of the route, of the
request, and then we have a route associated with it. So going back here, we get the directions
for an origin, a destination, and please call this callback with a request and a request
result. If you wonder how I get these wonderful fingerprints
right here, I don't remember these. This is not something I can store . I click on getDirections, say three hail
ma
rys, you know, that DOI will find it. And in this case, it does. And that's how I get these things because
then I can browse in the parameter list right here, a callback, click on the link, and here
we go. TFnCDirections getDirections callback, reference
to. And then here is my fingerprint. Of course, this is what I mentioned right
at the beginning of this tutorial series. If you write your code for multiple platforms,
you have these ifdefs everywhere. So that is the downside. That's why I opted
for client-server different. But here you see, this is exactly the fingerprint
that I used. So going back here, we get back the request
information and the request result, like status code, request result indicates if it's successful. Just be careful. A successful request can also mean that no
route exists. The request was successful. Like if I ask for route from Florida to Dortmund,
Germany, it's going to be successful. But the answer is there is no route. So we have to check a request and loo
k at
the items inside of request. This time, a request, not a request result. A request. Items.Count. And if that's bigger than zero, we have more
than one route. In this case, I only look at one route. I wanted to keep it simple. Obviously, I could loop and not only add one. I could create as many as are returned because
with Google, you can also ask for alternatives. But by default, here, if you look at getDirections
and go way back into the parameter list, there's something like here, alterna
tives, boolean. And that's by default false. So it only gives you one. And by default, it is the fastest, I think. So if you were to flip on alternatives to
true, you get one that is the fastest, the shortest, and these kinds of things, with
toll, without toll. You also can basically say avoid tolls to
true, and these kinds of things. That's all configurable. This is just a minimal solution I'm presenting
here. There's so much more to discover. And if you're interested to discover, think
about t
he fact that there is a book, TMS Maps, from me. In the end, I'm going to present all the learning
content that is available as well in addition to this tutorial. So if we have the item, or more than zero,
and take the first one—I love inline variables here. I mean, you will see in the web client when
we look at that how much easier and better the code becomes to read if you have this
compared to not having it. I mean, imagine having to go, 'Where do you
go now? Where do you declare a route?' We
ll, you have to declare it here because
it's not a variable declaration for here either, right? Is it? It's like we have one function inside the
function or procedure and another procedure. So having inline variables here makes the
code more readable and is really just a pointed to the first route in this case. We also create a route of our data model,
route, TRoute. And then for each step—and I named these
Google steps because these are the steps inside of a route. So Google step is actually th
e data type that
we get back from Google, right here. So we have all these funny things, coordinates
with TMS FNC Maps coordinates, distance, durations. It's the same, but these are the TMS Map specific
data types. So what I do now—and this is again, I said
like think about the data model first. I say, 'LStep, TStep, create.' And then I can say, 'LStep, assign from a
Google step.' Boom, initialized. Data model first. And then I say, 'LRoot, add step.' Yes, I created that for a reason so we can
a
dd the step easily. And now the biggie, FRoots, add a request
ID, a route. That's pretty straightforward. We want to, with the ID, and this in this
case, we can no longer use it from here, right? Because we need the request that has been
returned, and that is here in a request. It's returned to us as a parameter. And in LRoot is our TRoute. That's the one that we want to put on this
dictionary with this key. Perfect. However, it might happen at the same time
in a different thread. And remember,
if you write to the same memory
address from different threads at the same time, it's going to blow up. So what do you do? By the way, I'm not going to even try to give
any real-life analogies. Some people do that. I'm not. I'm just going to say never access the same
memory space in different threads at the same time without making certain that you have
a semaphore in place. So what we do is we call TMonitor.Enter, and
we pass as the tag FRRoots, which is available in all the threads because it'
s like a field
in the class that we use. So we lock it out. And what we do is we make sure that, as soon
as we enter the semaphore, we add it to the dictionary. Then we make sure that the semaphore is being
left with TMonitor.Exit. Very important, which makes certain that the
two requests do not access the same data structure at the same time. Obviously, at this point, our information
as it is in FRoots is also available anywhere in TDirectionsManager. Right? So what we need to do now is to have
a second
task which waits for the ID to be added to the dictionary and then return it to the client. And that's what ALaskWait does. To make things even more complicated, I created
a future. A future is a function that might return a
value at some point in the future. Okay? And the type is of TRoute. So we have the fingerprint function CRoute,
and this looks like in the code completion, looks like consun tun, right? And it's like one parameter and that is being
returned. So the function has no
parameters. So the function has no parameters, but it
returns a type TRoute, one of them. That's basically what's in there. Okay? So the function returns TRoute. I just explained it. That's again an anonymous function passed
as a parameter just like with the other task. And what I do is I want to wait a maximum
of 10 seconds. After 10 seconds, I want to bow out and say,
'Hey, we don't have a route.' Okay? That's the thing. So I'm going to try 20 times. So only if LStar smaller than 20, and each
iteration, I sleep for 500 milliseconds, which is half a second. And LRoot as only as long as we don't have
a root, right? That's why I initialize it here with nil. And as soon as we have a root, this loop is
left. The while condition is no longer met, and
LStar smaller than 20. So that means when I'm done, I increase LStar. So what do I do in the semaphore again? I check if FRRoots contains the key. And if it does, I found my root. I can say LRoot is FRoots and remove this key because we no lon
ger need
it. It's assigned to our request, so we no longer
need it to be in the dictionary. And then we return the root. Right here, we say LFuture.SetResult. Basically, that's how you return a result
from the future. That's how it works. So you create a future, set the result, and
if you want to make certain that at some point, somebody is going to call LFuture.Value, right? And if the future is already complete, it's
going to be available right away. If it's not complete, then it's going to wa
it
for the completion. That's what the magic is about. So this is how it works. Let's put a break point here and a break point
here and a break point here and in a client. Let's run this. Okay. Of course, it's going to work because it's
not asynchronous, but let's do something very stupid, like LTaskWaitForRetrieve.Result. Why is this going to fail? Well, it's very easy. There's no route. This has not been initialized. The result here is nil. If I use result, the program would crash. However, th
e future is complete. Right? So LAskWaitForResult is going to give me a
result right away, but it's going to be nil. So if I put a break point here, you will see
exactly that. So if I F5 through, so wait for it. Boom. We go here. LRoute is nil. Right? So you're not going to have a route because
it was nil. I didn't put anything in there. So if you, however, switch over to the client,
we have a break point here, which gets hit first. And let's go through. You see we got a result from a future, bu
t
it's nil. So what I'm doing now is I'm going to press
F8, which is going to jump over the code and go to the next iteration, basically. That
is going to be waiting again. And you will see that the future becomes complete
at some point. So let's F8 through here. This is going to be hit, the break point. And then we do a LFuture.Value, which actually
waits, you see. Now we're waiting. And as soon as this is complete, the result
will be there. So if I'm going to press F8, you see it's
complete,
and then we get the route. And now, LRoute is a valid route. We can inspect it. We have multiple steps inside of it, right? It's not nil anymore. And this is exactly what I meant with asynchronous
code because this is not going to work synchronously. This is not going to work like this. You have to really think about what you're
doing. Now let's set the asynchronous property back
to true. Let's put a break point here and
a break point here and put a break point here. And this time, when we run
it, we're going
to see different things. Let's go back here. And this time, instead of just doing this,
let's say we say, 'GetDirections', and then we do it like we did before. Origin, destination, callback, TTask.Create—let's
go back here, make sure that we have TTask, and run TTask. Okay, and we pass in an anonymous function
that calls LDirections.GetDirections. And let's make it more interesting. Let's say 'Sleep'. 1000, so 1 second. So it's going to be working asynchronously. We go back here
. Let's put a break point here, F5. And let's see how it behaves now. I'm not even going to F5 because you will
see that it's not going to stop here. It goes through and immediately stops here
because the result is nil. The future is not complete. So we press F8, and you see now it's waiting
for the future to be complete. And as soon as it's complete, we get the result. This is how you would be doing it asynchronously. And now, the question, of course, arises,
'How do you make certain that the c
lient knows about the completion of this?' You know, in this case, obviously, I made
it very simple by making sure that the client knows about the completion of it by simply
waiting for the future. But what if I'm in an event-driven environment? That's something that you cannot do, right? So you have to do it different. But how you can do this differently and how
you can implement a solution for that, I'm going to show you in the next video. Stay tuned.
Comments