线程 & 进程
1 进程
2 线程
优势:
1 共享资源
2 调度基本单位. 虽与同一进程内其他线程共享部分地址空间,但也有自己的私有数据(栈/寄存器状态等),可以独立运行
3 响应性. 不影响其他进程
4 资源利用率
劣势:
1 编程考虑因素多. 比如临界区/互斥/同步等
2 同步复杂性. 资源共享设计较复杂
3 调试复杂. 可能出现非必现问题
4 健壮性差
2.1 线程保序
在C++中,线程的执行顺序是由操作系统调度的,并且通常是不能被程序员直接控制的。也就是说,你不能直接强制线程按照特定的顺序执行。然而,你可以使用某些同步机制来影响线程的执行顺序,或者确保某些操作在特定线程完成之前不会被其他线程执行。
以下是一些常用的同步机制,它们可以帮助你管理线程的执行顺序:
-
互斥锁(Mutexes)和锁定(Locks):
- 通过在代码的关键部分使用互斥锁,你可以确保同一时间只有一个线程能够执行这段代码。虽然这不能直接控制线程的执行顺序,但它可以防止数据竞争和其他并发问题。
-
条件变量(Condition Variables):
- 条件变量允许一个或多个线程等待某个条件成立,另一个线程则可以修改该条件并通知等待的线程。通过巧妙地使用条件变量,你可以控制线程之间的依赖关系,从而影响它们的执行顺序。
-
信号量(Semaphores):
- 信号量是一种更通用的同步机制,它允许你控制对共享资源的访问。虽然信号量主要用于限制对资源的并发访问,但你也可以用它来模拟简单的锁或条件变量,从而影响线程的执行顺序。
-
屏障(Barriers):
- 屏障是一种同步原语,它允许一组线程等待彼此都到达某个点后再继续执行。这可以用于确保一组相关的操作在所有相关线程中都完成后再继续后续操作。
-
Future 和 Promise:
- 在C++11及以后的版本中,你可以使用
std::future
和std::promise
来异步获取某个操作的结果。虽然这本身并不直接控制线程的执行顺序,但它可以帮助你组织代码,以便在一个线程中启动异步操作,并在另一个线程中等待其完成。
- 在C++11及以后的版本中,你可以使用
-
任务队列(Task Queues):
- 你可以使用任务队列来安排线程的执行顺序。通过将任务放入队列中,并让一个或多个工作线程从队列中取出任务并执行,你可以间接地控制线程的执行顺序。然而,这仍然依赖于操作系统对工作线程的调度。
-
优先级调度:
- 一些操作系统和线程库支持设置线程的优先级。虽然这不能保证特定的执行顺序(因为高优先级的线程仍然可能被低优先级的线程抢占),但它可以影响线程的执行顺序。然而,过度依赖优先级可能会导致复杂的调度问题和不可预测的行为。
请注意,过度依赖特定的线程执行顺序可能会导致代码难以理解和维护。在可能的情况下,最好编写不依赖于特定执行顺序的并发代码。
2.2 线程阻塞
sleep_for & sleep_until
#include <iostream>
#include <thread>
#include <mutex>
void ThreadFunc()
{
// sleep_until 指定线程阻塞到某一个时间点time_point类型, 之后解除阻塞
// sleep_for 指定线程阻塞一定时间长度duration类型, 之后解除阻塞
for (auto i = 0; i < 5; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 休眠后线程从阻塞态变为就绪态,就绪态抢到cpu时间片后,变为运行态
std::cout << "sub thread id: 0x" << std::hex << std::this_thread::get_id() << std::endl;
auto currTime = std::chrono::system_clock::now();
std::this_thread::sleep_until(currTime + std::chrono::seconds(6));
std::cout << "sub thread id: 0x" << std::hex << std::this_thread::get_id() << " sleep_until end" << std::endl;
}
}
int main()
{
std::cout << "main thread id: 0x" << std::hex << std::this_thread::get_id() << std::endl;
std::thread t(ThreadFunc);
t.join();
return 0;
}
yield
#include <iostream>
#include <thread>
#include <mutex>
void ThreadFunc()
{
// sleep_until 指定线程阻塞到某一个时间点time_point类型, 之后解除阻塞
// sleep_for 指定线程阻塞一定时间长度duration类型, 之后解除阻塞
for (auto i = 0; i < 5; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 休眠后线程从阻塞态变为就绪态,就绪态抢到cpu时间片后,变为运行态
std::cout << "sub thread id: 0x" << std::hex << std::this_thread::get_id() << std::endl;
// auto currTime = std::chrono::system_clock::now();
// std::this_thread::sleep_until(currTime + std::chrono::seconds(6));
// std::cout << "sub thread id: 0x" << std::hex << std::this_thread::get_id() << " sleep_until end" << std::endl;
}
}
void ThreadFunc2()
{
// yield 避免一个线程长时间占用cpu资源,使多线程处理能力下降,让当前线程主动放弃自己抢到的cpu资源
for (size_t i = 0; i < 100; ++i) {
std::this_thread::yield();
std::cout << "sub thread id: 0x" << std::hex << std::this_thread::get_id() << std::dec << " i: " << i << std::endl;
}
}
int main()
{
std::cout << "main thread id: 0x" << std::hex << std::this_thread::get_id() << std::endl;
std::thread t(ThreadFunc);
std::this_thread::sleep_for(std::chrono::seconds(1));
std::thread t2(ThreadFunc2);
t.join();
t2.join();
return 0;
}
2.3 线程基本操作
C
// 创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
// pthread_t 是 POSIX 线程(Pthreads)库中定义的一个数据类型,用于唯一标识一个线程
// join/detach
// 等待线程结束并回收线程的资源,防止类似“僵尸进程”的情况
int pthread_join(pthread_t thread, void** retval);
// 如果觉得join操作是一种负担的时候,可以使用pthread_detach
// 用于分离线程,当线程结束时,自动回收线程资源。
int pthread_detach(pthread_t thread);
// 退出
// 用于终止当前的线程,因为exit会终止整个进程,所以有了这个函数
void pthread_exit(void* retval);
2.4 同步
锁的使用会增加性能的开销,而且线程可能会变成串行执行,为了避免多余的性能开销,每次使用锁都应该避免将非临界区的资源加锁。
C
// 互斥锁
// 创建
// 静态加锁 (全局变量或静态进行初始化)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 动态初始化
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
// 线程加锁/释放锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
使用示例:
int x = 0;
// 初始化锁对象
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *func(void *args)
{
for (int i = 0; i < 1000; i++)
{
pthread_mutex_lock(&mutex);
++x;
pthread_mutex_unlock(&mutex);
}
pthread_exit(nullptr);
}
int main()
{
// 线程冲突演示
pthread_t pid1, pid2;
pthread_create(&pid1, nullptr, func, nullptr);
pthread_create(&pid2, nullptr, func, nullptr);
pthread_join(pid1, nullptr);
pthread_join(pid2, nullptr);
cout << "x = " << x << endl;
return 0;
}
死锁可能的场景:
1 循环调用
2 两线程需要的锁已经被提前占用
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
void *func(void *args)
{
int* cnt = (int*)args;
if(*cnt <= 0) return nullptr;
pthread_mutex_lock(&mtx); // 第二次递归时等待着线程解锁
std::cout << "func()" << std::endl;
--(*cnt);
func(args); // 递归进入下一层,但锁还没解锁。
pthread_mutex_unlock(&mtx); // 程序永远走不到这里。
return nullptr;
}
int main()
{
// 线程冲突演示
pthread_t pid;
int* cnt = new int(10);
pthread_create(&pid, nullptr, func, (void*)cnt);
pthread_join(pid, nullptr);
return 0;
}
2.4 线程挂起
线程挂起(Suspending a Thread)是操作系统或编程环境中对线程执行状态的一种控制手段。当一个线程被挂起时,它会被置于一种非运行状态,不参与CPU的时间片分配,直到它被显式地恢复执行。线程挂起的原因可能有很多,包括但不限于:
-
资源等待:线程可能需要等待某些资源(如I/O操作完成、锁释放等)变得可用。在这种情况下,操作系统或运行时环境可能会选择挂起线程,以节省CPU资源。
-
优先级调度:在某些优先级调度的系统中,低优先级的线程可能会被挂起,以便高优先级的线程可以执行。
-
显式挂起:编程人员可以通过调用特定的API或函数来显式地挂起一个线程。这种机制通常用于同步或控制线程的执行顺序。
-
系统或应用需求:系统或应用可能需要在某些情况下暂停线程的执行,比如在进行系统维护、资源清理或响应外部事件时。
如何挂起和恢复线程
在不同的编程语言和操作系统中,挂起和恢复线程的方法会有所不同。
在Java中
Java并没有直接提供挂起(suspend)和恢复(resume)线程的API,因为这些操作可能会导致死锁或其他并发问题。相反,Java推荐使用wait()
和notify()
/notifyAll()
机制或Lock
和Condition
接口来控制线程间的协作。不过,Thread.suspend()
和Thread.resume()
方法确实存在,但它们已被标记为过时(deprecated),因为它们在多线程环境中可能导致不确定的行为。
在Windows API中
在Windows平台上,可以通过调用Win32 API中的SuspendThread
和ResumeThread
函数来挂起和恢复线程。然而,由于这些操作可能引入死锁和其他同步问题,Microsoft强烈建议不要在生产代码中使用它们。
在其他平台或语言中
大多数现代编程语言和操作系统都提供了更高级别的同步和并发控制机制,如信号量、互斥锁、条件变量等,这些机制比直接挂起和恢复线程更加安全和灵活。
结论
线程挂起是一个强大的功能,但它也带来了潜在的风险。在使用时,应该仔细考虑是否真的需要挂起线程,以及是否存在更安全、更合适的替代方案。在大多数情况下,通过设计合理的同步和并发控制机制,可以更有效地管理线程的执行,避免不必要的挂起和恢复操作。
N 参考资料
https://blog.csdn.net/CaTianRi/article/details/136658574