Task.Run creates a new thread to run the callback on (whether it is sync or async). It returns a task you can wait on. so in your sample:
// .GetString() returns a Task<string>
Task<string> task = client.GetStringAsync("http://ifconfig.me");
// Task<string>.Run<string> returns a Task<string>
// that returns the value of the callback which in turn
// returns a Task<string>
Task<string> task3 = Task<string>.Run<string>(async () => await client.GetStringAsync("https://api.ipify.org?format=json"));
//simplified call without unnecessary await
Task<string> task3 = Task.Run(() => client.GetStringAsync("https://api.ipify.org?format=json"));
as .GetStringAsync() returns a Task<string>, there is no need for the extra Task.Run(). typically you would use Task.Run(async () = {....}); when you waned to mix async and sync code in the task.
await is just C# syntax sugar. it converts to the code after the await to a continue statement
async Task<string> FooAsync(string s)
{
await Task.Delay(1000);
return s;
}
is compiled to:
Task<string> FooAsync(string s)
{
return Task.Delay(1000).ContinueWith((t) => s);
}
to call a Task sync (continue on the same thread) use:
var r = FooAsync.Result; // stall current thread until task completes
var r2 = FooAsync.GetAwaiter().GetResult(); // stall current thread until task completes
the difference between the two is how errors throw by the task are returned. .GetAwaiter(), like await, returns the exception unmodified, while for compatibility .Result wraps the exception as an AggregateException.