Organização simples de tarefas, assíncronas e aguardadas, e noções básicas de Thread usadas no Unity C#
Índice
1. Conceitos básicos de tarefa, assíncrona e espera, Thread
2. Uso básico de Tarefa, criação e inicialização de Tarefa
2. Crie uma tarefa com valor de retorno
3. Task fornece task.RunSynchronously() para execução síncrona de tarefas de Task
3. Método de bloqueio de tarefas (Wait/WaitAll/WaitAny)
1. Thread bloqueia o método de thread principal: método thread.Join()
4. A operação de continuação da tarefa (WhenAny/WhenAll/ContinueWith)
1. Operação de continuação de tarefa (WhenAny/WhenAll/ContinueWith)
2. A operação de continuação de Task.Factory (WhenAny/WhenAll/ContinueWith)
5. Cancelamento de tarefa de tarefa (CancellationTokenSource)
1. Cancelamento de tarefa de thread
3. CancellationTokenSource por source.Token.Register e source.CancelAfter(XXXXms)
5. Método assíncrono (async/await)
Seis, tarefa, assíncrona e espera, resumo simples do thread
1. Conceitos básicos de tarefa, assíncrona e espera, Thread
1. Thread, multithread
Thread (Thread) é a unidade básica de execução em um processo e a unidade básica para o sistema operacional alocar tempo de CPU.Um processo pode conter vários threads, e o primeiro thread executado na entrada do processo é considerado o thread principal do processo .
Em aplicativos .NET, o método Main() é usado como ponto de entrada. Quando esse método é chamado, o sistema criará automaticamente um thread principal. Threads são compostos principalmente de registros de CPU, pilhas de chamadas e armazenamento local de threads (Thread Local Storage, TLS) .
O registro da CPU registra principalmente o status do thread em execução no momento, a pilha de chamadas é usada principalmente para manter a memória e os dados chamados pelo thread, e o TLS é usado principalmente para armazenar as informações de status do thread.
Multithreading: Várias tarefas podem ser concluídas ao mesmo tempo; o programa pode responder mais rapidamente; tarefas que ocupam muito tempo de processamento ou tarefas que não estão sendo processadas no momento podem ceder periodicamente o tempo de processamento para outras tarefas; as tarefas podem ser interrompidas em a qualquer momento; Defina a prioridade de cada tarefa para otimizar o desempenho do programa.
class Program
{
static void Main(string[] args)
{
//创建无参的线程
Thread thread1 = new Thread(new ThreadStart(Thread1));
//调用Start方法执行线程
thread1.Start();
Console.ReadKey();
}
/// <summary>
/// 创建无参的方法
/// </summary>
static void Thread1()
{
Console.WriteLine("这是无参的方法");
}
}
2、Tarefa
A tarefa é iniciada com base no ThreadPool. Vamos entender brevemente o ThreadPool.
- Há um certo número de threads no ThreadPool. Se houver uma tarefa a ser processada, um thread ocioso será obtido do pool de threads para executar a tarefa. Depois que a tarefa for executada, o thread não será destruído, mas será ser reciclado pelo pool de threads para tarefas subsequentes.
- Quando todos os threads no pool de threads estiverem ocupados e houver uma nova tarefa a ser processada, o pool de threads criará um novo thread para processar a tarefa
- Se o número de threads atingir o valor máximo definido, a tarefa será enfileirada e aguardará que outras tarefas liberem threads antes da execução.
- O pool de threads pode reduzir a criação de threads e economizar sobrecarga. Veja o uso de um ThreadPool da seguinte maneira
static void Main(string[] args)
{
for (int i = 1; i <=10; i++)
{
//ThreadPool执行任务
ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => {
Console.WriteLine($"第{obj}个执行任务");
}),i);
}
Console.ReadKey();
}
O código acima executa 10 tarefas por meio de ThreadPool e o resultado da execução é
3. assíncrono (aguardar)
async é usado principalmente para modificar métodos.
Quando um método é chamado, o chamador precisa esperar que o método seja concluído e retorne antes de continuar . Chamamos esse método de método síncrono;
Quando um método é chamado, ele retorna imediatamente e obtém um thread para executar o negócio interno do método. O chamador não precisa esperar a conclusão do método. Chamamos esse método de método assíncrono.
A vantagem da assincronia é que ela não bloqueia (o thread de chamada não suspenderá a execução para aguardar a conclusão do subthread), portanto, definimos algumas tarefas demoradas que não precisam usar os resultados imediatamente para serem executadas de forma assíncrona, o que pode melhorar a eficiência de execução do programa.
Palavras-chave assíncronas e aguardadas em C# , a programação assíncrona é muito popular.
1) Quando estamos lidando com a UI, estamos usando um método de longa execução quando um botão é clicado, como ler um arquivo grande ou algo que leva muito tempo
2) Neste caso toda a aplicação deverá aguardar o término de toda a tarefa.
3) Em outras palavras, se algum processo da aplicação síncrona for bloqueado, toda a aplicação será bloqueada e nossa aplicação parará de responder até que toda a tarefa seja concluída.
A programação assíncrona é muito útil nesta situação. Ao utilizar a programação assíncrona, a aplicação pode continuar a realizar outros trabalhos que não dependem da conclusão de toda a tarefa.
Net4.0 lançou a classe Task com base em ThreadPool . A Microsoft recomenda fortemente o uso de Task para executar tarefas assíncronas. Agora, os métodos assíncronos na biblioteca de classes C# usam basicamente Task;
net5.0 introduziu async/await para tornar a programação assíncrona mais conveniente.
2. Uso básico de Tarefa, criação e inicialização de Tarefa
Conhecemos as desvantagens do ThreadPool:
(1) Não podemos controlar a ordem de execução dos threads no pool de threads,
(2) Também é impossível obter notificações de cancelamento/anormalidade/conclusão de thread no pool de threads.
net4.0 lançou Task com base em ThreadPool
(1) A tarefa tem as vantagens do pool de threads
(2) Ao mesmo tempo, também resolve as desvantagens de usar o pool de threads que não é fácil de controlar
1. Crie e execute uma tarefa
Vamos primeiro ver como criar e executar uma tarefa. Existem três maneiras de criar e iniciar uma tarefa:
- Tarefa tarefa = new Tarefa(); tarefa.Iniciar();
- Tarefa tarefa = Task.Factory.StartNew();
- Tarefa tarefa = Task.Run();
static void Main(string[] args)
{
//1.new方式实例化一个Task,需要通过Start方法启动
Task task = new Task(() =>
{
Thread.Sleep(100);
Console.WriteLine($"hello, task1的线程ID为{Thread.CurrentThread.ManagedThreadId}");
});
task.Start();
//2.Task.Factory.StartNew(Action action)创建和启动一个Task
Task task2 = Task.Factory.StartNew(() =>
{
Thread.Sleep(100);
Console.WriteLine($"hello, task2的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
});
//3.Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task
Task task3 = Task.Run(() =>
{
Thread.Sleep(100);
Console.WriteLine($"hello, task3的线程ID为{ Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine("执行主线程!");
Console.ReadKey();
}
Os resultados da execução são os seguintes: podemos ver que "executar o thread principal" é impresso primeiro e depois imprimir cada tarefa, o que mostra que a tarefa não bloqueará o thread principal:
2. Crie uma tarefa com valor de retorno
No código acima, Task não tem valor de retorno. Também podemos criar uma Task com valor de retorno . O uso é basicamente o mesmo que sem valor de retorno. Vamos simplesmente modificar a castanha acima. O código é o seguinte:
Task<tipo de dados>, task.Result obtém o resultado
Vale ressaltar que: task.Resut irá bloquear o thread ao obter o resultado, ou seja, se a tarefa não for executada, ele irá aguardar a conclusão da tarefa e obter o Resultado, para então executar o seguinte código
- Tarefa<string> tarefa = new Tarefa(); tarefa.Iniciar();
- Tarefa<string> tarefa = Task.Factory.StartNew();
- Tarefa<string> tarefa = Task.Run();
static void Main(string[] args)
{
//1.new方式实例化一个Task,需要通过Start方法启动
Task<string> task = new Task<string>(() =>
{
return $"hello, task1的ID为{Thread.CurrentThread.ManagedThreadId}";
});
task.Start();
//2.Task.Factory.StartNew(Func func)创建和启动一个Task
Task<string> task2 =Task.Factory.StartNew<string>(() =>
{
return $"hello, task2的ID为{ Thread.CurrentThread.ManagedThreadId}";
});
//3.Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task
Task<string> task3= Task.Run<string>(() =>
{
return $"hello, task3的ID为{ Thread.CurrentThread.ManagedThreadId}";
});
Console.WriteLine("执行主线程!");
Console.WriteLine(task.Result);
Console.WriteLine(task2.Result);
Console.WriteLine(task3.Result);
Console.ReadKey();
}
O resultado da operação é o seguinte:
3. Task fornece task.RunSynchronously() para execução síncrona de tarefas de Task
A execução de Task em todos os códigos acima é assíncrona e não bloqueará o thread principal. Em alguns cenários, o que devemos fazer se quisermos que a tarefa seja executada de forma síncrona? Task fornece task.RunSynchronously() para executar tarefas de Task de forma síncrona
- Tarefa tarefa = new Tarefa(); task.RunSynchronously();
static void Main(string[] args)
{
Task task = new Task(() =>
{
Thread.Sleep(100);
Console.WriteLine("执行Task结束!");
});
//同步执行,task会阻塞主线程
task.RunSynchronously();
Console.WriteLine("执行主线程结束!");
Console.ReadKey();
}
O resultado da operação é o seguinte:
3. Método de bloqueio de tarefas (Wait/WaitAll/WaitAny)
1. Thread bloqueia o método de thread principal: método thread.Join()
O método Join do Thread pode bloquear o thread de chamada , mas existem algumas desvantagens:
① Se quisermos implementar o bloqueio de muitos threads, cada thread deve chamar o método Join uma vez;
②Se quisermos desbloquear imediatamente quando todos os threads forem executados (ou qualquer thread for executado), não é fácil usar o método Join.
O código de referência é o seguinte
static void Main(string[] args)
{
Thread th1 = new Thread(() => {
Thread.Sleep(500);
Console.WriteLine("线程1执行完毕!");
});
th1.Start();
Thread th2 = new Thread(() => {
Thread.Sleep(1000);
Console.WriteLine("线程2执行完毕!");
});
th2.Start();
//阻塞主线程
th1.Join();
th2.Join();
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}
O resultado da operação é o seguinte:
2. A tarefa fornece métodos Wait/WaitAny/WaitAll, que podem controlar o bloqueio de thread de forma mais conveniente
- Task.Wait() significa aguardar a execução da tarefa e sua função é semelhante a thead.Join();
- Task.WaitAll(Task[] tarefas) significa que somente quando todas as tarefas forem executadas, o bloco será desbloqueado;
- Task.WaitAny ( Task[] tarefas ) significa que enquanto uma tarefa for executada, ela será desbloqueada;
O código de referência é o seguinte:
static void Main(string[] args)
{
Task task1 = new Task(() => {
Thread.Sleep(500);
Console.WriteLine("线程1执行完毕!");
});
task1.Start();
Task task2 = new Task(() => {
Thread.Sleep(1000);
Console.WriteLine("线程2执行完毕!");
});
task2.Start();
//阻塞主线程。task1,task2都执行完毕再执行主线程
//执行【task1.Wait();task2.Wait();】可以实现相同功能
Task.WaitAll(new Task[] { task1, task2 });
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}
O resultado da operação é o seguinte
4. A operação de continuação da tarefa (WhenAny/WhenAll/ContinueWith)
1. Operação de continuação de tarefa (WhenAny/WhenAll/ContinueWith)
O valor de retorno dos métodos Wait/WaitAny/WaitAll acima é nulo e esses métodos simplesmente implementam threads de bloqueio .
Agora pensamos: depois que todas as tarefas forem executadas (ou alguma tarefa for executada), comece a realizar as operações de acompanhamento, como fazer isso?
Neste momento, o método WhenAny/WhenAll pode ser usado e esses métodos retornam uma instância de tarefa após a execução.
- task.WhenAll(Task[] tarefas) indica que todas as tarefas são executadas antes de realizar operações subsequentes
- task.WhenAny(Task[] tarefas) Indica que após qualquer tarefa ser executada, as operações subsequentes serão executadas
O código de referência é o seguinte:
static void Main(string[] args)
{
Task task1 = new Task(() => {
Thread.Sleep(500);
Console.WriteLine("线程1执行完毕!");
});
task1.Start();
Task task2 = new Task(() => {
Thread.Sleep(1000);
Console.WriteLine("线程2执行完毕!");
});
task2.Start();
//task1,task2执行完了后执行后续操作
Task.WhenAll(task1, task2).ContinueWith((t) => {
Thread.Sleep(100);
Console.WriteLine("执行后续操作完毕!");
});
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}
O resultado da operação é o seguinte:
2. A operação de continuação de Task.Factory (WhenAny/WhenAll/ContinueWith)
As castanhas acima também podem ser implementadas por Task.Factory.ContinueWhenAll(Task[] tasks, Action continuationAction) e Task.Factory.ContinueWhenAny(Task[] tasks, Action continuationAction)
Modifique o código acima da seguinte maneira
static void Main(string[] args)
{
Task task1 = new Task(() => {
Thread.Sleep(500);
Console.WriteLine("线程1执行完毕!");
});
task1.Start();
Task task2 = new Task(() => {
Thread.Sleep(1000);
Console.WriteLine("线程2执行完毕!");
});
task2.Start();
//通过TaskFactroy实现
Task.Factory.ContinueWhenAll(new Task[] { task1, task2 }, (t) =>
{
Thread.Sleep(100);
Console.WriteLine("执行后续操作");
});
Console.WriteLine("主线程执行完毕!");
Console.ReadKey();
}
O resultado da execução permanece inalterado, como segue
5. Cancelamento de tarefa de tarefa (CancellationTokenSource)
1. Cancelamento de tarefa de thread
Antes da tarefa, usamos Thread para executar a tarefa. Como o Thread cancela a tarefa?
O processo geral é:
- 1) Defina uma variável para controlar se a tarefa será interrompida, como definir uma variável isStop
- 2) Em seguida, o thread pesquisa para verificar isStop e para se isStop for verdadeiro
O código de referência é o seguinte:
static void Main(string[] args)
{
bool isStop = false;
int index = 0;
//开启一个线程执行任务
Thread th1 = new Thread(() =>
{
while (!isStop)
{
Thread.Sleep(1000);
Console.WriteLine($"第{++index}次执行,线程运行中...");
}
});
th1.Start();
//五秒后取消任务执行
Thread.Sleep(5000);
isStop = true;
Console.ReadKey();
}
O resultado da operação é o seguinte
2. Cancelamento de tarefa
Existe uma classe especial CancellationTokenSource em Task para cancelar a execução da tarefa
Ainda usando o exemplo acima, ajuste o código da seguinte forma
static void Main(string[] args)
{
CancellationTokenSource source = new CancellationTokenSource();
int index = 0;
//开启一个task执行任务
Task task1 = new Task(() =>
{
while (!source.IsCancellationRequested)
{
Thread.Sleep(1000);
Console.WriteLine($"第{++index}次执行,线程运行中...");
}
});
task1.Start();
//五秒后取消任务执行
Thread.Sleep(5000);
//source.Cancel()方法请求取消任务,IsCancellationRequested会变成true
source.Cancel();
Console.ReadKey();
}
O efeito da execução do programa permanece inalterado, como segue
3. CancellationTokenSource por source.Token.Register e source.CancelAfter(XXXXms)
A função de CancellationTokenSource não é apenas cancelar a execução da tarefa , podemos usar
- source.CancelAfter(5000) cancela automaticamente a tarefa após 5 segundos
- source.Token.Register(Action action) pode registrar a função de callback acionada pelo cancelamento da tarefa, ou seja, a ação registrada será executada quando a tarefa for cancelada
O código de referência é o seguinte
static void Main(string[] args)
{
CancellationTokenSource source = new CancellationTokenSource();
//注册任务取消的事件
source.Token.Register(() =>
{
Console.WriteLine("任务被取消后执行xx操作!");
});
int index = 0;
//开启一个task执行任务
Task task1 = new Task(() =>
{
while (!source.IsCancellationRequested)
{
Thread.Sleep(1000);
Console.WriteLine($"第{++index}次执行,线程运行中...");
}
});
task1.Start();
//延时取消,效果等同于Thread.Sleep(5000);source.Cancel();
source.CancelAfter(5000);
Console.ReadKey();
}
O resultado da operação é o seguinte
5. Método assíncrono (async/await)
A aparência de async e await no C# 5.0 facilita a programação assíncrona.
Essa abordagem aproveita o suporte assíncrono no .NET Framework 4.5 e posterior, no .NET Core e no Windows Runtime.
O compilador faz o trabalho árduo que os desenvolvedores fizeram e o aplicativo mantém uma estrutura lógica semelhante ao código síncrono.
- async é usado antes da definição do método e await só pode ser escrito em métodos marcados com async.
- Preste atenção onde o await espera de forma assíncrona. O código por trás do await pode ser executado em um thread diferente do código anterior.
- A palavra-chave async cria uma máquina de estado, semelhante à instrução yield return; await desbloqueará o thread atual e concluirá outras tarefas
Veja como ler o conteúdo do arquivo de forma síncrona e assíncrona.
Podemos ver que o código de leitura assíncrona é basicamente igual ao código de leitura síncrona. async/await torna a codificação assíncrona mais fácil, podemos escrever código assíncrono como código síncrono.
Preste atenção a um pequeno problema: o valor de retorno da assinatura do método no método assíncrono é Task, e o valor de retorno no código é T.
O valor de retorno da assinatura de GetContentAsync no código é Task, enquanto o valor de retorno no código é string. Manter esse detalhe em mente é útil ao analisar código assíncrono.
O valor de retorno da assinatura do método assíncrono possui os três tipos a seguir:
- ① Tarefa: Se o método chamador deseja obter um valor de retorno do tipo T chamando um método assíncrono, a assinatura deve ser Tarefa;
- ② Tarefa: Se o método de chamada não deseja obter um valor por meio do método assíncrono, mas apenas deseja rastrear o status de execução do método assíncrono, podemos definir o valor de retorno da assinatura do método assíncrono como Tarefa;
- ③ void: Se o método de chamada for apenas para chamar o método assíncrono e não interagir com o método assíncrono, podemos definir o valor de retorno da assinatura do método assíncrono como void, que também é chamado de "chamar e esquecer".
O código de referência é o seguinte
static void Main(string[] args)
{
//Console.WriteLine(" Environment.CurrentDirectory "+ Environment.CurrentDirectory);
string content = GetContentAsync(Environment.CurrentDirectory + @"/test.txt").Result;
//调用同步方法
//string content = GetContent(Environment.CurrentDirectory + @"/test.txt");
Console.WriteLine(content);
Console.ReadKey();
}
/// <summary>
/// 异步读取文件内容
/// </summary>
/// <param name="filename"></param>
/// <returns></returns>
async static Task<string> GetContentAsync(string filename)
{
FileStream fs = new FileStream(filename, FileMode.Open);
var bytes = new byte[fs.Length];
//ReadAync方法异步读取内容,不阻塞线程
Console.WriteLine("开始读取文件");
int len = await fs.ReadAsync(bytes, 0, bytes.Length);
string result = Encoding.UTF8.GetString(bytes);
return result;
}
/// <summary>
/// 同步读取文件内容
/// </summary>
/// <param name="filename"></param>
/// <returns></returns>
static string GetContent(string filename)
{
FileStream fs = new FileStream(filename, FileMode.Open);
var bytes = new byte[fs.Length];
//Read方法同步读取内容,阻塞线程
int len = fs.Read(bytes, 0, bytes.Length);
string result = Encoding.UTF8.GetString(bytes);
return result;
}
O resultado da operação é o seguinte:
Seis, tarefa, assíncrona e espera, resumo simples do thread
Por fim, simplesmente lembramos de alguns recursos, para que possamos entendê-los melhor:
- 1) async/await é baseado em Task
- 2) Task é uma melhoria no encapsulamento do ThreadPool, principalmente para um controle mais eficaz dos threads no pool de threads (threads no ThreadPool, é difícil para nós controlar sua ordem de execução, continuação e cancelamento de tarefas, etc. através do código);
- 3) ThreadPool é baseado em Thread, o objetivo principal é reduzir o número de criação de Thread e o custo de gerenciamento de Thread.
- 4) Async/await Task é mais avançada em C# e também é um recurso promovido pela Microsoft
- 5) Podemos tentar usar Task em vez de Thread/ThreadPool durante o desenvolvimento. Para lidar com tarefas de IO locais e de rede, tente usar async/await para melhorar a eficiência de execução de tarefas.