博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

asio多线程, strand

Posted on 2018-03-07 13:36  bw_0927  阅读(1405)  评论(0)    收藏  举报

google查询:

one io_service per thread

one io_service multiple threads

一个io_service 多个线程

io_service 多个线程

 

http://www.crazygaze.com/blog/2016/03/17/how-strands-work-and-why-you-should-use-them/ 

 

http://www.cnblogs.com/my_life/articles/4804223.html

 

http://blog.csdn.net/ml232528/article/details/8710927

 

1、实现多线程方法:

其实就是多个线程同时调用io_service::run

        for (int i = 0; i != m_nThreads; ++i)
        {
            boost::shared_ptr<boost::thread> pTh(new boost::thread(
                boost::bind(&boost::asio::io_service::run,&m_ioService)));
            m_listThread.push_back(pTh);
        }

2、多线程调度情况:

asio规定:只能在调用io_service::run的线程中才能调用事件完成处理器

注:事件完成处理器就是你async_accept、async_write等注册的句柄,类似于回调的东西。

单线程:

如果只有一个线程调用io_service::run,根据asio的规定,事件完成处理器也只能在这个线程中执行。也就是说,你所有代码都在同一个线程中运行,因此变量的访问是安全的。

多线程:

如果有多个线程同时调用同一个io_service实例的run()方法以实现多线程并发处理。

对于asio来说,这些线程都是平等的,没有主次之分。如果你投递的一个请求比如async_write完成时,asio将随机的激活调用io_service::run的线程。并在这个线程中调用事件完成处理器(async_write当时注册的句柄)。

如果你的代码耗时较长,这个时候你投递的另一个async_write请求完成时,asio将不等待你的代码处理完成,它将在另外的一个调用io_service::run线程中,调用async_write当时注册的句柄。

也就是说,你注册的同一个事件完成处理器有可能同时多个线程中调用

当然你可以使用 boost::asio::io_service::strand让完成事件处理器的调用,在同一时间只有一个, 比如下面的的代码:

  socket_.async_read_some(boost::asio::buffer(buffer_),
      strand_.wrap(
        boost::bind(&connection::handle_read, shared_from_this(),
          boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred)));

...

boost::asio::io_service::strand strand_;

 

此时async_read_som完成后掉用handle_read时,必须等待其它handle_read调用完成时才能被执行(async_read_som引起的handle_read调用)。

多线程调用时,还有一个重要的问题,那就是无序化

比如说,你短时间内投递多个async_write,那么完成处理器的调用并不是按照你投递async_write的顺序调用的。asio第一次调用完成事件处理器,有可能是第二次async_write返回的结果,也有可能是第3次的。

即使使用strand也是这样的。strand只是保证同一时间只运行一个完成处理器,但它并不保证顺序。

保证不会并发(用来互斥的访问共享资源),但不会保证顺序。

通过strand分发的handler可以和那些不是通过strand分发的handler或者通过另外一个不同的strand分发的handler同时在执行。

 

代码测试:

服务器:

将下面的代码编译以后,使用cmd命令提示符下传人参数<IP> <port> <threads>调用

比如:test.exe 0.0.0.0 3005 10   

客服端 使用windows自带的telnet

cmd命令提示符:

telnet 127.0.0.1 3005

 

原理:客户端连接成功后,同一时间调用100次boost::asio::async_write给客户端发送数据,并且在完成事件处理器中打印调用序号,和线程ID。

核心代码:

    void start()
    {
        for (int i = 0; i != 100; ++i)
        {
            boost::shared_ptr<string> pStr(new string);
            *pStr = boost::lexical_cast<string>(boost::this_thread::get_id());
            *pStr += "\r\n";
            boost::asio::async_write(m_nSocket,boost::asio::buffer(*pStr),
                boost::bind(&CMyTcpConnection::HandleWrite,shared_from_this(),
                 boost::asio::placeholders::error,
                 boost::asio::placeholders::bytes_transferred,
                 pStr,i)
                );
        }
    }

//去掉 boost::mutex::scoped_lock lk(m_ioMutex); 效果更明显。

    void HandleWrite(const boost::system::error_code& error
        ,std::size_t bytes_transferred
        ,boost::shared_ptr<string> pStr,int nIndex)
    {
        if (!error)
        {
            boost::mutex::scoped_lock lk(m_ioMutex);
            cout << "发送序号=" << nIndex << ",线程id=" << boost::this_thread::get_id() << endl;
        }
        else
        {
            cout << "连接断开" << endl;
        }
    }

 

完整代码:

#include <boost/bind.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/asio.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <string>
#include <iostream>


using std::cout;
using std::endl;
using std::string;
using boost::asio::ip::tcp;


class CMyTcpConnection
    : public boost::enable_shared_from_this<CMyTcpConnection>
{
public:
    CMyTcpConnection(boost::asio::io_service &ser)
        :m_nSocket(ser)
    {
    }
    typedef boost::shared_ptr<CMyTcpConnection> CPMyTcpCon;


    static CPMyTcpCon CreateNew(boost::asio::io_service& io_service)
    {
        return CPMyTcpCon(new CMyTcpConnection(io_service));
    }


   
public:
    void start()
    {
        for (int i = 0; i != 100; ++i)
        {
            boost::shared_ptr<string> pStr(new string);
            *pStr = boost::lexical_cast<string>(boost::this_thread::get_id());
            *pStr += "\r\n";
            boost::asio::async_write(m_nSocket,boost::asio::buffer(*pStr),
                boost::bind(&CMyTcpConnection::HandleWrite,shared_from_this(),
                 boost::asio::placeholders::error,
                 boost::asio::placeholders::bytes_transferred,
                 pStr,i)
                );
        }
    }
    tcp::socket& socket()
    {
        return m_nSocket;
    }
private:
    void HandleWrite(const boost::system::error_code& error
        ,std::size_t bytes_transferred
        ,boost::shared_ptr<string> pStr,int nIndex)
    {
        if (!error)
        {
            boost::mutex::scoped_lock lk(m_ioMutex);
            cout << "发送序号=" << nIndex << ",线程id=" << boost::this_thread::get_id() << endl;
        }
        else
        {
            cout << "连接断开" << endl;
        }
    }
private:
    tcp::socket m_nSocket;                  //connecton封装socket
    boost::mutex m_ioMutex;
};


class CMyService
    : private boost::noncopyable
{
public:
    CMyService(string const &strIP,string const &strPort,int nThreads)
        :m_tcpAcceptor(m_ioService)
        ,m_nThreads(nThreads)
    {
        tcp::resolver resolver(m_ioService);
        tcp::resolver::query query(strIP,strPort);
        tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
        boost::asio::ip::tcp::endpoint endpoint = *resolver.resolve(query);
        m_tcpAcceptor.open(endpoint.protocol());
        m_tcpAcceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
        m_tcpAcceptor.bind(endpoint);
        m_tcpAcceptor.listen();


        StartAccept();
    }
    ~CMyService(){Stop();}
public:
    void Stop() 
    { 
        m_ioService.stop();
        for (std::vector<boost::shared_ptr<boost::thread>>::const_iterator it = m_listThread.cbegin();
            it != m_listThread.cend(); ++ it)
        {
            (*it)->join();
        }
    }
    void Start()
    {
        for (int i = 0; i != m_nThreads; ++i)
        {
            boost::shared_ptr<boost::thread> pTh(new boost::thread(
                boost::bind(&boost::asio::io_service::run,&m_ioService)));                  //多个线程,一个io_service
            m_listThread.push_back(pTh);
        }
    }
private:
    void HandleAccept(const boost::system::error_code& error
        ,boost::shared_ptr<CMyTcpConnection> newConnect)
    {
        if (!error)
        {
            newConnect->start();                       
        }
        StartAccept();
    }


    void StartAccept()
    {
        CMyTcpConnection::CPMyTcpCon newConnect = CMyTcpConnection::CreateNew(m_tcpAcceptor.get_io_service());
        m_tcpAcceptor.async_accept(newConnect->socket(),
            boost::bind(&CMyService::HandleAccept, this,
            boost::asio::placeholders::error,newConnect));
    }
private:
    boost::asio::io_service m_ioService;
    boost::asio::ip::tcp::acceptor m_tcpAcceptor;
    std::vector<boost::shared_ptr<boost::thread>> m_listThread;
    std::size_t m_nThreads;
};


int main(int argc, char* argv[])
{
    try
    {
        if (argc != 4)
        {
            std::cerr << "<IP> <port> <threads>\n";
            return 1;
        }
        int nThreads = boost::lexical_cast<int>(argv[3]);
        CMyService mySer(argv[1],argv[2],nThreads);
        mySer.Start();
        getchar();
        mySer.Stop();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }
    return 0;
}

 

 

测试发现和上面的理论是一致的,发送序号是乱的,线程ID也不是同一个。

 

asio多线程中线程的合理个数:

作为服务器,在不考虑省电的情况下,应该尽可能的使用cpu。也就是说,为了让cpu都忙起来,你的线程个数应该大于等于你电脑的cpu核心数(一个核心运行一个线程)。具体的值没有最优方案,大多数人使用cpu核心数*2 + 2的这种方案,但它不一定适合你的情况。

asio在windows xp等系统中的实现:

asio在windows下使用完成端口,如果你投递的请求没有完成,那么这些线程都在等待GetQueuedCompletionStatus的返回,也就是等待内核对象,此时线程是不占用cpu时间的。

 

==========================

http://senlinzhan.github.io/2017/09/17/boost-asio/

 

Boost.Asio 有两种支持多线程的方式,第一种方式比较简单:在多线程的场景下,每个线程都持有一个io_service,并且每个线程都调用各自的io_servicerun()方法。
另一种支持多线程的方式:全局只分配一个io_service,并且让这个io_service在多个线程之间共享,每个线程都调用全局的io_servicerun()方法。

每个线程一个 I/O Service  (One thread per loop)

  让我们先分析第一种方案:在多线程的场景下,每个线程都持有一个io_service (通常的做法是,让线程数和 CPU 核心数保持一致)。那么这种方案有什么特点呢?

  • 在多核的机器上,这种方案可以充分利用多个 CPU 核心。
  • 某个 socket 描述符并不会在多个线程之间共享,所以不需要引入同步机制。  因为一个socket只会绑定到一个固定的io_service/eventloop上。
  • 在 event handler 中不能执行阻塞的操作,否则将会阻塞掉io_service所在的线程

  下面我们实现了一个AsioIOServicePool,封装了线程池的创建操作 [完整代码]

class AsioIOServicePool
{
public:
    using IOService = boost::asio::io_service;                       //http://www.cnblogs.com/my_life/articles/8487082.html
    using Work = boost::asio::io_service::work;                      //类型别名
    using WorkPtr = std::unique_ptr<Work>;
    AsioIOServicePool(std::size_t size = std::thread::hardware_concurrency())
        : ioServices_(size),
          works_(size),
          nextIOService_(0)
    {
        for (std::size_t i = 0; i < size; ++i)
        {
            works_[i] = std::unique_ptr<Work>(new Work(ioServices_[i]));             //给每个io_service各自的work
        }
        for (std::size_t i = 0; i < ioServices_.size(); ++i)
        {
            threads_.emplace_back([this, i] ()                         //http://www.cnblogs.com/my_life/articles/7772513.html
                                  {
                                      ioServices_[i].run();
                                  });
        }
    }
    AsioIOServicePool(const AsioIOServicePool &) = delete;            //http://www.cnblogs.com/my_life/articles/7909814.html
    AsioIOServicePool &operator=(const AsioIOServicePool &) = delete;
    // 使用 round-robin 的方式返回一个 io_service
    boost::asio::io_service &getIOService()
    {
        auto &service = ioServices_[nextIOService_++];
        if (nextIOService_ == ioServices_.size())
        {
            nextIOService_ = 0;
        }
        return service;
    }
    void stop()
    {
        for (auto &work: works_)              http://www.cnblogs.com/my_life/articles/7910074.html
        {
            work.reset();
        }
        for (auto &t: threads_)
        {
            t.join();
        }
    }
private:
    std::vector<IOService>       ioServices_;        //多个io_service
    std::vector<WorkPtr>         works_;
    std::vector<std::thread>     threads_;           //多个线程
    std::size_t                  nextIOService_;
};

  

 


  AsioIOServicePool使用起来也很简单: 

1
2
3
4
5
6
7
8
9
10
std::mutex mtx; // protect std::cout
AsioIOServicePool pool;
 
boost::asio::steady_timer timer{pool.getIOService(), std::chrono::seconds{2}};
timer.async_wait([&mtx] (const boost::system::error_code &ec)
{
std::lock_guard<std::mutex> lock(mtx);
std::cout << "Hello, World! " << std::endl;
});
pool.stop();

 

一个 I/O Service 与多个线程【这种模式适用于并发执行简单的无序操作,不太适合网络IO】

  另一种方案则是先分配一个全局io_service,然后开启多个线程,每个线程都调用这个io_servicerun()方法。这样,当某个异步事件完成时,io_service就会将相应的 event handler 交给任意一个线程去执行。
  然而这种方案在实际使用中,需要注意一些问题:

  • 在 event handler 中允许执行阻塞的操作 (例如数据库查询操作)。
  • 线程数可以大于 CPU 核心数,譬如说,如果需要在 event handler 中执行阻塞的操作,为了提高程序的响应速度,这时就需要提高线程的数目。
  • 由于多个线程同时运行事件循环(event loop),所以会导致一个问题:即一个 socket 描述符可能会在多个线程之间共享,容易出现竞态条件 (race condition)。

     譬如说,如果某个 socket 的可读事件很快发生了两次,那么就会出现两个线程同时读同一个 socket 的问题 (可以使用strand解决这个问题)。

  下面实现了一个线程池,在每个 worker 线程中执行io_servicerun()方法 [完整代码]

 

class AsioThreadPool
{
public:
    AsioThreadPool(int threadNum = std::thread::hardware_concurrency())
        : work_(new boost::asio::io_service::work(service_))
    {
        for (int i = 0; i < threadNum; ++i)
        {
            threads_.emplace_back([this] () { service_.run(); });
        }
    }
    AsioThreadPool(const AsioThreadPool &) = delete;
    AsioThreadPool &operator=(const AsioThreadPool &) = delete;
    boost::asio::io_service &getIOService()
    {
        return service_;
    }
    void stop()
    {
        work_.reset();
        for (auto &t: threads_)
        {
            t.join();            
        }        
    }
private:
    boost::asio::io_service service_;                  //一个io_serice
    std::unique_ptr<boost::asio::io_service::work> work_;
    std::vector<std::thread> threads_;                 //多个线程
};

  


无锁的同步方式

  要怎样解决前面提到的竞态条件呢?

Boost.Asio 提供了io_service::strand:如果多个 event handler 通过同一个 strand 对象分发 (dispatch),那么这些 event handler 就会保证顺序地执行。   【错了,这种方式不能保证顺序,只能保证非并发】
  例如,下面的例子使用 strand,所以不需要使用互斥锁保证同步了 [完整代码]

http://www.cnblogs.com/my_life/articles/5331789.html

http://www.cnblogs.com/my_life/articles/5452088.html

 

AsioThreadPool pool(4);    // 开启 4 个线程
boost::asio::steady_timer timer1{pool.getIOService(), std::chrono::seconds{1}};
boost::asio::steady_timer timer2{pool.getIOService(), std::chrono::seconds{1}};
int value = 0;
boost::asio::io_service::strand strand{pool.getIOService()};	
	
timer1.async_wait(strand.wrap([&value] (const boost::system::error_code &ec)
                              {
                                  std::cout << "Hello, World! " << value++ << std::endl;
                              }));
timer2.async_wait(strand.wrap([&value] (const boost::system::error_code &ec)
                              {
                                  std::cout << "Hello, World! " << value++ << std::endl;
                              }));
pool.stop();

  

http://www.boost.org/doc/libs/1_43_0/doc/html/boost_asio/reference/io_service__strand.html

async_op_1(..., s.wrap(a));
async_op_2(..., s.wrap(b)); 

the completion of the first async operation will perform s.dispatch(a), and the second will perform s.dispatch(b), but the order in which those are performed is unspecified. 

 


多线程 Echo Server
 

  下面的EchoServer可以在多线程中使用,它使用asio::strand来解决前面提到的竞态问题 [完整代码]

class TCPConnection : public std::enable_shared_from_this<TCPConnection> 
{
public:
    TCPConnection(boost::asio::io_service &io_service)
        : socket_(io_service),
          strand_(io_service)
    { }
    
    tcp::socket &socket() {  return socket_;  }
    void start() {  doRead();  }
    
private:
    void doRead()
    {
        auto self = shared_from_this();
        socket_.async_read_some(
            boost::asio::buffer(buffer_, buffer_.size()),
            strand_.wrap([this, self](boost::system::error_code ec, 
                                      std::size_t bytes_transferred)
                         {
                             if (!ec) {  doWrite(bytes_transferred);  }
                         }));
    }
    void doWrite(std::size_t length)
    {
        auto self = shared_from_this();
        boost::asio::async_write(
            socket_, boost::asio::buffer(buffer_, length),
            strand_.wrap([this, self](boost::system::error_code ec,
                                      std::size_t /* bytes_transferred */)
                         {
                             if (!ec) {  doRead();  }
                         }));
    }
private:
    tcp::socket socket_;
    boost::asio::io_service::strand strand_;
    std::array<char, 8192> buffer_;
};
class EchoServer
{
public:
    EchoServer(boost::asio::io_service &io_service, unsigned short port)
        : io_service_(io_service),
          acceptor_(io_service, tcp::endpoint(tcp::v4(), port))
    {
        doAccept();
    }
    void doAccept()
    {
        auto conn = std::make_shared<TCPConnection>(io_service_);
        acceptor_.async_accept(conn->socket(),
                               [this, conn](boost::system::error_code ec)
                               {
                                   if (!ec) {  conn->start();  }
                                   this->doAccept();
                               });
    }
    
private: 
    boost::asio::io_service &io_service_;
    tcp::acceptor acceptor_;
};

  


参考资料