C++、Linux多线程编程

线程的创建、退出函数和回收函数

在Linux系统上,可以使用POSIX线程库(pthread库)进行多线程编程。pthread库提供了一系列线程相关的函数,可以用于创建和管理线程、同步和互斥等操作。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

typedef struct {
  int a;
  double b;
} MyStruct;

void *my_function(void *arg) {
  //在堆内存中动态分配内存空间以存储MyStruct类型的结构体
  MyStruct *result = (MyStruct *)malloc(sizeof(MyStruct));
  if (result == NULL) {
    perror("malloc");
    pthread_exit(NULL);
  }

  result->a = 42;
  result->b = 3.14;

  pthread_exit(result); // 退出线程并传入结构体指针作为返回值
}

int main() {
  pthread_t my_thread;
  int status;

  status = pthread_create(&my_thread, NULL, my_function, NULL);
  if (status != 0) {
    printf("Error: pthread_create failed\n");
    return 1;
  }

  // 等待线程执行完成,并获取线程的返回值
  void *retval;
  pthread_join(my_thread, &retval);

  MyStruct *result = (MyStruct *)retval;
  printf("Thread result: a = %d, b = %f\n", result->a, result->b);

  free(result); // 释放结构体内存

  return 0;
}

线程分离

线程分离是多线程编程中的一个概念,当一个线程被分离后,其状态将无法被其他线程获取,但是系统会在该线程运行结束后自动回收其资源。

在C++中,你可以使用std::thread::detach函数来分离线程:

#include <iostream>
#include <thread>

void my_function() {
  std::cout << "Hello from my_function!" << std::endl;
}

int main() {
  std::thread my_thread(my_function);
  my_thread.detach(); // 分离线程

  return 0;
}

在这个例子中,我们创建了一个新线程并在其中执行my_function函数。然后,我们调用std::thread::detach函数来分离这个线程。一旦线程被分离,我们就不能再使用std::thread::join函数来等待其结束,否则会抛出异常。

在Linux的POSIX线程库中,你可以使用pthread_detach函数来分离线程:

#include <stdio.h>
#include <pthread.h>

void *my_function(void *arg) {
  printf("Hello from my_function!\n");
  pthread_exit(NULL);
}

int main() {
  pthread_t my_thread;
  int status;

  status = pthread_create(&my_thread, NULL, my_function, NULL);
  if (status != 0) {
    printf("Error: pthread_create failed\n");
    return 1;
  }

  pthread_detach(my_thread); // 分离线程

  return 0;
}

为什么使用线程分离,什么时候使用:

线程分离是多线程编程中的一个重要概念。当一个线程被创建后,它处于可结合(joinable)状态。在这种状态下,创建线程需要在新线程结束后调用join()来获取线程的退出状态,并回收线程的资源。如果没有调用join(),那么新线程结束后,部分资源(如线程的ID、内部的线程变量)不会被系统回收,这就会导致内存泄漏。这种未被回收的资源通常被称为僵尸线程。

然而,在某些情况下,我们可能并不关心线程的返回状态,也不希望主线程等待新线程结束。此时,我们可以将新线程设置为分离(detached)状态。在分离状态下,新线程结束后,系统会自动回收其占用的所有资源,不会留下僵尸线程。这就是线程分离的主要作用。

通常,在以下情况下,我们会使用线程分离:

  1. 当我们不关心线程的返回状态,也不希望主线程等待新线程结束时,可以将新线程设置为分离状态。这样,新线程结束后,系统会自动回收其资源。

  2. 当我们希望新线程在后台运行,并且长时间存在时,可以将新线程设置为分离状态。这样,新线程就可以独立于主线程运行,而主线程可以继续执行其他任务,而不需要等待新线程结束。

  3. 当我们创建了大量的短生命周期的线程,且不希望频繁地调用join()来回收线程资源时,可以将这些线程设置为分离状态。这样,这些线程在结束后,系统会自动回收其资源,可以避免因为忘记调用join()而导致的内存泄漏。

 分离线程的主要作用是让线程在后台运行,而主线程可以继续执行其他任务,不需要等待分离线程结束。分离线程在结束时,系统会自动回收其资源,包括线程ID、堆栈空间等。因此,在将线程设置为分离状态后,可以放心地让线程独立运行,而不需要调用join()函数来回收资源。

互斥锁(mutex)

是一种用于多线程编程中保护共享资源的同步原语。当多个线程需要访问共享资源(如全局变量、文件、数据库连接等)时,互斥锁可以确保在同一时间只有一个线程能够访问该资源,从而避免竞争条件和数据不一致。
 

你可以把互斥锁想象成一个房间里的钥匙。当一个线程需要使用房间里的资源时,它需要拿到钥匙(加锁),然后进入房间使用资源。在这期间,其他试图使用房间资源的线程会发现钥匙不在,所以它们必须等待(阻塞),直到拿到钥匙为止。当线程使用完房间资源后,它会把钥匙放回原处(解锁),以便其他线程可以拿到钥匙并使用房间资源。

当一个线程试图锁定一个已经被另一个线程锁定的互斥锁时,该线程会被阻塞,直到锁被释放。C++11中的互斥锁定义在<mutex>头文件中。

使用互斥锁的基本步骤如下:

  • 包含头文件<mutex>
  • 定义一个互斥锁对象(例如std::mutex mtx)
  • 当需要访问共享资源时,使用std::unique_lock或std::lock_guard锁定互斥锁对象
  • 在访问完共享资源后,锁会在离开作用域时自动释放
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx; // 定义互斥锁对象
int shared_data = 0; // 共享资源

void increment() {
    for (int i = 0; i < 10000; ++i) {
        std::unique_lock<std::mutex> lock(mtx); // 锁定互斥锁
        ++shared_data; // 访问共享资源
        lock.unlock(); // 释放互斥锁(也可以省略,离开作用域时会自动释放)
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Shared data: " << shared_data << std::endl; // 输出共享资源的值

    return 0;
}

unique_lock

unique_lock是一个更灵活的锁,它允许延迟锁定、尝试锁定以及条件变量等高级功能。它的使用方法与互斥锁类似,但需要将互斥锁对象作为参数传递给unique_lock构造函数。unique_lock的析构函数会自动释放锁。

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int shared_data = 0;

void increment() {
    for (int i = 0; i < 10000; ++i) {
        std::unique_lock<std::mutex> lock(mtx); // 使用unique_lock锁定互斥锁
        ++shared_data;
        lock.unlock();
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Shared data: " << shared_data << std::endl;

    return 0;
}

lock_guard

lock_guard是一个更简单的锁,它提供了自动管理的互斥锁。它的使用方法与unique_lock类似,但不支持延迟锁定、尝试锁定等高级功能。lock_guard的优点是简单易用,适用于简单的互斥场景。性能更好

std::mutex mtx;
int shared_data = 0;

void increment() {
    for (int i = 0; i < 10000; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // 使用lock_guard锁定互斥锁
        ++shared_data;
    }
}

注:

在多线程编程中,当多个线程需要访问共享资源时,为了保护数据的完整性,我们通常会使用互斥锁(mutex)来确保在同一时间只有一个线程可以访问或修改共享资源。然而,如果不正确地使用互斥锁,可能会引发一些问题,如死锁和性能问题。

  1. 死锁:死锁是指两个或多个线程在运行过程中,因为争夺资源而造成的一种互相等待的现象。如果系统资源充足,进程的资源请求都能得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。例如,线程A锁定了互斥锁1并试图锁定互斥锁2,而线程B锁定了互斥锁2并试图锁定互斥锁1,这样就会导致两个线程都在等待对方释放互斥锁,从而陷入死锁。

  2. 性能问题:如果一个程序过度依赖互斥锁,可能会导致线程频繁地阻塞和唤醒,从而降低并发性能。这是因为,每次锁定和解锁互斥锁都需要进行系统调用,这会带来一定的性能开销。此外,当一个线程锁定了互斥锁,其他试图锁定同一个互斥锁的线程将会被阻塞,这会导致CPU时间片的浪费。

为了避免这些问题,我们在设计多线程程序时,应尽量减少共享资源的使用,或使用其他同步原语来降低锁的使用。例如:

  • 尽量设计无共享资源的并行算法,使得每个线程都在自己的数据上工作,而不需要访问其他线程的数据。
  • 使用原子操作(atomic operations),原子操作是一种特殊类型的操作,它们在执行过程中不会被其他线程中断。原子操作可以用来实现无锁(lock-free)的并发数据结构。
  • 使用条件变量(condition variables)或信号量(semaphores),这些同步原语可以用来实现更复杂的同步场景,如生产者-消费者问题。

原子操作(atomic operations)

在多线程环境中,某个操作(如读取、修改或写入数据)在执行过程中不会被其他线程中断的操作。换句话说,原子操作要么完全执行,要么完全不执行。这种特性使得原子操作在多线程编程中非常有用,因为它们可以在不使用锁的情况下确保数据的一致性。

在C++11中,原子操作由<atomic>头文件提供,主要包括std::atomic模板类和一系列原子操作函数。std::atomic模板类为基本类型和自定义类型提供原子操作。原子操作函数主要用于实现各种原子操作,如原子加载、存储、修改和比较交换等。

使用原子操作可以实现无锁(lock-free)的并发数据结构,这些数据结构在多线程环境下可以实现高效、可扩展的并发访问。无锁数据结构相比于使用互斥锁的数据结构,具有更低的同步开销和更高的并发性能。

#include <iostream>
#include <atomic>
#include <vector>
#include <thread>

std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 10000; ++i) {
        ++counter; // 原子操作
    }
}

int main() {
    std::vector<std::thread> threads;

    // 创建10个线程,每个线程递增计数器10000次
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }

    // 等待所有线程完成任务
    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Counter: " << counter << std::endl;

    return 0;
}

条件变量

条件变量是一种让线程在特定条件满足之前等待(暂停执行)的方法。当条件满足时,可以唤醒等待的线程继续执行。例如,在一个生产者-消费者问题中,生产者线程生产数据,消费者线程处理数据。当没有数据可处理时,消费者线程可以等待条件变量,直到生产者线程生产了新数据并通知条件变量,消费者线程才继续执行。C++11中的条件变量由std::condition_variable类表示,定义在<condition_variable>头文件中。

主要成员函数:

  • wait(std::unique_lock<std::mutex>& lock): 等待条件变量。这个函数会解锁互斥锁,并将当前线程阻塞,直到其他线程通知条件变量。当函数返回时,互斥锁会再次被锁定。

  • wait(std::unique_lock<std::mutex>& lock, Predicate pred): 等待条件变量,同时检查条件pred。这个函数会循环地检查条件pred,如果条件predfalse,则解锁互斥锁并阻塞当前线程;如果条件predtrue,则返回。

  • notify_one(): 通知一个等待的线程。如果有一个或多个线程正在等待条件变量,这个函数会唤醒其中一个线程。

  • notify_all(): 通知所有等待的线程。如果有一个或多个线程正在等待条件变量,这个函数会唤醒所有线程。

信号量

信号量是一种用于管理有限资源的方法。它是一个计数器,表示可用资源的数量。当线程需要使用资源时,会尝试减少信号量的计数值;当线程完成资源的使用后,会增加信号量的计数值。如果信号量的计数值为零,那么需要使用资源的线程将等待,直到其他线程释放资源。信号量可以用于实现多线程环境下的资源管理和同步。

以生产者-消费者问题为例,假设有一个工厂(生产者线程)生产产品,一个商店(消费者线程)销售产品。工厂生产产品并将产品放入仓库(队列)。商店从仓库中取出产品并销售。

  • 当仓库中没有产品时,商店需要等待,直到工厂生产了新产品。这时,可以使用条件变量让商店线程等待。当工厂生产了新产品并放入仓库时,通知条件变量,商店线程就可以继续执行,从仓库中取出产品并销售。
  • 当仓库的容量有限时,可以使用信号量来管理仓库的容量。信号量的计数值表示仓库中的可用空间。当工厂生产产品并放入仓库时,减少信号量的计数值;当商店从仓库中取出产品并销售时,增加信号量的计数值。如果仓库已满(信号量的计数值为零),工厂需要等待,直到商店取出产品并释放仓库空间。

#include <iostream>
#include <queue>
#include <thread>
#include <condition_variable>
#include <mutex>

std::queue<int> data_queue; // 数据队列,用于存储生产者生产的数据
std::mutex mtx; // 互斥锁,用于保护数据队列的访问
std::condition_variable cond; // 条件变量,用于同步生产者和消费者线程

void producer() {
    for (int i = 0; i < 10; ++i) {
        std::lock_guard<std::mutex> lock(mtx); // 使用lock_guard自动加锁,保护数据队列的访问
        data_queue.push(i); // 将生产的数据放入队列
        cond.notify_one(); // 通知一个等待的消费者线程
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx); // 使用unique_lock自动加锁,保护数据队列的访问
        cond.wait(lock, []{ return !data_queue.empty(); }); // 等待条件变量,直到队列不为空
        int data = data_queue.front(); // 从队列中取出数据
        data_queue.pop(); // 移除队列中的数据
        lock.unlock(); // 解锁互斥锁
        std::cout << "Consumed: " << data << std::endl; // 处理数据(打印)
        if (data == 9) break; // 如果处理完所有数据,退出循环
    }
}

int main() {
    std::thread t1(producer); // 创建生产者线程
    std::thread t2(consumer); // 创建消费者线程

    t1.join(); // 等待生产者线程完成
    t2.join(); // 等待消费者线程完成

    return 0;
}

读写锁

使用std::shared_mutex(C++17引入)来实现类似的功能。读写锁允许多个线程同时读取共享资源,但在写入共享资源时,只允许一个线程执行写操作,且在写操作期间不允许其他线程执行读操作。

#include <iostream>
#include <shared_mutex>
#include <mutex>
#include <thread>
#include <vector>

std::shared_mutex rw_mutex; // 读写锁
int shared_data = 0; // 共享数据

void reader(int id) {
    for (int i = 0; i < 5; ++i) {
        std::shared_lock<std::shared_mutex> lock(rw_mutex); // 以共享(读)模式锁定读写锁
        std::cout << "Reader " << id << " reads " << shared_data << std::endl;
        lock.unlock(); // 解锁读写锁
        //用于让当前线程暂停执行一段时间
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void writer(int id) {
    for (int i = 0; i < 5; ++i) {
        std::unique_lock<std::shared_mutex> lock(rw_mutex); // 以独占(写)模式锁定读写锁
        ++shared_data;
        std::cout << "Writer " << id << " writes " << shared_data << std::endl;
        lock.unlock(); // 解锁读写锁
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    std::vector<std::thread> threads;

    // 创建3个读线程
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(reader, i + 1);
    }

    // 创建2个写线程
    for (int i = 0; i < 2; ++i) {
        threads.emplace_back(writer, i + 1);
    }

    // 等待所有线程完成
    for (auto &t : threads) {
        t.join();
    }

    return 0;
}

线程池

线程池是一种基于池化概念的多线程处理形式。线程过多会造成系统性能下降,线程过少会造成系统资源利用率不高,线程池就是为了平衡这两种情况的发生。线程池会一次性创建一定数量的线程,放入空闲队列中。这些线程都是处于睡眠状态,也就是并未运行任何任务,不消耗CPU,而只是占用内存空间。在这个意义上,线程的数量可以大大超过CPU的数量。当请求到来时,需要创建新的线程时,会优先从线程池中取出空闲的线程,当处理完请求后,线程不会被销毁,而是再次返回到线程池中。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值