39、多线程应用与套接字编程全解析

多线程应用与套接字编程全解析

1. 多线程编程基础

1.1 wxSemaphore 详解

信号量(Semaphores)是一种将互斥锁(mutex)和计数器相结合的通用机制。与互斥锁不同的是,信号量可以从任何线程发出信号,而不仅仅是拥有它的线程,因此可以将信号量看作是一个没有所有者的计数器。

当一个线程调用信号量的 Wait 方法时,它会等待计数器变为正数,然后将计数器减 1 并返回。而调用 Post 方法则会将计数器加 1 并返回。

在 wxWidgets 中,信号量还有一个额外的特性:可以在构造时指定一个最大值,默认值为 0,表示“无限制”。如果指定了非零的最大值,并且某个线程在不恰当的时候调用 Post 方法,导致计数器的值超过最大值,就会产生 wxSEMA_OVERFLOW 错误。

下面通过“通用互斥锁”的例子来说明信号量的使用:
- 一个可以在不同线程中加锁和解锁的互斥锁可以使用初始计数为 1 的信号量来实现。 mutex.Lock 可以通过 semaphore.Wait 实现, mutex.Unlock 可以通过 semaphore.Post 实现。
- 第一个调用 Lock (即 Wait )的线程会发现信号量的值为正,将其减 1 后继续执行。
- 下一个调用 Lock 的线程会看到信号量的值为 0,必须等待某个线程(不一定是第一个线程)调用 Unlock (即 Post )。

1.2 wxWidgets 线程示例

在 wxWidgets 发行版的 samples/thread 目录中可以找到一个包含上述许多特性的工作示例。在这个示例中,可以启动、停止、暂停和恢复线程。它展示了一个“工作线程”,该线程会定期使用 wxPostEvent 向主线程发送事件,通过一个进度对话框来指示,当进度达到范围的末尾时会取消线程。

1.3 多线程替代方案

1.3.1 使用 wxTimer

wxTimer 类可以让应用程序周期性地接收通知,既可以是“单次触发”,也可以是重复触发。如果可以将任务分解为每隔几毫秒执行一次的小任务,并且给应用程序留出足够的时间来响应用户界面事件,那么可以使用 wxTimer 作为线程的替代方案。

可以选择不同的通知方式:
- 如果喜欢使用虚函数,可以从 wxTimer 派生一个类并覆盖 Notify 函数。
- 如果希望接收 wxTimerEvent 事件,可以将 wxEvtHandler 指针传递给定时器对象(在构造函数中或使用 SetOwner 方法),并使用 EVT_TIMER(id, func) 将定时器连接到事件处理函数。

还可以传递一个标识符来唯一标识定时器对象,然后将该标识符传递给 EVT_TIMER ,这在需要处理多个定时器对象时非常有用。

启动定时器可以调用 Start 方法,传递时间间隔(以毫秒为单位),如果只需要单次通知,可以传递 wxTIMER_ONE_SHOT 。调用 Stop 方法可以停止定时器,使用 IsRunning 方法可以确定定时器是否正在运行。

以下是一个使用事件处理程序的示例:

#define TIMER_ID 1000
class MyFrame : public wxFrame
{
public:
    ...
    void OnTimer(wxTimerEvent& event);
private:
    wxTimer m_timer;
};
BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_TIMER(TIMER_ID, MyFrame::OnTimer)
END_EVENT_TABLE()
MyFrame::MyFrame()
    : m_timer(this, TIMER_ID)
{
    // 1 秒间隔
    m_timer.Start(1000);    
}
void MyFrame::OnTimer(wxTimerEvent& event)
{
    // 在这里执行每秒要做的事情
}

需要注意的是,事件处理程序并不保证恰好每隔 n 毫秒被调用一次,实际的间隔取决于在处理定时器事件之前正在进行的其他处理。

另外, wxStopWatch 是一个用于测量时间间隔的有用类。构造函数会启动定时器,可以暂停和恢复它,并获取以毫秒为单位的已用时间。例如:

wxStopWatch sw;
SlowBoringFunction();
// 停止计时
sw.Pause();
wxLogMessage("The slow boring function took %ldms to execute", sw.Time());
// 恢复计时
sw.Resume(); 
SlowBoringFunction();
wxLogMessage("And calling it twice took %ldms in all", sw.Time());
1.3.2 空闲时间处理

应用程序还可以通过实现空闲事件处理程序来定期接收通知。当其他事件处理完成后,应用程序对象和所有窗口都会收到空闲事件。如果空闲事件处理程序调用 wxIdleEvent::RequestMore ,则会再次生成空闲事件;否则,直到下一批用户界面事件被处理后才会再次发送空闲事件。通常应该调用 wxIdleEvent::Skip 方法,以便调用基类的空闲处理程序。

以下是一个示例:

class MyFrame : public wxFrame
{
public:
    ...
    void OnIdle(wxIdleEvent& event);
    // 执行一小部分工作,如果任务完成则返回 true
    bool FinishedIdleTask();
};
BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_IDLE(MyFrame::OnIdle)
END_EVENT_TABLE()
void MyFrame::OnIdle(wxIdleEvent& event)
{
    // 进行空闲处理,如果任务未完成则请求更多空闲处理
    if (!FinishedIdleTask())
        event.RequestMore();
    event.Skip();
}

空闲事件处理并不局限于顶级窗口,任何窗口都可以拦截空闲事件。例如,可以实现一个图像显示自定义控件,只在空闲时间调整图像大小以适应窗口大小,避免窗口调整大小时出现闪烁。为了确保应用程序的空闲事件不会意外干扰控件的实现,可以在控件中覆盖虚函数 OnInternalIdle ,并在覆盖的函数中调用基类的 OnInternalIdle 方法。

有时候可能需要强制进行空闲事件处理,即使没有其他待处理的事件。可以使用 wxWakeUpIdle 函数来启动空闲事件处理,另一种方法是启动一个不执行任何工作的 wxTimer ,因为它会发送定时器事件,也会偶尔触发空闲事件处理。要立即处理所有空闲事件,可以调用 wxApp::ProcessIdle ,但需要注意这可能会影响内部空闲更新,具体取决于平台(在 GTK+ 上,窗口绘制是在空闲时间进行的)。

1.3.3 让出控制权

当应用程序忙于执行一个长时间的任务,导致用户界面冻结时,可以定期调用 wxApp::Yield (或其同义词 wxYield )来处理待处理的事件。但这种技术应该谨慎使用,因为它可能会导致一些不必要的副作用,例如重入问题。例如, Yield 可能会处理用户命令事件,导致任务在仍在执行时再次被执行。 wxSafeYield 函数会禁用所有窗口,让出控制权,然后再次启用窗口,以防止用户交互导致重入问题。如果将 true 传递给 wxApp::Yield ,它只会在当前没有处于让出状态时才让出控制权,这是减轻重入问题的另一种方法。

如果要定期更新特定的显示,可以尝试调用 wxWindow::Update 方法,它只会处理该窗口的待处理绘制事件。

1.4 多线程编程总结

多线程编程并不一定会让应用程序运行得更快(至少在没有特殊硬件的情况下),就像给收银员两条队伍而不是一条队伍并不能让她每小时处理更多的顾客一样。然而,多线程可以让用户感觉应用程序更快,因为用户界面更加响应式,就像收银员在处理一条队伍的同时等待另一条队伍的信用卡授权一样,多线程可以更有效地利用可用资源。它也可以是一种比单线程更优雅地解决某些问题的方法。

2. 套接字编程基础

2.1 套接字概述

套接字(Socket)是一种数据传输通道,它不关心传输的数据类型、数据的去向或来源,其目标是将数据从 A 点传输到 B 点。在浏览网页、查看电子邮件或登录即时通讯工具时都会使用到套接字。套接字的一个很棒的特点是,它可以用于连接任何支持套接字的设备,即使其中一个是计算机,另一个是冰箱!

套接字 API 最初是 BSD Unix 操作系统的一部分,由于其来源单一,已经成为了标准。所有现代操作系统都提供了套接字层,允许使用常见的协议(如 TCP 或 UDP)在网络(如互联网)上发送数据。使用 wxWidgets 的 wxSocket 类,可以可靠地在不同计算机之间传输任意数量的数据。

2.2 套接字类和功能概述

套接字操作的核心是 wxSocketBase ,它提供了发送和接收数据、关闭套接字、错误报告等基本功能。建立监听套接字或连接到服务器分别需要使用 wxSocketServer wxSocketClient wxSocketEvent 用于通知应用程序套接字上发生的事件。抽象类 wxSocketBase 及其子类(如 wxIPV4address )可以让你指定远程主机和端口。最后,像 wxSocketInputStream wxSocketOutputStream 这样的流类可以与其他流结合使用,在套接字上移动和转换数据。

在 wxWidgets 中,套接字可以以不同的方式运行,传统的线程套接字方法是禁用套接字事件并使用阻塞套接字调用。另一方面,可以启用套接字事件,从而消除对单独线程的需求;当套接字需要处理时,wxWidgets 会向应用程序发送事件。通过让数据在后台到达,并仅在有数据时进行处理,可以避免阻塞 GUI,也避免了为每个套接字创建单独线程的复杂性。

2.3 套接字编程示例

2.3.1 事件驱动的客户端/服务器示例

下面是一个简单的事件驱动的客户端/服务器示例,服务器监听连接,当有连接建立时,从客户端读取 10 个字符,然后将这些字符原样发送回客户端。客户端创建连接,发送 10 个字符,然后接收返回的 10 个字符。客户端发送的字符串在示例中被硬编码为 0123456789

2.3.2 客户端代码
BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_MENU(CLIENT_CONNECT, MyFrame::OnConnectToServer)
EVT_SOCKET(SOCKET_ID,    MyFrame::OnSocketEvent)
END_EVENT_TABLE()
void MyFrame::OnConnectToServer(wxCommandEvent& WXUNUSED(event))
{
    wxIPV4address addr;
    addr.Hostname(wxT("localhost"));
    addr.Service(3000);
    // 创建套接字
    wxSocketClient* Socket = new wxSocketClient();
    // 设置事件处理程序并订阅大多数事件
    Socket->SetEventHandler(*this, SOCKET_ID);
    Socket->SetNotify(wxSOCKET_CONNECTION_FLAG |
                      wxSOCKET_INPUT_FLAG |
                      wxSOCKET_LOST_FLAG);
    Socket->Notify(true);
    // 等待连接事件
    Socket->Connect(addr, false);
}
void MyFrame::OnSocketEvent(wxSocketEvent& event)
{
    // 发生事件的套接字
    wxSocketBase* sock = event.GetSocket();
    // 事件共享的公共缓冲区
    char buf[10];
    switch(event.GetSocketEvent())
    {
        case wxSOCKET_CONNECTION:
        {
            // 将数组填充为字符 0 到 9
            char mychar = '0';
            for (int i = 0; i < 10; i++)
            {
                buf[i] = mychar++;
            }
            // 向服务器发送字符
            sock->Write(buf, sizeof(buf));
            break;
        } 
        case wxSOCKET_INPUT:
        {
            sock->Read(buf, sizeof(buf));
            break;
        }
        // 服务器发送数据后断开连接
        case wxSOCKET_LOST:
        {
            sock->Destroy();
            break;
        }
    }
}
2.3.3 服务器代码
BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_MENU(SERVER_START, MyFrame::OnServerStart)
EVT_SOCKET(SERVER_ID,  MyFrame::OnServerEvent)
EVT_SOCKET(SOCKET_ID,  MyFrame::OnSocketEvent)
END_EVENT_TABLE() 
void MyFrame::OnServerStart(wxCommandEvent& WXUNUSED(event))
{
    // 创建地址 - 初始默认为 localhost:0
    wxIPV4address addr;
    addr.Service(3000);
    // 创建套接字。维护一个类指针以便关闭它
    m_server = new wxSocketServer(addr);
    // 使用 Ok() 检查服务器是否真的在监听
    if (! m_server->Ok())
    {
        return;
    }
    // 设置事件处理程序并订阅连接事件
    m_server->SetEventHandler(*this, SERVER_ID);
    m_server->SetNotify(wxSOCKET_CONNECTION_FLAG);
    m_server->Notify(true);
}
void MyFrame::OnServerEvent(wxSocketEvent& WXUNUSED(event))
{
    // 接受新连接并获取套接字指针
    wxSocketBase* sock = m_server->Accept(false);
    // 告诉新套接字如何和在哪里处理其事件
    sock->SetEventHandler(*this, SOCKET_ID);
    sock->SetNotify(wxSOCKET_INPUT_FLAG | wxSOCKET_LOST_FLAG);
    sock->Notify(true);
}
void MyFrame::OnSocketEvent(wxSocketEvent& event)
{
    wxSocketBase *sock = event.GetSocket();
    // 处理事件
    switch(event.GetSocketEvent())
    {
        case wxSOCKET_INPUT:
        { 
            char buf[10];
            // 读取数据
            sock->Read(buf, sizeof(buf));
            // 写回数据
            sock->Write(buf, sizeof(buf));
            // 完成套接字操作,销毁它
            sock->Destroy();
            break;
        }
        case wxSOCKET_LOST:
        {
            sock->Destroy();
            break;
        }
    }
}

2.4 连接到服务器

2.4.1 套接字地址

所有套接字地址类都派生自抽象基类 wxSockAddress ,为套接字方法提供了一个通用的参数类型,无论使用的地址协议是什么。 wxIPV4address 类提供了使用当前标准 Internet 地址方案 IPv4 指定远程主机所需的所有方法。 wxIPV6address 类部分实现,当 IPv6 更广泛使用时肯定会完成。

需要注意的是,当以无符号长整型表示地址时,期望的是网络字节序,并且总是返回网络字节序。网络字节序对应于大端字节序(Intel 或 AMD x86 架构是小端字节序,苹果的架构是大端字节序)。根据无符号长整型地址的存储或输入方式,可以使用字节序宏 wxINT32_SWAP_ON_LE ,它只会在小端字节序平台上交换字节序。

Hostname 方法可以接受一个 wxString 类型的字符串地址(例如 www.wxwidgets.org ),也可以接受一个 4 字节无符号长整型格式的 IP 地址(大端字节序)。如果不传递任何参数, Hostname 会返回当前指定主机的名称。 Service 方法用于设置远程端口,可以使用一个 wxString 描述一个知名端口,也可以使用一个无符号短整型表示任何端口。如果不传递任何参数, Service 会返回当前选择的端口号。 IPAddress 方法以点分十进制表示法返回远程主机的 wxString 表示。 AnyAddress 方法将地址设置为当前机器的任何地址,这与将地址设置为 INADDR_ANY 相同。

2.4.2 套接字客户端

wxSocketClient 类派生自 wxSocketBase ,继承了所有常见的套接字方法。客户端类添加的唯一方法是用于发起和建立与远程服务器连接的方法。

Connect 方法接受一个 wxSockAddress 参数,用于告诉套接字客户端连接的地址和端口。通常会使用 wxIPV4address 这样的类,而不是直接使用 wxSockAddress 。第二个参数是一个布尔值,默认为 true ,表示 Connect 调用应该阻塞直到连接建立。如果从主 GUI 线程调用,GUI 在连接时会阻塞。

如果 Connect 方法被告知不阻塞,可以使用 WaitOnConnect 方法。第一个参数是等待的秒数,第二个参数是等待的毫秒数。如果连接成功或明确失败(例如主机不存在),则返回 true

2.5 套接字编程总结

套接字编程是一种强大的技术,可以实现不同设备之间的数据传输。wxWidgets 提供的套接字类使得在高级应用程序中使用套接字变得容易,无需担心特定平台的实现或特性。虽然目前 wxWidgets 不支持使用 UDP 协议发送和接收数据报,但未来的版本可能会添加 UDP 功能。

通过上述的介绍和示例,我们了解了多线程编程和套接字编程的基础知识和实现方法。在实际应用中,可以根据具体的需求选择合适的编程方式,以提高应用程序的性能和响应性。

3. 多线程与套接字编程的综合应用

3.1 多线程在套接字编程中的应用场景

在套接字编程中,多线程可以发挥重要作用。例如,当服务器需要同时处理多个客户端连接时,使用多线程可以避免一个客户端的操作阻塞其他客户端的请求。以下是一个简单的多线程服务器示例,用于处理多个客户端连接:

#include <wx/wx.h>
#include <wx/socket.h>
#include <wx/thread.h>

class ClientThread : public wxThread
{
public:
    ClientThread(wxSocketBase* socket) : m_socket(socket) {}

    virtual ExitCode Entry()
    {
        char buf[10];
        if (m_socket->IsConnected())
        {
            m_socket->Read(buf, sizeof(buf));
            m_socket->Write(buf, sizeof(buf));
        }
        m_socket->Destroy();
        return (ExitCode)0;
    }

private:
    wxSocketBase* m_socket;
};

class MyFrame : public wxFrame
{
public:
    MyFrame() : wxFrame(nullptr, wxID_ANY, "Multi-threaded Socket Server")
    {
        wxIPV4address addr;
        addr.Service(3000);
        m_server = new wxSocketServer(addr);
        if (m_server->Ok())
        {
            m_server->SetEventHandler(*this, SERVER_ID);
            m_server->SetNotify(wxSOCKET_CONNECTION_FLAG);
            m_server->Notify(true);
        }
    }

    void OnServerEvent(wxSocketEvent& event)
    {
        wxSocketBase* sock = m_server->Accept(false);
        if (sock)
        {
            ClientThread* thread = new ClientThread(sock);
            thread->Create();
            thread->Run();
        }
    }

private:
    wxSocketServer* m_server;
    enum { SERVER_ID = 1 };
    DECLARE_EVENT_TABLE()
};

BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_SOCKET(SERVER_ID, MyFrame::OnServerEvent)
END_EVENT_TABLE()

class MyApp : public wxApp
{
public:
    virtual bool OnInit()
    {
        MyFrame* frame = new MyFrame();
        frame->Show(true);
        return true;
    }
};

IMPLEMENT_APP(MyApp)

在这个示例中,每当有新的客户端连接时,服务器会创建一个新的线程来处理该客户端的请求。这样,多个客户端可以同时与服务器进行通信,而不会相互干扰。

3.2 多线程与套接字编程的性能优化

在多线程和套接字编程中,性能优化是一个重要的问题。以下是一些优化建议:
- 减少锁的使用 :锁的使用会导致线程阻塞,降低性能。可以使用无锁数据结构或原子操作来减少锁的使用。
- 合理分配线程资源 :根据系统的 CPU 核心数和任务的特点,合理分配线程资源,避免创建过多的线程导致系统资源耗尽。
- 使用异步 I/O :在套接字编程中,使用异步 I/O 可以避免线程阻塞,提高系统的并发性能。

3.3 多线程与套接字编程的错误处理

在多线程和套接字编程中,错误处理是必不可少的。以下是一些常见的错误处理方法:
- 检查返回值 :在调用套接字和线程相关的函数时,检查返回值,判断函数是否执行成功。
- 捕获异常 :在多线程编程中,使用异常处理机制捕获可能的异常,避免程序崩溃。
- 日志记录 :记录程序运行过程中的错误信息,方便后续的调试和排查问题。

4. 总结与展望

4.1 多线程与套接字编程的总结

多线程编程和套接字编程是现代软件开发中非常重要的技术。多线程可以提高应用程序的并发性能和响应性,而套接字编程可以实现不同设备之间的数据传输。通过使用 wxWidgets 提供的相关类和方法,可以方便地实现多线程和套接字编程,同时避免了特定平台的实现细节。

4.2 未来发展趋势

随着计算机技术的不断发展,多线程和套接字编程也将不断发展。未来可能会出现更多的高性能、高并发的编程模型和框架,同时对异步 I/O 和分布式系统的支持也将更加完善。

4.3 学习建议

对于想要深入学习多线程和套接字编程的开发者,建议多阅读相关的书籍和文档,同时进行实践项目的开发。可以从简单的示例开始,逐步深入学习,掌握多线程和套接字编程的核心技术。

以下是一个简单的 mermaid 流程图,展示了多线程服务器处理客户端连接的流程:

graph TD;
    A[服务器启动] --> B[等待客户端连接];
    B --> C{有新连接?};
    C -- 是 --> D[创建新线程];
    D --> E[线程处理客户端请求];
    E --> F[关闭客户端连接];
    F --> B;
    C -- 否 --> B;

同时,为了更清晰地对比多线程和单线程在套接字编程中的优缺点,我们可以列出以下表格:

比较项 单线程 多线程
并发处理能力 低,一次只能处理一个客户端请求 高,可以同时处理多个客户端请求
资源占用 低,只需要一个线程的资源 高,需要多个线程的资源
编程复杂度 低,代码逻辑简单 高,需要处理线程同步和通信问题
响应性 低,容易出现阻塞 高,减少阻塞,提高响应速度

通过以上的总结和分析,我们可以更好地理解多线程和套接字编程的特点和应用场景,在实际开发中做出更合适的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值