如何为async / await包装多线程回调?

时间:2018-02-17 14:26:55

标签: c# multithreading async-await synchronizationcontext

我有异步/等待代码,并希望使用类似于websocket的API。它需要一个回调来接收从另一个线程调用的新消息。

我可以在与启动连接相同的async / await上下文中执行此回调而不需要锁定吗?

我认为这就是SynchronizationContext的用途,但我无法判断它的线程是否安全。如果我记录了thread-id,那么每个回调都将在不同的线程上。如果我将Task.CurrentId记录为null。我认为相同的同步上下文跨越不同的线程,所以这可能没问题,但我不知道如何确认它。

// External api, the callbacks will be from multiple threads
public class Api
{
    public static Connect(
        Action<Connection> onConnect,
        Action<Connection> onMessage) 
    {}
}

async Task<Connection> ConnectAsync(Action<Message> callback)
{
    if (SynchronizationContext.Current == null)
    {
        SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
    }

    var syncContext = SynchronizationContext.Current;

    var tcs = new TaskCompletionSource<Connection>();

    // use post() to ensure callbacks from other threads are executed thread-safely

    Action<Connection> onConnect = conn => 
    {
        syncContext.Post(o => tcs.SetResult(conn), null);
    };
    Action<Message> onMsg = msg => 
    { 
        syncContext.Post(o => callback(msg), null);
    };

    // call the multi-threaded non async/await api supplying the callbacks

    Api.Connect(onConnect, onMsg);

    return await tcs.Task;
}

var connection = await ConnectAsync(
    msg => 
    { 
        /* is code here threadsafe with the send without extra locking? */ 
    });

await connection.Send("Hello world);

1 个答案:

答案 0 :(得分:0)

感谢@Evk指出默认的SynchronizationContext没有实际同步任何内容或以您期望的方式实现发送/发布。

https://github.com/StephenClearyArchive/AsyncEx.Context

修复是使用Stephen Cleary的异步库,它在单个线程中将SynchronizationContext实现为消息泵,以便在与其他等待的调用相同的线程中调用post()调用。

// External api, the callbacks will be from multiple threads
public class Api
{
    public static Connect(
        Action<Connection> onConnect,
        Action<Connection> onMessage) 
    {}
}

async Task<Connection> ConnectAsync(Action<Message> callback)
{
    var syncContext = SynchronizationContext.Current;

    var tcs = new TaskCompletionSource<Connection>();

    // use post() to ensure callbacks from other threads are executed thread-safely

    Action<Connection> onConnect = conn => 
    {
        syncContext.Post(o => tcs.SetResult(conn), null);
    };
    Action<Message> onMsg = msg => 
    { 
        syncContext.Post(o => callback(msg), null);
    };

    // call the multi-threaded non async/await api supplying the callbacks

    Api.Connect(onConnect, onMsg);

    return await tcs.Task;
}

//
// https://github.com/StephenClearyArchive/AsyncEx.Context
//
Nito.AsyncEx.AsyncContext.Run(async () =>
{
    var connection = await ConnectAsync(
        msg => 
        { 
            /* this will execute in same thread as ConnectAsync and Send */ 
        });

    await connection.Send("Hello world);

    ... more async/await code
});