Сколько там недочетов. потенциальных ошибок. неверных решений?
1. У вас намешаны синхронная - в с одном потоке с блокировками (Wait, Sleep) - и асинхронная (async/await) многозадачность Теоретически, совмещать их можно, но лучше остановиться на каком-то одном подходе - легче будет. Синхронное выполнение задач (каждая - в своем потоке, который она блокирует по необходимости) - это проще, но асинхронное выполнение (при которм поток не блокируется, а освобождается, а задача планируется для продолжения после завершения операции в свободном потоке) позволяет получить больше производительности. Вот и выбирайте, что вам нужнее.
2. Для синхронизации используйте не переменные (а, к примеру, isRun у вас используется именно для этого), а объекты синхронизации. В частности, isRun следует заменить на объект синхронизации. Для синхронного выполнения лучше всего IMHO подойдет ManualResetEventSlim. для асинхронного - что-нибудь, на чем можно сделать await (например, TaskCompletionSource.Task).
3. Любой доступ к совместно используемым в разных задачах объектам(ресурсам) следует синхронизировать путем захвата связанного с ресурсом объекта исключающего доступа. Для синхронного выполнения подойдет оператор lock (вижу, что вы его уже используете). Для асихронного выполнения lock можно использовать, но - только если в его теле нет await (и компилятор вам не позволит нарушить это правило). Не пытайтесь обойти это правило, используя вместо lock другие объекты синхронизации, подразумевающие захват владения на уровне потока (Monitor, Mutex...) - компилятор всего лишь не сможет вам помешать сделать ошибку. В таком случае для синхронизации можно использовать SemaphoreSlim, но это требует понимания и аккуратности, поэтому подробности пока не пишу.
Думаю, пока этой информации к размышлению вам хватит.