多线程
线程
互斥锁
什么是互斥锁?
互斥锁用于保护共享资源,以确保在同一时间只有一个线程可以访问该资源,从而避免竞态条件。
函数原型
C
在C语言中,互斥锁的函数原型在头文件pthread.h中声明,常用的互斥锁函数有以下几个:
pthread_mutex_init:初始化互斥锁。
pthread_mutex_lock:加锁互斥锁。如果互斥锁已经被其他线程锁定,则当前线程会阻塞,直到获取到锁为止。
pthread_mutex_unlock:解锁互斥锁。
pthread_mutex_destroy:销毁互斥锁。
以下是互斥锁的函数原型和简要用法:
pthread_mutex_t:互斥锁类型。
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr):
该函数用于初始化互斥锁。mutex是指向互斥锁的指针,attr是指向互斥锁属性的指针,通常设为NULL表示使用默认属性。
int pthread_mutex_lock(pthread_mutex_t *mutex):
该函数用于加锁互斥锁。如果互斥锁已经被其他线程锁定,则当前线程会阻塞,直到获取到锁为止。
int pthread_mutex_unlock(pthread_mutex_t *mutex):
该函数用于解锁互斥锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex):
该函数用于销毁互斥锁。在释放互斥锁前,必须确保没有其他线程正在使用该互斥锁。
使用例子:
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex;
void* thread_function(void* arg) {
pthread_mutex_lock(&mutex); // 加锁
// 访问共享资源
printf("Thread is accessing the shared resource.\n");
pthread_mutex_unlock(&mutex); // 解锁
return NULL;
}
int main() {
pthread_t thread;
pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
pthread_create(&thread, NULL, thread_function, NULL); // 创建线程
// 等待线程结束
pthread_join(thread, NULL);
pthread_mutex_destroy(&mutex); // 销毁互斥锁
return 0;
}
C++
在C中,互斥锁的函数原型和用法与C语言中的互斥锁函数非常类似。C11 引入了标准库的std::mutex类,提供了一种更方便和类型安全的互斥锁实现。以下是std::mutex的函数原型和简要用法:
std::mutex:互斥锁类。
void std::mutex::lock():
该成员函数用于加锁互斥锁。如果互斥锁已经被其他线程锁定,则当前线程会阻塞,直到获取到锁为止。
void std::mutex::unlock():
该成员函数用于解锁互斥锁。
bool std::mutex::try_lock():
该成员函数尝试加锁互斥锁,如果互斥锁已经被其他线程锁定,则返回false,否则返回true。
使用例子:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void thread_function() {
mtx.lock(); // 加锁
// 访问共享资源
std::cout << "Thread is accessing the shared resource." << std::endl;
mtx.unlock(); // 解锁
}
int main() {
std::thread thread1(thread_function); // 创建线程
thread1.join(); // 等待线程结束
return 0;
}
什么是死锁?
死锁是多线程或多进程编程中一种常见的并发问题,它发生在两个或多个线程(或进程)相互持有对方所需的资源而无法继续执行的情况下。在死锁的情况下,所有的线程(或进程)都会被阻塞,无法继续进行,造成程序永远无法完成。
死锁通常发生在并发环境下,其中每个线程(或进程)试图获取一些资源并等待其他线程(或进程)释放它们所持有的资源。由于所有线程都在等待其他线程释放资源,而不愿意先释放自己持有的资源,导致了循环等待的情况。
死锁的四个必要条件是:
- 互斥(Mutual Exclusion):资源不能同时被多个线程(或进程)共享,一次只能由一个线程(或进程)占有。
- 请求与保持(Hold and Wait):线程(或进程)可以持有某个资源并请求其他资源,同时保持对已占有资源的持有状态。
- 不剥夺(No Preemption):资源不能被强制剥夺,只能在线程(或进程)使用完毕后自愿释放。
- 环路等待(Circular Wait):若干线程(或进程)之间形成一个环路,每个线程都在等待下一个线程所持有的资源。
死锁的代码例子:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1;
std::mutex mutex2;
void thread1_function() {
std::unique_lock<std::mutex> lock1(mutex1);
std::cout << "Thread 1 acquired mutex1" << std::endl;
// 等待一段时间,模拟处理过程
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock2(mutex2);
std::cout << "Thread 1 acquired mutex2" << std::endl;
// 释放锁
lock2.unlock();
lock1.unlock();
}
void thread2_function() {
std::unique_lock<std::mutex> lock2(mutex2);
std::cout << "Thread 2 acquired mutex2" << std::endl;
// 等待一段时间,模拟处理过程
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock1(mutex1);
std::cout << "Thread 2 acquired mutex1" << std::endl;
// 释放锁
lock1.unlock();
lock2.unlock();
}
int main() {
std::thread thread1(thread1_function);
std::thread thread2(thread2_function);
thread1.join();
thread2.join();
return 0;
}
在上述代码中,thread1_function和thread2_function分别表示两个线程的执行函数。每个线程首先获取一个互斥锁,然后休眠一段时间模拟处理过程。接着,它试图获取另一个互斥锁。由于两个线程都试图获取对方已持有的互斥锁,它们将会永远等待,导致死锁。
条件变量
信号量
代码实战
交替打印奇数和偶数
C
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex;
pthread_cond_t condition;
int counter = 0;
const int MAX_COUNT = 10;
// 奇数线程函数
void* odd_printer(void* arg) {
while (counter < MAX_COUNT) {
pthread_mutex_lock(&mutex);
while (counter % 2 == 0) {
pthread_cond_wait(&condition, &mutex);
}
printf("Odd: %d\n", counter++);
pthread_cond_signal(&condition);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
// 偶数线程函数
void* even_printer(void* arg) {
while (counter < MAX_COUNT) {
pthread_mutex_lock(&mutex);
while (counter % 2 == 1) {
pthread_cond_wait(&condition, &mutex);
}
printf("Even: %d\n", counter++);
pthread_cond_signal(&condition);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t odd_thread, even_thread;
// 初始化互斥锁和条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condition, NULL);
// 创建奇数和偶数线程并启动
pthread_create(&odd_thread, NULL, odd_printer, NULL);
pthread_create(&even_thread, NULL, even_printer, NULL);
// 等待线程结束
pthread_join(odd_thread, NULL);
pthread_join(even_thread, NULL);
// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&condition);
return 0;
}
代码分析:
初始值counter = 0
,因此先执行odd_thread
会触发pthread_cond_wait(&condition, &mutex);
,因而释放mutex,将线程挂起。此时,even_thread
立即获取锁,因为不满足条件,会顺序执行到pthread_cond_signal(&condition);
此时会重新唤醒odd_thread
,pthread_mutex_unlock(&mutex);
后,odd_thread
能够重新获取锁,使得程序只能继续从pthread_cond_wait(&condition, &mutex);
开始执行下面的代码,设置
while (counter % 2 == 0) {
pthread_cond_wait(&condition, &mutex);
}
是为了进一步确认是否真的满足了唤醒的条件。
一般会采用
pthread_mutex_lock(&mutex);
while (!condition_is_met) {
pthread_cond_wait(&condition, &mutex);
}
// 等待条件满足后,继续执行需要的操作
来确认条件满足。
之后循环上面的过程,交替打印奇数和偶数。
C++
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
int counter = 0;
const int MAX_COUNT = 10;
void odd_printer() {
while (counter < MAX_COUNT) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return counter % 2 == 1; });
std::cout << "Odd: " << counter << std::endl;
counter++;
cv.notify_one();
}
}
void even_printer() {
while (counter < MAX_COUNT) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return counter % 2 == 0; });
std::cout << "Even: " << counter << std::endl;
counter++;
cv.notify_one();
}
}
int main() {
std::thread t1(odd_printer);
std::thread t2(even_printer);
t1.join();
t2.join();
return 0;
}
std::unique_lock
C中,std::unique_lock<std::mutex> lock(mutex);
和mutex.lock();
都用于在C中获取互斥锁,两者之间有一些区别。
-
RAII(资源获取即初始化)语法:
std::unique_lock是C++11引入的一个类模板,使用RAII(资源获取即初始化)语法。它在构造时获取互斥锁,并在析构时自动释放互斥锁。这样可以确保在作用域结束时自动释放锁,避免忘记手动调用unlock()导致死锁或其他问题。而mutex.lock();是直接调用互斥锁的成员函数lock(),需要在适当的地方手动调用unlock()来释放锁。 -
灵活性:
std::unique_lock提供了更多的灵活性。它可以在构造时选择是否立即获取锁,以及是否在析构时自动释放锁。例如,可以使用std::defer_lock参数延迟获取锁,然后在适当的时候手动调用lock()来获取锁。而mutex.lock();是直接获取锁,没有这些选项。 -
性能:
在构造std::unique_lock对象时,会带来一些额外的开销,例如内存分配。因此,如果只需要简单地获取和释放锁,并不需要额外的灵活性,直接使用mutex.lock();可能会稍微更快一些。 -
异常安全:
使用std::unique_lock更容易实现异常安全。如果在获取锁之后发生异常,由于std::unique_lock的析构函数会自动释放锁,所以不会发生资源泄漏。而使用mutex.lock();时,如果在获取锁后发生异常,并且忘记在适当的地方调用unlock(),就可能导致锁没有释放,从而造成死锁或资源泄漏。
std::condition_variable::wait
函数原型:
template<class Predicate>
void std::condition_variable::wait(std::unique_lock<std::mutex>& lock, Predicate pred);
参数解释:
- lock:一个指向互斥锁(std::mutex)的std::unique_lock对象。在调用该函数前,必须先通过lock()成员函数获得互斥锁,否则将导致编译错误或运行时异常。
- pred:一个可调用对象(函数、函数对象或Lambda表达式),用于定义等待的条件。当pred返回false时,当前线程将进入等待状态。当条件满足并且其他线程调用了notify_one()或notify_all()通知等待的线程时,线程将从等待状态被唤醒,继续执行。
函数原理:
- 线程在调用cv.wait(lock, pred)时,首先获取由lock指向的互斥锁,确保其他线程不能同时修改共享数据。
- 然后,它检查pred返回的条件是否满足,如果满足,则不需要等待,直接继续执行。如果条件不满足(即pred返回false),则线程会释放互斥锁,并进入等待状态。
- 当线程进入等待状态后,它会自动释放互斥锁,这样其他线程可以继续访问共享数据。
- 当其他线程对共享数据做出修改,并且满足了条件时,它会调用cv.notify_one()或cv.notify_all()来通知等待的线程。
- 一旦线程被唤醒,它会重新获取互斥锁,然后继续检查条件是否满足。如果条件仍然不满足,线程会再次进入等待状态,直到满足条件后才继续执行。
cv.wait(lock, [] { return counter % 2 == 0; });
实现的功能和
while (counter % 2 == 0) {
pthread_cond_wait(&condition, &mutex);
}
是一样的。
C++的代码逻辑和C代码是一样的。
拓展:Lambda表达式
Lambada表达式的一般形式如下:
[ captures ] ( parameters ) -> return_type {
// 函数体
}
- captures:捕获列表,用于在lambda表达式内部访问外部变量。可以为空或使用[=]表示以传值方式捕获所有外部变量,或使用[&]表示以引用方式捕获所有外部变量,也可以根据需要指定特定的变量捕获方式。
parameters:函数参数列表,用于传递参数给lambda函数。
return_type:返回类型,用于指定lambda表达式的返回类型。可以省略,由编译器自动推断。
{}:函数体,用于定义lambda函数的实现。
示例:
#include <iostream>
int main() {
int x = 5;
int y = 10;
// 使用 lambda 表达式定义一个简单的函数对象
auto add = [](int a, int b) -> int {
return a + b;
};
// 调用 lambda 函数
int result = add(x, y);
std::cout << "Result: " << result << std::endl;
return 0;
}
拓展:operator()
在C++中,如果一个类重载了operator()函数,那么该类的对象就可以像函数一样被调用,就像调用普通函数一样使用对象后加上()运算符。这使得这些对象可以表现得像函数一样,因此被称为函数对象。
例如:
class FunctionObject {
public:
void operator()(int x) const {
std::cout << "Function Object: " << x << std::endl;
}
};
调用:
FunctionObject funcObj;
funcObj(42); // 调用 operator()(int),输出 "Function Object: 42"