天天看点

Linux多线程服务端编程 第三章 多线程服务器场合与常用编程模型

作者:明政面朝大海春暖花开

在Linux多线程服务端编程中,可以封装一些常用的同步机制,如互斥锁(MutexLock)、互斥锁保护(MutexLockGuard)和条件变量(Condition),以简化多线程编程的操作。

  1. MutexLock(互斥锁):封装了互斥锁的操作,提供了加锁(lock)和解锁(unlock)的方法。互斥锁用于保护临界区,确保只有一个线程可以访问共享资源。
#include <pthread.h>

class MutexLock {
public:
    MutexLock() {
        pthread_mutex_init(&mutex, NULL);
    }

    ~MutexLock() {
        pthread_mutex_destroy(&mutex);
    }

    void lock() {
        pthread_mutex_lock(&mutex);
    }

    void unlock() {
        pthread_mutex_unlock(&mutex);
    }

private:
    pthread_mutex_t mutex;
};
           
  1. MutexLockGuard(互斥锁保护):封装了互斥锁的加锁和解锁操作,通过构造函数和析构函数的方式,确保在作用域结束时自动释放互斥锁。
class MutexLockGuard {
public:
    explicit MutexLockGuard(MutexLock& mutex) : lock(mutex) {
        lock.lock();
    }

    ~MutexLockGuard() {
        lock.unlock();
    }

private:
    MutexLock& lock;
};
           

使用MutexLockGuard可以简化加锁和解锁的操作,例如:

MutexLock mutex;

void critical_section() {
    MutexLockGuard lock(mutex);
    // 这里是临界区,对共享资源的操作
    // 不需要手动加锁和解锁
}
           
  1. Condition(条件变量):封装了条件变量的操作,提供了等待(wait)、通知(notify)和广播(broadcast)的方法。条件变量用于线程间的等待和通知机制。
class Condition {
public:
    Condition() {
        pthread_cond_init(&cond, NULL);
    }

    ~Condition() {
        pthread_cond_destroy(&cond);
    }

    void wait(MutexLock& mutex) {
        pthread_cond_wait(&cond, &mutex.mutex);
    }

    void notify() {
        pthread_cond_signal(&cond);
    }

    void notifyAll() {
        pthread_cond_broadcast(&cond);
    }

private:
    pthread_cond_t cond;
};
           

使用Condition可以实现线程间的等待和通知,例如:

MutexLock mutex;
Condition cond;

void wait_for_data() {
    MutexLockGuard lock(mutex);
    while (!data_ready) {
        cond.wait(mutex);
    }
    // 条件满足,继续执行
}

void notify_data_ready() {
    MutexLockGuard lock(mutex);
    data_ready = true;
    cond.notify();
}
           

通过封装MutexLock、MutexLockGuard和Condition,可以简化多线程编程中的同步操作,提高代码的可读性和可维护性。同时,封装后的接口也更加安全,可以避免一些常见的错误,如忘记解锁等。

在Linux多线程服务端编程中,实现线程安全的Singleton模式是一种常见需求。Singleton模式用于确保一个类只有一个实例,并提供一个全局访问点。在多线程环境下,需要保证Singleton的实现是线程安全的,即多个线程同时访问时也能保证只有一个实例被创建。

下面是一个线程安全的Singleton实现的示例:

class Singleton {
public:
    static Singleton& getInstance() {
        // 使用双重检查锁定来确保线程安全
        if (instance == nullptr) {
            MutexLockGuard lock(mutex); // 加锁
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return *instance;
    }

private:
    Singleton() {} // 私有构造函数,禁止外部创建实例
    Singleton(const Singleton&) = delete; // 禁止拷贝构造函数
    Singleton& operator=(const Singleton&) = delete; // 禁止赋值运算符

    static Singleton* instance;
    static MutexLock mutex;
};

Singleton* Singleton::instance = nullptr;
MutexLock Singleton::mutex;
           

在上述示例中,getInstance()方法使用双重检查锁定(double-checked locking)来保证线程安全。在第一次调用时,如果instance为空,则使用互斥锁进行加锁,然后再次检查instance是否为空,如果为空则创建实例。这样可以避免每次调用getInstance()都进行加锁操作,提高了性能。

需要注意的是,为了保证线程安全,需要使用互斥锁来保护instance的创建过程。在示例中,使用了MutexLockGuard来自动管理互斥锁的加锁和解锁操作,确保在任何情况下都能正确释放锁。

这样,无论多个线程同时调用getInstance()方法,都能保证只有一个实例被创建,并且线程安全。

在Linux多线程服务端编程中,sleep(3)函数不是同步原语。sleep(3)函数用于暂停当前线程的执行,让线程进入睡眠状态,等待指定的时间后再继续执行。但它并不能提供同步机制,不能保证在指定的时间后线程一定会被唤醒。

举个例子来说明sleep(3)不是同步原语的特点:

#include <iostream>
#include <thread>
#include <unistd.h>

void worker() {
    std::cout << "Worker thread starts." << std::endl;
    sleep(5); // 暂停当前线程5秒
    std::cout << "Worker thread resumes." << std::endl;
}

int main() {
    std::cout << "Main thread starts." << std::endl;
    
    std::thread t(worker); // 创建一个工作线程
    t.join(); // 等待工作线程结束
    
    std::cout << "Main thread finishes." << std::endl;
    
    return 0;
}
           

在上面的例子中,主线程创建了一个工作线程,并等待工作线程结束。工作线程在执行过程中调用了sleep(5),暂停了5秒。在这5秒内,工作线程处于睡眠状态,主线程也处于等待状态。当5秒时间到达后,工作线程会被唤醒,继续执行。最后,主线程打印出"Main thread finishes.",程序结束。

需要注意的是,sleep(3)函数的精度依赖于系统的调度策略和实现,不能保证在指定的时间后精确唤醒线程。因此,如果需要实现同步操作,应该使用其他同步原语,如互斥锁、条件变量等。

在Linux多线程服务端编程中,Copy-On-Write(COW)是一种优化技术,用于在多个线程共享数据时减少数据的拷贝开销。借助C++中的std::shared_ptr,可以实现Copy-On-Write。

Copy-On-Write的基本思想是,当多个线程共享同一个数据时,只有在其中一个线程对数据进行修改时,才会进行实际的拷贝操作,其他线程仍然共享原始数据。这样可以避免不必要的数据拷贝,提高性能和效率。

下面是一个使用std::shared_ptr实现Copy-On-Write的示例:

#include <iostream>
#include <memory>
#include <string>

class SharedData {
public:
    SharedData(const std::string& data) : data_(data) {}

    std::string getData() const {
        return data_;
    }

    void setData(const std::string& newData) {
        // 当有多个线程共享数据时,只有在写操作时才进行实际的拷贝
        if (!data_.unique()) {
            std::cout << "Copy-On-Write: Making a copy of data." << std::endl;
            data_ = *data_.get(); // 实际的拷贝操作
        }
        data_ = newData;
    }

private:
    std::shared_ptr<std::string> data_;
};

int main() {
    SharedData shared("Hello");

    std::cout << "Shared data: " << shared.getData() << std::endl;

    shared.setData("World"); // 修改数据

    std::cout << "Shared data: " << shared.getData() << std::endl;

    return 0;
}
           

在上述示例中,SharedData类中的数据成员data_使用了std::shared_ptr来管理。当调用setData()方法修改数据时,首先通过调用unique()函数判断是否有多个线程共享数据,如果是,则进行实际的拷贝操作,然后再进行修改。这样,在没有写操作时,多个线程仍然共享同一个数据,避免了不必要的拷贝开销。

需要注意的是,使用std::shared_ptr实现Copy-On-Write时,需要在写操作时进行拷贝,这可能会导致一些性能开销。因此,Copy-On-Write适用于读多写少的场景,可以有效减少数据拷贝的次数,提高性能。

在Linux多线程服务端编程中,Copy-On-Write(COW)是一种优化技术,用于在多个线程共享数据时减少数据的拷贝开销。借助C++中的std::shared_ptr,可以实现Copy-On-Write。

Copy-On-Write的基本思想是,当多个线程共享同一个数据时,只有在其中一个线程对数据进行修改时,才会进行实际的拷贝操作,其他线程仍然共享原始数据。这样可以避免不必要的数据拷贝,提高性能和效率。

下面是一个使用std::shared_ptr实现Copy-On-Write的示例:

#include <iostream>
#include <memory>
#include <string>

class SharedData {
public:
    SharedData(const std::string& data) : data_(data) {}

    std::string getData() const {
        return data_;
    }

    void setData(const std::string& newData) {
        // 当有多个线程共享数据时,只有在写操作时才进行实际的拷贝
        if (!data_.unique()) {
            std::cout << "Copy-On-Write: Making a copy of data." << std::endl;
            data_ = *data_.get(); // 实际的拷贝操作
        }
        data_ = newData;
    }

private:
    std::shared_ptr<std::string> data_;
};

int main() {
    SharedData shared("Hello");

    std::cout << "Shared data: " << shared.getData() << std::endl;

    shared.setData("World"); // 修改数据

    std::cout << "Shared data: " << shared.getData() << std::endl;

    return 0;
}
           

在上述示例中,SharedData类中的数据成员data_使用了std::shared_ptr来管理。当调用setData()方法修改数据时,首先通过调用unique()函数判断是否有多个线程共享数据,如果是,则进行实际的拷贝操作,然后再进行修改。这样,在没有写操作时,多个线程仍然共享同一个数据,避免了不必要的拷贝开销。

需要注意的是,使用std::shared_ptr实现Copy-On-Write时,需要在写操作时进行拷贝,这可能会导致一些性能开销。因此,Copy-On-Write适用于读多写少的场景,可以有效减少数据拷贝的次数,提高性能。

在Linux多线程服务端编程中,进程和线程是两种不同的执行单元。

进程是操作系统中的一个独立执行单位,它拥有自己的地址空间、文件描述符、堆栈等资源。每个进程都是独立运行的,它们之间相互隔离,不能直接共享数据。进程之间通过进程间通信(IPC)机制进行数据交换和通信。

线程是进程内的一个执行流,它共享进程的地址空间和资源。多个线程可以同时执行,它们之间可以直接访问共享的数据,因此线程之间的通信和数据共享更加方便和高效。但是,线程之间的共享数据需要进行同步和互斥操作,以避免竞态条件和数据不一致的问题。

下面是一个简单的例子来说明进程和线程的区别:

#include <iostream>
#include <thread>
#include <unistd.h>

void threadFunc() {
    std::cout << "This is a thread." << std::endl;
    sleep(1);
}

int main() {
    std::cout << "This is a process." << std::endl;

    std::thread t(threadFunc); // 创建一个线程
    t.join(); // 等待线程结束

    return 0;
}
           

在上面的例子中,主进程创建了一个线程,并等待线程结束。主进程打印出"This is a process.",而线程打印出"This is a thread."。

需要注意的是,线程之间共享的数据需要进行适当的同步和互斥操作,以避免竞态条件和数据不一致的问题。在Linux多线程服务端编程中,常用的同步机制包括互斥锁、条件变量、原子操作等。

在Linux多线程服务端编程中,单线程服务器是一种常用的编程模型,它使用一个主线程来处理所有的客户端请求。虽然单线程服务器只有一个线程,但它可以通过非阻塞I/O和事件驱动的方式来处理多个客户端连接,从而实现并发处理。

单线程服务器的基本原理是通过使用非阻塞I/O和事件驱动来实现异步处理。主线程通过监听套接字上的事件,当有新的客户端连接请求到来时,主线程接受连接,并将连接套接字注册到事件循环中。然后,主线程通过事件循环不断地检查已注册的套接字上是否有可读或可写事件发生,如果有,则进行相应的处理。

下面是一个简单的单线程服务器的示例:

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

int main() {
    int listenSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (listenSocket == -1) {
        std::cerr << "Failed to create socket." << std::endl;
        return -1;
    }

    sockaddr_in serverAddr{};
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    serverAddr.sin_port = htons(8080);

    if (bind(listenSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
        std::cerr << "Failed to bind." << std::endl;
        close(listenSocket);
        return -1;
    }

    if (listen(listenSocket, 10) == -1) {
        std::cerr << "Failed to listen." << std::endl;
        close(listenSocket);
        return -1;
    }

    std::cout << "Server started, listening on port 8080." << std::endl;

    while (true) {
        int clientSocket = accept(listenSocket, nullptr, nullptr);
        if (clientSocket == -1) {
            std::cerr << "Failed to accept client connection." << std::endl;
            continue;
        }

        // 处理客户端请求
        std::cout << "Accepted client connection." << std::endl;

        // 关闭客户端连接
        close(clientSocket);
    }

    close(listenSocket);

    return 0;
}
           

在上述示例中,主线程创建了一个监听套接字,并将其绑定到本地的8080端口。然后,主线程通过accept函数接受客户端连接,并在接受到连接后进行相应的处理。在这个示例中,主线程只是简单地打印出接受到连接的消息,并关闭客户端连接。然后,主线程继续监听下一个客户端连接。

需要注意的是,单线程服务器在处理客户端请求时是按照顺序进行的,即一个请求处理完之后才会处理下一个请求。因此,如果某个请求的处理时间过长,会导致其他请求的等待时间增加,影响服务器的性能和响应速度。单线程服务器适用于请求处理时间较短且并发连接数较少的场景。如果需要处理大量的并发连接,可以考虑使用多线程或多进程服务器模型。

继续阅读