一、func
int n = 0;
void func1(int n);
void func2(int* n);
void func3(int& n);
void func1(int n);
-
接受一个整型变量
n
作为参数,只传递值。 -
n 是按值传递的,即函数内部对
n
的修改不会影响到调用者提供的变量。 -
调用:
func1(n);
// 函数内部对n
的修改{n++}不会改变 n 的值。
void func2(int*
n);
-
接受一个指向整型变量的指针
n
作为参数。 -
n 是按指针传递的,即函数内部可以通过指针访问和修改调用者提供的变量。
-
调用:
func2(&n);
// 函数内部对n
的修改{(*n)++}会改变 n 的值。
void func3(int& n);
-
接受一个整型变量的引用
n
作为参数。 -
n 是按引用传递的,即函数内部对
n
的修改会直接影响到调用者提供的变量。 -
一般调用:
func3(n);
// 函数内部对n
的修改{n++}会改变 n 的值。 -
特殊调用,当
原因:因为有些调用函数会认为传入func3
作为线程函数时, 传入值n
时要借助std::ref(n)
,如:thread t1(func1, std::ref(n))
。n
是按值传入,并不是引用所以要借助std::ref()
二、thread
- 有两个线程函数库
#include <thread>
#include <pthread.h>
1.thread线程创建,使用,传参
#include<iostream>
#include<thread>
using namespace std;
void func1(int* n){
while(1){
if(*n == 1) {
cout << "func1 end" << endl;
return;
}
}
}
void func2(int* n){
while(1) {
if(*n == 2){
cout << "func2 end" << endl;
return;
}
}
}
// 下面是对通过n对t1, t2线程的先后调用
int main(){
int n = 0;
thread t1(func1, &n); // 多个参数, t1(func, a, b, c);
thread t2(func2, &n);
cout << "input n: ";
cin >> n;
cout << "input n: ";
cin >> n;
t1.join(); // **detach() or join()**
t2.join();
return 0;
}
2.pthread线程创建,使用,传参
#include <iostream>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
// 定义信号量
sem_t semaphore; // 后面有讲到用法
// 线程函数1
void* threadFunction1(void* arg) {
while (true) {
// 等待信号量
sem_wait(&semaphore);
std::cout << "Thread 1 is running..." << std::endl;
sleep(1);
// 释放信号量
sem_post(&semaphore);
}
return nullptr;
}
// 线程函数2
void* threadFunction2(void* arg) {
while (true) {
// 等待信号量
sem_wait(&semaphore);
std::cout << "Thread 2 is running..." << std::endl;
sleep(1);
// 释放信号量
sem_post(&semaphore);
}
return nullptr;
}
int main() {
// 初始化信号量,初始值为1
sem_init(&semaphore, 0, 1);
// 创建线程1
pthread_t thread1;
pthread_create(&thread1, nullptr, threadFunction1, nullptr); // **NULL or nullptr**
// 创建线程2
pthread_t thread2;
pthread_create(&thread2, nullptr, threadFunction2, nullptr);
// 等待线程结束
pthread_join(thread1, nullptr);
pthread_join(thread2, nullptr);
// 销毁信号量
sem_destroy(&semaphore);
return 0;
}
3.thrread与pthread的区别
pthread_t thread1;
和 std::thread t1;
是两种不同的方式来创建线程,它们分别代表了不同的线程库和线程创建方式。
-
`pthread_t thread1':
-
pthread_t
是 POSIX 线程库中表示线程的数据类型,它是一个不透明的结构体类型,通常是一个整数或指针。 -
使用
pthread_create
函数创建线程时,需要提供一个pthread_t
类型的变量作为线程的标识符,用于存储新创建线程的信息。 -
pthread_create
函数的原型为int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
,其中thread
参数是一个指向pthread_t
类型的指针,用于接收新创建线程的标识符。
-
-
std::thread t1
:-
std::thread
是 C++11 引入的线程库中表示线程的类,它提供了更加方便和面向对象的线程创建和管理方式。 -
使用
std::thread
类创建线程时,只需简单地声明一个std::thread
类型的对象,并将要执行的函数或可调用对象作为参数传递给构造函数即可。 -
std::thread
类的构造函数根据参数的不同,可以创建一个新的线程并执行指定的函数或可调用对象。
-
主要区别:
-
pthread_t
是 C 语言的 POSIX 线程库提供的原生线程标识符类型,需要手动管理线程的创建、执行和销毁过程,较为底层。 -
std::thread
是 C++11 标准库提供的高级线程封装类,封装了线程的创建、执行和销毁过程,使用更加方便和安全,是更现代化的线程管理方式
4.join()与detach()的区别
-
join()
:-
join()
函数用于等待线程的执行完成,并阻塞当前线程直到被调用的线程执行完毕。 -
当调用
join()
函数时,当前线程会等待被调用的线程执行完毕后再继续执行。 -
一般情况下,主线程(即创建其他线程的线程)会调用
join()
函数等待其他线程执行完成,以确保程序的正确执行顺序和资源的正确释放。
-
-
detach()
:-
detach()
函数用于分离线程,使其成为守护线程(daemon thread),从而使其在后台运行,与主线程独立。 -
调用
detach()
函数后,当前线程将不再关心被调用的线程的状态,被调用的线程会在后台独立执行,当其执行完毕后自动释放资源。 -
一旦线程被分离,就无法再通过 join() 函数来等待其执行完毕,也无法再通过
detach()
函数重新将其加入到主线程。
-
主要区别:
-
使用
join()
函数可以等待线程执行完毕,并确保线程执行顺序的正确性,常用于需要等待线程执行结果的情况。 -
使用
detach()
函数可以将线程分离,使其独立运行,适用于不需要等待线程执行完毕的情况,如守护线程等。
5.NULL与nullptr的区别
在C++中,nullptr 和 NULL 都用于表示空指针,但它们是不同的:
nullptr
:
-
nullptr
是 C++11 引入的关键字,用于表示空指针常量。 -
nullptr
是一个具有特定类型的字面量,被定义为 nullptr_t 类型,可以隐式转换为任意指针类型,但不会发生与整数类型的隐式转换。 -
使用
nullptr
可以更明确地表示空指针,避免了在某些情况下发生类型转换或二义性的问题。
NULL
:
-
NULL
是 C 和 C++ 中用于表示空指针的宏,通常被定义为整数 0。 -
在 C 语言中,
NULL
只是一个预处理器宏,被定义为整数 0,用于表示空指针。在 C++ 中,NULL
通常也被定义为整数 0,但它的行为可能会因编译器或库的不同而有所差异。 -
在 C++11 之前,NULL 被用于表示空指针,但在 C++11 中推荐使用
nullptr
来代替。
总的来说,nullptr
是 C++11 中引入的一种更加安全和明确的表示空指针的方式,而 NULL
则是传统的 C/C++ 中用于表示空指针的宏。在新的 C++ 代码中,推荐使用 nullptr
来表示空指针。
三、mutex
1.分类
-
互斥锁(Mutex):
-
互斥锁是最常见的一种锁,用于实现对共享资源的互斥访问。
-
在 C++11 中,标准库提供了 std::mutex 类来实现互斥锁,可通过 lock()、try_lock()、unlock() 等成员函数来控制锁的获取和释放。
-
互斥锁的特点是当一个线程持有锁时,其他线程请求同一个锁会被阻塞,直到锁被释放。
-
-
读写锁(Read-Write Lock):
-
读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。
-
在 C++11 中,标准库并未提供原生的读写锁实现,但可以通过互斥锁和条件变量等结合使用来实现读写锁的功能。
-
-
自旋锁(Spin Lock):
-
自旋锁是一种忙等待锁,即线程在获取锁时不会被阻塞,而是循环地检查锁的状态直到获取到锁为止。
-
在 C++11 中,标准库并未提供原生的自旋锁实现,但可以通过原子操作和循环等待来实现简单的自旋锁。
-
-
递归锁(Recursive Lock):
- 递归锁允许同一个线程对锁进行多次加锁操作,而不会造成死锁。
在 C++11 中,标准库提供了 std::recursive_mutex 类来实现递归锁。
- 递归锁允许同一个线程对锁进行多次加锁操作,而不会造成死锁。
2.举例
#include<iostream>
#include<thread>
#include<atomic>
using namespace std;
void func(int& n){
for(int i = 0; i < 100000; i++) { // 循环次数尽可能大一些,以确保不会被CPU优化或存在偏差
++n;
}
}
int main(){
int n = 0;
thread t1(func, std::ref(n));
thread t2(func, std::ref(n));
t1.join();
t2.join();
cout << "n : " << n << endl;
return 0;
}
-
输出结果: n : 163459, n 不为200000,可能为随机值。
-
由于这两个线程同时对同一个变量 n 进行递增操作,存在数据竞争的问题,因为递增操作不是原子的。这意味着多个线程同时对 n 进行递增操作时,可能会导致竞态条件,结果是不确定的。
-
多核CPU中通常一个线程对应一个核;一个线程在取 n 时,另一个进程也可能同时取 n,CPU对其进行++操作,如n = 0, t1为n = 1,t2为n = 1,写回寄存器时n = 1,损失了一次计算;或t1 线程更快计算了两次n = 2, t2线程第一次还未运行完,当t2计算完后,n = 1,会覆盖掉原来n = 2的值,使n = 1.
-
单核CPU可能不会出现这种问题,因为单核CPU宏观上是并行的,微观上单行,是两个线程的交替进行,如果出现线程调度或阻塞情况可能最后的值不为200000,但偏差值不会很大,调度意味着CPU存储了线程信息包括参数值,会引起结果偏差,t1线程创建,获得n = 0,但t1未运行被调度出去了,t2创建,运行了n = 1,此时t1被唤醒,运行n = 1,最终结果n = 1,缺失了一次计算。
3.对n进行加锁操作
- 对n进行加锁,以确保两个线程不会同时计算
#include<iostream>
#include<thread>
#include<atomic>
using namespace std;
std::mutex mtx; // 锁
void func(int& n){
for(int i = 0; i < 100000; i++){
mtx.lock();
++n;
mtx.unlock();
}
}
int main(){
int n = 0;
thread t1(func, std::ref(n));
thread t2(func, std::ref(n));
t1.join();
t2.join();
cout << "n : " << n << endl;
return 0;
}
- 这次运行结果为: n : 200000
- n 变量进行上锁,以确保t1,t2可以并行的操作n。
四、atomic
1.举例
#include <iostream>
#include <thread>
#include <atomic>
// 无锁计数器
std::atomic<int> counter(0); // **counter{0} or counter(0)**
// counter = 0 C++11标准下错误
// 线程函数,无锁递增计数器
void threadFunction(int id) {
for (int i = 0; i < 100000; ++i) {
// 使用 std::atomic 的原子递增操作
counter++;
}
}
int main() {
// 创建两个线程
std::thread t1(threadFunction, 1);
std::thread t2(threadFunction, 2);
// 等待线程结束
t1.join();
t2.join();
std::cout << "counter: " << counter << std::endl;
return 0;
}
-
运行结果同样 counter 为 200000
-
相比较上一次加锁,atomic操作省去了加锁解锁操作。
-
atomic 为原子操作,原子操作是一种在多线程环境中用于确保对共享变量的操作是不可分割的操作。
-
注意C++11 atomic 禁用了拷贝构造 std::atomic counter = 0; 为错误写法
2.原子操作性质
-
原子性(Atomicity): 原子操作是不可分割的,要么完全执行,要么完全不执行。即使在多线程环境下,也不会发生线程切换或中断导致操作被中途打断的情况。
-
可见性(Visibility): 原子操作的结果对其他线程是可见的。当一个线程对原子变量进行修改后,其他线程能够立即看到变化。
-
顺序性(Ordering): 原子操作可以通过指定内存顺序来控制对共享变量的访问顺序。这样可以确保在多线程环境下,对共享变量的操作顺序是可控的,避免了内存乱序访问导致的问题。
3.counter{0} or counter(0)区别(尽量用大括号)
在C++中,std::atomic<int> counter(0);
和 std::atomic<int> counter{0};
在功能上是等效的,都是将 counter 初始化为0的原子整型。
-
圆括号 vs 大括号:
-
std::atomic<int> counter(0);
使用的是圆括号语法,被称为直接初始化。在C++11之前,这种初始化方式是唯一可用的。它直接调用了std::atomic<int>
的构造函数,将0作为参数传递给构造函数,以初始化counter
。 -
std::atomic<int> counter{0};
使用的是大括号语法,被称为列表初始化或统一初始化。这是C++11引入的新特性,提供了更严格和一致的初始化语法。它将0作为初始化器列表的一部分,然后根据std::atomic<int>
的构造函数重载,选择最适合的构造函数进行初始化。
-
-
构造函数调用:
- 实际上,在C++11标准之后,大括号语法{}会优先选择列表初始化,而不是直接初始化。因此,
std::atomic<int> counter{0};
实际上会调用std::atomic<int>
的构造函数,将0作为参数传递给构造函数进行初始化。这与圆括号语法()调用构造函数的效果相同。
- 实际上,在C++11标准之后,大括号语法{}会优先选择列表初始化,而不是直接初始化。因此,
总的来说,这两种初始化方式在功能上是等效的,都会将counter初始化为0。然而,大括号语法{}更为现代化和一致(圆括号 < 大括号),因为它同时适用于各种类型的初始化,并且可以避免一些潜在的歧义和错误。
五、semaphore
1. 举例:
#include <semaphore.h>
#include<iostream>
#include<thread>
sem_t semaphore;
void func(int *n){
for(int i = 0; i < 100000; i++) {
// 等待信号量
sem_wait(&semaphore);
(*n)++;
// 释放信号量
sem_post(&semaphore); // **sem_close(&semaphore) or sem_post(&semaphore)**
}
}
int main(){
int n = 0;
// 初始化信号量,初始值为1
sem_init(&semaphore, 0, 1);
// 第二个参数用于指定信号量的共享性,它可以取两个值:
//如果该参数的值为0,表示信号量是线程间共享的,即信号量可以被同一进程内的多个线程访问和修改。
//如果该参数的值为非0,表示信号量是进程间共享的,即信号量可以被不同进程中的线程访问和修改。
std::thread t1(func, &n);
std::thread t2(func, &n);
t1.join();
t2.join();
std::cout << "n : " << n << std::endl;
// 销毁信号量
sem_destroy(&semaphore);
return 0;
}
-
在初始化信号量时,初始值的选择通常取决于具体的应用场景和需求。将信号量初始化为1的常见做法是为了实现互斥访问,即确保同一时间只有一个线程能够访问共享资源。
-
当一个线程需要访问共享资源时,它首先调用 sem_wait 函数来等待信号量。如果信号量的值大于0(表示资源可用),则该线程可以继续执行,同时信号量的值减一。如果信号量的值等于0(表示资源已被其他线程占用),则该线程会被阻塞,直到有其他线程释放资源,使信号量的值变为大于0。
六、future 处理异步
1.举例
#include<iostream>
#include<future>
#include <mutex>
using namespace std;
std::mutex mt;
int func(int& n){
int i = 0;
for(i = 0; i < 10000; i++){
mt.lock();
n++;
mt.unlock();
}
return 1;
}
int main() {
int n = 0;
// 注意① 与 ②的传参方式不同,详细内容见开头func
std::future<int> future_result = std::async(std::launch::async, func, std::ref(n)); // ①
func(n); // ②
// func(std::ref(n)); 也是正确的
cout << future_result.get() << endl; //等待结束并获取结果
//future_result.wait(); 等待结束
cout << n << endl;
return 0;
}
- 在
C++
中,std::future
是用于处理异步任务的类,它提供了一种访问异步操作结果的方式。通过std::future
,可以在一个线程中启动一个任务,并在另一个线程中等待该任务的完成并获取其结果。
-
异步任务的启动和获取结果:
-
使用 std::async 函数可以启动一个异步任务,并返回一个 std::future 对象,用于获取该任务的结果。
-
std::future 的 get() 方法可以用来等待任务完成并获取其结果。
-
-
等待异步任务的完成:
- std::future 的 wait() 方法可以用来等待异步任务的完成,而不获取其结果。
七、promise
1.举例
#include<iostream>
#include<future>
#include<thread>
using namespace std;
void func(std::promise<int> &f){
f.set_value(1000); // 设置值
}
int main(){
std::promise<int> f;
std::future<int> funture_result = f.get_future();
std::thread t1(func, std::ref(f)); // 启动线程
t1.join();
cout << funture_result.get() << endl; //获取返回值
return 0;
}
- 在 C++ 中,
std::promise
是一种用于实现异步任务的机制,它可以用来在一个线程中设置值,而在另一个线程中获取这个值。std::promise
提供了一种将值或异常传递给与之相关联的std::future
的方式。
- 设置值或异常:
- 使用
std::promise
的set_value()
方法可以设置一个值,使用set_exception()
方法可以设置一个异常。 - 只能调用
set_value()
或set_exception()
中的一个,且只能调用一次。
- 使用
八、condition_variable
1.举例
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void thread_func() {
std::unique_lock<std::mutex> lck(mtx); // 自动管理锁,调用mtx.lock()与调用mtx.unlock()
while (!ready) {
cv.wait(lck); // 等待条件发生
}
std::cout << "Thread is ready" << std::endl; // 真正线程开始运作
}
int main() {
std::thread t(thread_func);
{
std::lock_guard<std::mutex> lck(mtx); // 自动管理锁,调用mtx.lock()与调用mtx.unlock()
ready = true;
}
cv.notify_one(); // 唤醒线程
t.join();
return 0;
}
std::condition_variable
是C++
中用于多线程间条件同步的机制,它允许一个线程等待另一个线程满足特定条件后再继续执行。通常与 std::mutex 一起使用,用于实现线程的等待和唤醒操作。
2. condition_variable 的基本介绍和使用方法
-
等待条件的发生:
-
使用
std::condition_variable
的wait()
方法可以让线程等待条件的发生。 -
wait()
方法会自动释放与之关联的std::unique_lock<std::mutex>
,并将线程置于阻塞状态,直到其他线程调用notify_one()
或notify_all()
方法唤醒它。
-
-
唤醒等待线程:
-
使用
std::condition_variable
的notify_one()
方法可以唤醒一个等待线程,使用notify_all()
方法可以唤醒所有等待线程。 -
被唤醒的线程会尝试重新获取与之关联的
std::unique_lock<std::mutex>
,如果成功则继续执行,否则继续等待。
-
-
超时等待:
-
wait_for()
方法允许设置超时时间,如果超过指定时间仍未收到通知,则会自动返回。 -
wait_until()
方法允许设置等待直到指定的时间点。
-
3. 扩展:
-
std::unique_lock<std::mutex> lck(mtx);
-
创建了一个名为
lck
的std::unique_lock
对象,该对象关联了一个名为 mtx 的std::mutex
。这种构造函数调用会立即锁定(lock)该互斥量,因此在创建std::unique_lock
对象时,会自动调用mtx.lock()
来锁定mtx
。 -
std::unique_lock
是一个智能锁(smart lock)类,它提供了比直接使用std::mutex
更灵活的锁定和解锁方式。通过std::unique_lock
,可以将锁的生命周期与std::unique_lock
对象的生命周期绑定,从而避免手动管理锁的锁定和解锁。 -
在本例中,
std::unique_lock
对象lck
的构造函数调用会锁定mtx
,在std::unique_lock
对象lck
被销毁时(通常是超出其作用域时),会自动解锁mtx
。这种自动管理锁的生命周期方式可以避免忘记解锁互斥量而导致的死锁等问题。
-
-
std::lock_guard<std::mutex> lck(mtx);
-
这行代码创建了一个名为
lck
的std::lock_guard
对象,该对象关联了一个名为mtx
的std::mutex
。这种构造函数调用会立即锁定(lock)该互斥量,因此在创建std::lock_guard
对象时,会自动调用mtx.lock()
来锁定mtx
。 -
std::lock_guard
是一个在构造时锁定互斥量,在析构时解锁互斥量的类,它实现了 RAII(资源获取即初始化)的概念,可以确保在作用域结束时互斥量被正确地解锁,从而避免了手动管理锁的锁定和解锁。 -
在本例中,
std::lock_guard
对象lck
的构造函数调用会锁定mtx
,在std::lock_guard
对象lck
被销毁时(通常是超出其作用域时),会自动解锁 mtx。这种自动管理锁的生命周期方式可以避免忘记解锁互斥量而导致的死锁等问题。
-
区别:
-
所有权:
-
std::lock_guard
在构造时会锁定互斥量,并在析构时自动解锁,因此它没有所有权的概念,一旦构造成功,它就拥有了对互斥量的独占访问权,直到它被销毁为止。 -
std::unique_lock
具有更灵活的所有权,它可以在构造时锁定互斥量,也可以在后续使用lock()
和unlock()
方法手动控制互斥量的锁定和解锁,因此它可以在需要时释放互斥量,然后重新锁定。
-
-
灵活性:
-
std::unique_lock
提供了更多的灵活性,它可以在构造时选择是否锁定互斥量,也可以在后续根据需要多次锁定和解锁互斥量。 -
std::lock_guard
在构造时必须锁定互斥量,并且无法手动解锁,只能在析构时自动解锁,因此它相对更加简单。
-
-
性能:
-
std::lock_guard
的实现比std::unique_lock
更加轻量级,因此在只需要简单的锁定和解锁操作时,性能可能会更好。 -
std::unique_lock
的实现更加复杂,因为它需要支持手动控制锁定和解锁,因此在性能要求较高的场景下可能会略逊于std::lock_guard
。
-
九、packaged_task
1.举例
#include<iostream>
#include<future>
#include<thread>
using namespace std;
int func(){
int i = 0;
for(i = 0; i < 1000; i++){
i++;
}
return i;
}
int main(){
std::packaged_task<int()> task(func); // int() 括号里为传参
std::future<int> future_result = task.get_future();
// ① 与 ② 同时运行
// 在另一个线程中执行任务
std::thread t1(std::move(task)); // ①
cout << func() << endl; // ②
t1.join();
cout << future_result.get() << endl; // 得到返回值
return 0;
}
-
std::packaged_task
是C++
中用于封装可调用对象(函数、函数对象或Lambda
表达式)的类,可以将可调用对象与std::future
结合起来,实现异步调用并获取返回值的功能。 -
使用
std::packaged_task
可以将任务封装为一个可调用对象,并将其结果传递给一个std::future
对象,使得可以在一个线程中执行任务,并在另一个线程中等待任务的完成并获取其结果。
十、cal_once 补充
单例设计模式是一种常见的设计模式,用于确保某个类只能创建一个实例。由于单例实例是全局唯一的,因此在多线程环境中使用单例模式时,需要考虑线程安全的问题。
下面是一个简单的单例模式的实现(打印日志例子):
`std::call_once` 的作用是,确保在多个线程中同时调用 `call_once` 时,只有一个线程能够成功执行 `func` 函数,而其他线程则会等待该函数执行完成。
#include<iostream>
#include<thread>
#include<string>
#include<mutex>
class Log;
static Log* log = nullptr;
static std::once_flag once;
class Log{
public:
Log() {}
Log(const Log& log) = delete;
Log& operator=(const Log& log) = delete;
static Log& GetInstance() {
/*static Log log; //懒汉模式
return log;*/
//static Log* log = nullptr; //饿汉模式
if(!log)
log = new Log;
std::call_once(once, init);
return *log;
}
static void init(){
if(!log) log = new Log;
}
void PrintLog(std::string msg){
std::cout << __TIME__ << " " <<msg << std::endl;
}
};
void print(){
Log::GetInstance().PrintLog("error");
}
int main(){
std::thread t1(print);
std::thread t2(print);
t1.join();
t2.join();
return 0;
}
这个九不过多解释,不懂的自己搜,问ai。
十一、c++多线程有练手的项目
Web服务器:开发一个简单的多线程Web服务器,可以处理并发的HTTP请求,包括GET和POST请求。
文件搜索工具:开发一个多线程的文件搜索工具,可以递归地搜索指定目录下的文件,并支持按文件名、文件类型等条件进行搜索。
并行计算工具:实现一个多线程的并行计算工具,可以利用多核处理器并行计算复杂的数学运算或算法。
生产者-消费者模型:实现一个生产者-消费者模型,用多线程实现生产者生成数据并存入缓冲区,消费者从缓冲区取出数据进行处理。
网络爬虫:开发一个多线程网络爬虫,可以并发地从互联网上爬取网页内容,并提取其中的链接或数据。
图像处理工具:实现一个多线程的图像处理工具,可以并行地对图像进行处理,比如图像滤波、边缘检测等。
游戏开发:尝试使用多线程技术开发一个简单的游戏,比如迷宫游戏、飞机大战等,利用多线程实现游戏逻辑和渲染。
并行排序算法:实现一个多线程的并行排序算法,比如并行快速排序、并行归并排序等,用于对大型数据集进行排序。