thread 订阅
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。一个进程可以有很多线程,每条线程并行执行不同的任务。在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。 展开全文
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。一个进程可以有很多线程,每条线程并行执行不同的任务。在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。
信息
提出者
dozer
定    义
程序执行流的最小单元
多线程中
CPU的基本单位
应用学科
计算机
中文名
线程
外文名
thread
别    称
执行绪;轻量进程
线程发展简史
线程的引入:60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。因此在80年代,出现了能独立运行的基本单位——线程(Threads)。
收起全文
精华内容
下载资源
问答
  • thread
    万次阅读 多人点赞
    2021-07-17 19:40:26

    注:此教程以 Visual Studio 2019 Version 16.10.3 (MSVC 19.29.30038.1) 为标准,大多数内容参照cplusplus.com里的解释
    此文章允许转载,但请标明出处(https://blog.csdn.net/sjc_0910/article/details/118861539)

    前方高能:本文字数接近2万

    线程?进程?多线程?

    什么是多线程?

    百度百科中的解释:

    多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
    在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。

    进程与线程的区别

    定义:

    进程是正在运行的程序的实例,而线程是是进程中的实际运作单位。

    区别:

    • 一个程序有且只有一个进程,但可以拥有至少一个的线程。
    • 不同进程拥有不同的地址空间,互不相关,而不同线程共同拥有相同进程的地址空间。

    看了上述介绍,你应该明白进程与线程的区别了。什么,还不明白?下面这幅图应该能让你搞清楚:
    进程与线程的区别
    (自己画的图,不好看请见谅)

    C++11的std::thread

    在C中已经有一个叫做pthread的东西来进行多线程编程,但是并不好用 (如果你认为句柄、回调式编程很实用,那请当我没说),所以c++11标准库中出现了一个叫作std::thread的东西。

    std::thread常用成员函数

    构造&析构函数

    函数类别作用
    thread() noexcept默认构造函数创建一个线程,
    什么也不做
    template <class Fn, class… Args>
    explicit thread(Fn&& fn, Args&&… args)
    初始化构造函数创建一个线程,
    args为参数
    执行fn函数
    thread(const thread&) = delete复制构造函数(已删除)
    thread(thread&& x) noexcept移动构造函数构造一个与x
    相同的对象,会破坏x对象
    ~thread()析构函数析构对象

    常用成员函数

    函数作用
    void join()等待线程结束并清理资源(会阻塞)
    bool joinable()返回线程是否可以执行join函数
    void detach()将线程与调用其的线程分离,彼此独立执行(此函数必须在线程创建时立即调用,且调用此函数会使其不能被join)
    std::thread::id get_id()获取线程id
    thread& operator=(thread &&rhs)见移动构造函数
    (如果对象是joinable的,那么会调用std::terminate()结果程序)

    举个栗子

    例一:thread的基本使用

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    #include <thread>
    using namespace std;
    void doit() { cout << "World!" << endl; }
    int main() {
    	// 这里的线程a使用了 C++11标准新增的lambda函数
    	// 有关lambda的语法,请参考我之前的一篇博客
    	// https://blog.csdn.net/sjc_0910/article/details/109230162
    	thread a([]{
    		cout << "Hello, " << flush;
    	}), b(doit);
    	a.join();
    	b.join();
    	return 0;
    }
    

    输出结果:

    Hello, World!
    

    或者是

    World!
    Hello,
    

    那么,为什么会有不同的结果呢?
    这就是多线程的特色!

    多线程运行时是以异步方式执行的,与我们平时写的同步方式不同。异步方式可以同时执行多条语句。

    在上面的例子中,我们定义了2个thread,这2个thread在执行时并不会按照一定的顺序。打个比方,2个thread执行时,就好比赛跑,谁先跑到终点,谁就先执行完毕。

    例二:thread执行有参数的函数

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    #include <thread>
    using namespace std;
    void countnumber(int id, unsigned int n) {
    	for (unsigned int i = 1; i <= n; i++);
    	cout << "Thread " << id << " finished!" << endl;
    }
    int main() {
    	thread th[10];
    	for (int i = 0; i < 10; i++)
    		th[i] = thread(countnumber, i, 100000000);
    	for (int i = 0; i < 10; i++)
    		th[i].join();
    	return 0;
    }
    

    你的输出有可能是这样

    Thread 2 finished!Thread 3 finished!
    Thread 7 finished!
    Thread 5 finished!
    
    Thread 8 finished!
    Thread 4 finished!
    Thread 6 finished!
    Thread 0 finished!
    Thread 1 finished!
    Thread 9 finished!
    

    注意:我说的是有可能。你的运行结果可能和我的不一样,这是正常现象,在上一个例子中我们分析过原因。

    这个例子中我们在创建线程时向函数传递了一些参数,但如果要传递引用参数呢?是不是像这个例子中直接传递就行了?让我们来看看第三个例子:

    例三:thread执行带有引用参数的函数

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    #include <thread>
    using namespace std;
    template<class T> void changevalue(T &x, T val) {
    	x = val;
    }
    int main() {
    	thread th[100];
    	int nums[100];
    	for (int i = 0; i < 100; i++)
    		th[i] = thread(changevalue<int>, nums[i], i+1);
    	for (int i = 0; i < 100; i++) {
    		th[i].join();
    		cout << nums[i] << endl;
    	}
    	return 0;
    }
    

    如果你尝试编译这个程序,那你的编译器一定会报错

    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(55): error C2672: “std::invoke”: 未找到匹配的重载函数
    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(61): note: 查看对正在编
    译的函数 模板 实例化“unsigned int std::thread::_Invoke<_Tuple,0,1,2>(void *) noexcept”的引用
            with
            [
                _Tuple=_Tuple
            ]
    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(66): note: 查看对正在编 
    译的函数 模板 实例化“unsigned int (__cdecl *std::thread::_Get_invoke<_Tuple,0,1,2>(std::integer_sequence<size_t,0,1,2>) noexcept)(void *) noexcept”的引用
    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(89): note: 查看对正在编
    译的函数 模板 实例化“void std::thread::_Start<void(__cdecl &)(T &,T),int&,_Ty>(_Fn,int &,_Ty &&)”的引用
            with
            [
                T=int,
                _Ty=int,
                _Fn=void (__cdecl &)(int &,int)
            ]
    main.cpp(11): note: 查看对正在编译的函数 模板 实例化“std::thread::thread<void(__cdecl &)(T &,T),int&,int,0>(_Fn,int &,int &&)” 
    的引用
            with
            [
                T=int,
                _Fn=void (__cdecl &)(int &,int)
            ]
    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): error C2893: 未能使
    函数模板“unknown-type std::invoke(_Callable &&,_Ty1 &&,_Types2 &&...) noexcept(<expr>)”专用化
    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\type_traits(1589): note: 参见“std::invoke”的声明
    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: 用下列模板参 
    数:
    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: “_Callable=void (__cdecl *)(T &,T)”
    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: “_Ty1=int”   
    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): note: “_Types2={int}”
    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\thread(51): error C2780: “unknown-type std::invoke(_Callable &&) noexcept(<expr>)”: 应输入 1 个参数,却提供了 3 个
    E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\include\type_traits(1583): note: 参见“std::invoke”的声明
    

    这是怎么回事呢?原来thread在传递参数时,是以右值传递的:

    template <class Fn, class... Args>
    explicit thread(Fn&& fn, Args&&... args)
    

    划重点:Args&&... args
    很明显的右值引用,那么我们该如何传递一个左值呢?std::refstd::cref很好地解决了这个问题。
    std::ref 可以包装按引用传递的值。
    std::cref 可以包装按const引用传递的值。
    针对上面的例子,我们可以使用以下代码来修改:

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    #include <thread>
    using namespace std;
    template<class T> void changevalue(T &x, T val) {
    	x = val;
    }
    int main() {
    	thread th[100];
    	int nums[100];
    	for (int i = 0; i < 100; i++)
    		th[i] = thread(changevalue<int>, ref(nums[i]), i+1);
    	for (int i = 0; i < 100; i++) {
    		th[i].join();
    		cout << nums[i] << endl;
    	}
    	return 0;
    }
    

    这次编译可以成功通过,你的程序输出的结果应该是这样的:

    1
    2
    3
    4
    ...
    99
    100
    

    (中间省略了一堆数)

    注意事项

    • 线程是在thread对象被定义的时候开始执行的,而不是在调用join函数时才执行的,调用join函数只是阻塞等待线程结束并回收资源。
    • 分离的线程(执行过detach的线程)会在调用它的线程结束或自己结束时释放资源。
    • 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏。
    • 没有执行joindetach的线程在程序结束时会引发异常

    C++11中的std::atomic和std::mutex

    我们现在已经知道如何在c++11中创建线程,那么如果多个线程需要操作同一个变量呢?

    为什么要有atomic和mutex

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    #include <thread>
    using namespace std;
    int n = 0;
    void count10000() {
    	for (int i = 1; i <= 10000; i++)
    		n++;
    }
    int main() {
    	thread th[100];
    	// 这里偷了一下懒,用了c++11的foreach结构
    	for (thread &x : th)
    		x = thread(count10000);
    	for (thread &x : th)
    		x.join();
    	cout << n << endl;
    	return 0;
    }
    

    我的2次输出结果分别是:

    991164
    996417
    

    我们的输出结果应该是1000000,可是为什么实际输出结果比1000000小呢?
    在上文我们分析过多线程的执行顺序——同时进行、无次序,所以这样就会导致一个问题:多个线程进行时,如果它们同时操作同一个变量,那么肯定会出错。为了应对这种情况,c++11中出现了std::atomicstd::mutex

    std::mutex

    std::mutex是 C++11 中最基本的互斥量,一个线程将mutex锁住时,其它的线程就不能操作mutex,直到这个线程将mutex解锁。根据这个特性,我们可以修改一下上一个例子中的代码:

    例四:std::mutex的使用

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    #include <thread>
    #include <mutex>
    using namespace std;
    int n = 0;
    mutex mtx;
    void count10000() {
    	for (int i = 1; i <= 10000; i++) {
    		mtx.lock();
    		n++;
    		mtx.unlock();
    	}
    }
    int main() {
    	thread th[100];
    	for (thread &x : th)
    		x = thread(count10000);
    	for (thread &x : th)
    		x.join();
    	cout << n << endl;
    	return 0;
    }
    

    执行了好几次,输出结果都是1000000,说明正确。

    mutex的常用成员函数

    (这里用mutex代指对象

    函数作用
    void lock()将mutex上锁。
    如果mutex已经被其它线程上锁,
    那么会阻塞,直到解锁;
    如果mutex已经被同一个线程锁住,
    那么会产生死锁。
    void unlock()解锁mutex,释放其所有权。
    如果有线程因为调用lock()不能上锁而被阻塞,则调用此函数会将mutex的主动权随机交给其中一个线程;
    如果mutex不是被此线程上锁,那么会引发未定义的异常。
    bool try_lock()尝试将mutex上锁。
    如果mutex未被上锁,则将其上锁并返回true;
    如果mutex已被锁则返回false。

    std::atomic

    mutex很好地解决了多线程资源争抢的问题,但它也有缺点:太……慢……了……
    以例四为标准,我们定义了100个thread,每个thread要循环10000次,每次循环都要加锁、解锁,这样固然会浪费很多的时间,那么该怎么办呢?接下来就是atomic大展拳脚的时间了。

    例五:std::atomic的使用

    根据atomic的定义,我又修改了例四的代码:

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    #include <thread>
    // #include <mutex> //这个例子不需要mutex了
    #include <atomic>
    using namespace std;
    atomic_int n = 0;
    void count10000() {
    	for (int i = 1; i <= 10000; i++) {
    		n++;
    	}
    }
    int main() {
    	thread th[100];
    	for (thread &x : th)
    		x = thread(count10000);
    	for (thread &x : th)
    		x.join();
    	cout << n << endl;
    	return 0;
    }
    

    输出结果:1000000,正常

    代码解释

    可以看到,我们只是改动了n的类型(int->std::atomic_int),其他的地方一点没动,输出却正常了。
    有人可能会问了:这个std::atomic_int是个什么玩意儿?其实,std::atomic_int只是std::atomic<int>的别名罢了。
    atomic,本意为原子,官方 (我不确定是不是官方,反正继续解释就对了) 对其的解释是

    原子操作是最小的且不可并行化的操作。

    这就意味着即使是多线程,也要像同步进行一样同步操作atomic对象,从而省去了mutex上锁、解锁的时间消耗。

    std::atomic常用成员函数

    构造函数

    对,atomic没有显式定义析构函数

    函数类型作用
    atomic() noexcept = default默认构造函数构造一个atomic对象(未初始化,可通过atomic_init进行初始化)
    constexpr atomic(T val) noexcept初始化构造函数构造一个atomic对象,用val的值来初始化
    atomic(const atomic&) = delete复制构造函数(已删除)

    常用成员函数

    atomic能够直接当作普通变量使用,成员函数貌似没啥用,所以这里就不列举了,想搞明白的点这里 (英语渣慎入,不过程序猿中应该没有英语渣吧)

    C++11中的std::async

    注:std::async定义在future头文件中。

    为什么大多数情况下使用async而不用thread

    thread可以快速、方便地创建线程,但在async面前,就是小巫见大巫了。
    async可以根据情况选择同步执行或创建新线程来异步执行,当然也可以手动选择。对于async的返回值操作也比thread更加方便。

    std::async参数

    不同于thread,async是一个函数,所以没有成员函数。

    重载版本作用
    template <class Fn, class… Args>
      future<typename result_of<Fn(Args…)>::type>
        async (Fn&& fn, Args&&… args)
    异步或同步(根据操作系统而定)以args为参数执行fn
    同样地,传递引用参数需要std::refstd::cref
    template <class Fn, class… Args>
      future<typename result_of<Fn(Args…)>::type>
        async (launch policy, Fn&& fn, Args&&… args);
    异步或同步(根据policy参数而定(见下文))以args为参数执行fn,引用参数同上

    std::launch强枚举类(enum class)

    std::launch有2个枚举值和1个特殊值:

    标识符实际值(以Visual Studio 2019为标准)作用
    枚举值:launch::async0x1(1)异步启动
    枚举值:launch::deferred0x2(2)在调用future::get、future::wait时同步启动(std::future见后文)
    特殊值:launch::async | launch::defereed0x3(3)同步或异步,根据操作系统而定

    例六:std::async的使用

    暂且不管它的返回值std::future是啥,先举个例再说。

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    #include <thread>
    #include <future>
    using namespace std;
    int main() {
    	async(launch::async, [](const char *message){
    		cout << message << flush;
    	}, "Hello, ");
    	cout << "World!" << endl;
    	return 0;
    }
    

    你的编译器可能会给出一条警告:

    warning C4834: 放弃具有 "nodiscard" 属性的函数的返回值
    

    这是因为编译器不想让你丢弃async的返回值std::future,不过在这个例子中不需要它,忽略这个警告就行了。
    你的输出结果:

    Hello, World!
    

    不过如果你输出的是

    World!
    Hello,
    

    也别慌,正常现象,多线程嘛!反正我执行了好几次也没出现这个结果。

    C++11中的std::future

    我们已经知道如何使用async来异步或同步执行任务,但如何获得函数的返回值呢?这时候,async的返回值std::future就派上用场了。

    例七:使用std::future获取线程的返回值

    在之前的所有例子中,我们创建线程时调用的函数都没有返回值,但如果调用的函数有返回值呢?

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    // #include <thread> // 这里我们用async创建线程
    #include <future> // std::async std::future
    using namespace std;
    
    template<class ... Args> decltype(auto) sum(Args&&... args) {
    	// C++17折叠表达式
    	// "0 +"避免空参数包错误
    	return (0 + ... + args);
    }
    
    int main() {
    	// 注:这里不能只写函数名sum,必须带模板参数
    	future<int> val = async(launch::async, sum<int, int, int>, 1, 10, 100);
    	// future::get() 阻塞等待线程结束并获得返回值
    	cout << val.get() << endl;
    	return 0;
    }
    

    输出:

    111
    

    代码解释

    我们定义了一个函数sum,它可以计算多个数字的和,之后我们又定义了一个对象val,它的类型是std::future<int>,这里的int代表这个函数的返回值是int类型。在创建线程后,我们使用了future::get()来阻塞等待线程结束并获取其返回值。至于sum函数中的折叠表达式(fold expression),不是我们这篇文章的重点。

    std::future常用成员函数

    构造&析构函数

    函数类型作用
    future() noexcept默认构造函数构造一个空的、无效的future对象,但可以移动分配到另一个future对象
    future(const future&) = delete复制构造函数(已删除)
    future (future&& x) noexcept移动构造函数构造一个与x相同的对象并破坏x
    ~future()析构函数析构对象

    常用成员函数

    函数作用
    一般:T get()
    当类型为引用:R& future<R&>::get()
    当类型为void:void future::get()
    阻塞等待线程结束并获取返回值。
    若类型为void,则与future::wait()相同。
    只能调用一次。
    void wait() const阻塞等待线程结束
    template <class Rep, class Period>
      future_status wait_for(const chrono::duration<Rep,Period>& rel_time) const;
    阻塞等待rel_timerel_time是一段时间),
    若在这段时间内线程结束则返回future_status::ready
    若没结束则返回future_status::timeout
    若async是以launch::deferred启动的,则不会阻塞并立即返回future_status::deferred
    不知道std::chrono::duration的点这里

    std::future_status强枚举类

    见上文future::wait_for解释

    为啥要有void特化的std::future?

    std::future的作用并不只有获取返回值,它还可以检测线程是否已结束、阻塞等待,所以对于返回值是void的线程来说,future也同样重要。

    例八:void特化std::future

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    #include <future>
    using namespace std;
    void count_big_number() {
    	// C++14标准中,可以在数字中间加上单
    	// 引号 ' 来分隔数字,使其可读性更强
    	for (int i = 0; i <= 10'0000'0000; i++);
    }
    int main() {
    	future<void> fut = async(launch::async, count_big_number);
    	cout << "Please wait" << flush;
    	// 每次等待1秒
    	while (fut.wait_for(chrono::seconds(1)) != future_status::ready)
    		cout << '.' << flush;
    	cout << endl << "Finished!" << endl;
    	return 0;
    }
    

    如果你运行一下这个代码,你也许就能搞懂那些软件的加载画面是怎么实现的。

    C++11中的std::promise

    在上文,我们已经讲到如何获取async创建线程的返回值。不过在某些特殊情况下,我们可能需要使用thread而不是async,那么如何获得thread的返回值呢?
    如果你尝试这么写,那么你的编译器肯定会报错:

    std::thread th(func);
    std::future<int> return_value = th.join();
    

    还记得之前我们讲的thread成员函数吗?thread::join()的返回值是void类型,所以你不能通过join来获得线程返回值。那么thread里有什么函数能获得返回值呢?
    答案是:没有。
    惊不惊喜?意不意外?thread竟然不能获取返回值!难道thread真的就没有办法返回点什么东西吗?如果你真是那么想的,那你就太低估C++了。一些聪明的人可能已经想到解决办法了:可以通过传递引用的方式来获取返回值。

    例九:引用传递返回值

    这个例子中我们先不牵扯多线程的问题。假如你写一个函数,需要返回3个值,那你会怎么办呢?vector?嵌套pair?不不不,都不需要,3个引用参数就可以了。

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    using namespace std;
    constexpr long double PI = 3.14159265358979323846264338327950288419716939937510582097494459230781640628;
    // 给定圆的半径r,求圆的直径、周长及面积
    void get_circle_info(double r, double &d, double &c, double &s) {
    	d = r * 2;
    	c = PI * d;
    	s = PI * r * r;
    }
    int main() {
    	double r;
    	cin >> r;
    	double d, c, s;
    	get_circle_info(r, d, c, s);
    	cout << d << ' ' << c << ' ' <<  s << endl;
    	return 0;
    }
    

    输入5,输出:

    10 31.4159 78.5398
    

    如果你和我输出有一些误差,是正常现象,不同编译器、不同机器处理精度也有所不同

    std::promise到底是啥

    promise实际上是std::future的一个包装,在讲解future时,我们并没有牵扯到改变future值的问题,但是如果使用thread以引用传递返回值的话,就必须要改变future的值,那么该怎么办呢?
    实际上,future的值不能被改变,但你可以通过promise来创建一个拥有特定值的future。什么?没听懂?好吧,那我就举个例子:

    例十:std::future的值不能改变,那么如何利用引用传递返回值

    constexpr int a = 1;
    

    现在,把常量当成future,把a当作一个future对象,那我们想拥有一个值为2的future对象该怎么办?
    很简单:

    constexpr int a = 1;
    constexpr int b = 2;
    

    这样,我们就不用思考如何改动a的值,直接创建一个新常量就能解决问题了。
    promise的原理就是这样,不改变已有future的值,而是创建新的future对象。什么?还没听懂?好吧,记住这句话:

    future的值不能改变,promise的值可以改变。

    std::promise常用成员函数

    构造&析构函数

    函数类型作用
    promise()默认构造函数构造一个空的promise对象
    template <class Alloc> promise(allocator_arg_t aa, const Alloc& alloc)构造函数与默认构造函数相同,但使用特定的内存分配器alloc构造对象
    promise (const promise&) = delete复制构造函数(已删除)
    promise (promise&& x) noexcept移动构造函数构造一个与x相同的对象并破坏x
    ~promise()析构函数析构对象

    常用成员函数

    函数作用
    一般:
    void set_value (const T& val)
    void set_value (T&& val)
    当类型为引用:void promise<R&>::set_value (R& val)
    当类型为void:void promise::set_value (void)
    设置promise的值并将共享状态设为ready(将future_status设为ready)
    void特化:只将共享状态设为ready
    future get_future()构造一个future对象,其值与promise相同,status也与promise相同

    例十一:std::promise的使用

    以例七中的代码为基础加以修改:

    // Compiler: MSVC 19.29.30038.1
    // C++ Standard: C++17
    #include <iostream>
    #include <thread>
    #include <future> // std::promise std::future
    using namespace std;
    
    template<class ... Args> decltype(auto) sum(Args&&... args) {
    	return (0 + ... + args);
    }
    
    template<class ... Args> void sum_thread(promise<long long> &val, Args&&... args) {
    	val.set_value(sum(args...));
    }
    
    int main() {
    	promise<long long> sum_value;
    	thread get_sum(sum_thread<int, int, int>, ref(sum_value), 1, 10, 100);
    	cout << sum_value.get_future().get() << endl;
    	get_sum.join(); // 感谢评论区 未来想做游戏 的提醒
    	return 0;
    }
    

    输出:

    111
    

    C++11中的std::this_thread

    上面讲了那么多关于创建、控制线程的方法,现在该讲讲关于线程控制自己的方法了。
    <thread>头文件中,不仅有std::thread这个类,而且还有一个std::this_thread命名空间,它可以很方便地让线程对自己进行控制。

    std::this_thread常用函数

    std::this_thread是个命名空间,所以你可以使用using namespace std::this_thread;这样的语句来展开这个命名空间,不过我不建议这么做。

    函数作用
    std::thread::id get_id() noexcept获取当前线程id
    template<class Rep, class Period>
    void sleep_for( const std::chrono::duration<Rep, Period>& sleep_duration )
    等待sleep_durationsleep_duration是一段时间)
    void yield() noexcept暂时放弃线程的执行,将主动权交给其他线程
    (放心,主动权还会回来)

    例十二:std::this_thread中常用函数的使用

    #include <iostream>
    #include <thread>
    using namespace std;
    atomic_bool ready = 0;
    // uintmax_t ==> unsigned long long
    void sleep(uintmax_t ms) {
    	this_thread::sleep_for(chrono::milliseconds(ms));
    }
    void count() {
    	while (!ready) this_thread::yield();
    	for (int i = 0; i <= 20'0000'0000; i++);
    	cout << "Thread " << this_thread::get_id() << " finished!" << endl;
    	return;
    }
    int main() {
    	thread th[10];
    	for (int i = 0; i < 10; i++)
    		th[i] = thread(::count);
    	sleep(5000);
    	ready = true;
    	cout << "Start!" << endl;
    	for (int i = 0; i < 10; i++)
    		th[i].join();
    	return 0;
    }
    

    我的输出:

    Start!
    Thread 8820 finished!Thread 6676 finished!
    
    Thread 13720 finished!
    Thread 3148 finished!
    Thread 13716 finished!
    Thread 16424 finished!
    Thread 14228 finished!
    Thread 15464 finished!
    Thread 3348 finished!
    Thread 6804 finished!
    

    你的输出几乎不可能和我一样,不仅是多线程并行的问题,而且每个线程的id也可能不同。

    结尾

    这篇文章到这里就结束了 (说不定以后还会写个c++20的std::jthread讲解)。这是我第一篇接近2万字的文章。其实我刚开始写这篇文章时,也没想到这篇文章会吸引这么多人看,评论里还会有很多的好评,并且还上过一次热榜:
    热榜
    (厚颜无耻地给自己点赞)
    又入选过C/C++领域内容榜:
    C/C++领域内容榜
    这着实是出乎我的意料的。在此也感谢评论区里各位的好评,我就不一一回复了。
    如果你觉得这篇文章有不对、不标准之处,也可以在评论区里说一下,感谢支持。

    更多相关内容
  • 异步多线程之Thread详解

    千次阅读 2021-11-02 12:37:53
    在 .NET中随着时间的发展,线程是有许多个版本的1.0 Thread、2.0TthreadPool、3.0 Task、4.0 Parallel 等,今天我就一一进行讲解。 1.0 时代 在1.0 时代C# 就考虑到了多线程,于是就设计了 Thread 其实现在都不会再...

    上一篇:异步多线程之入门介绍
    下一篇:异步多线程之入ThreadPool


    介绍

    Thread 是 1.0 时代的产物,当时 C# 就考虑到了多线程,于是就设计了 Thread 。其实现在不推荐使用,除非在维护老的项目已经用了的。Thread 也是比较鸡肋的,有很多缺陷,但也并不是一无是处。

    Thread API

    这里对 Thread 的一些常用 API 进行介绍,使用一些案例进行说明。由于 Thread 的不可控与效率问题,Thread 现在已经不常用了,这里介绍一些 API ,想更深入的同学可以继续研究研究。

    Instance

    首先看 Thread 的构造函数,有 ThreadStart 、ParameterizedThreadStart 、maxStackSize 类型的参数,这三个常用的也就 ThreadStart ,其他两个可以作为了解。
    在这里插入图片描述
    分别 F12 查看 ThreadStart、ParameterizedThreadStart ,可以看到 ThreadStart 是无参数类型的委托、ParameterizedThreadStart 是有参数类型的委托。maxStackSize 是指定线程占用的最大内存数。
    在这里插入图片描述在这里插入图片描述在这里插入图片描述接着我们创建一个简单的案例,启动一个线程,模拟做一些任务行。如下代码

    Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    
    ThreadStart threadStart = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
        // 做一些任务
    
    
        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    };
    Thread thread = new Thread(threadStart);
    thread.Start();
    
    Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    
    Console.ReadLine();
    

    在这里插入图片描述
    启动程序,可以看到线程 1(主线程),没有等待线程 3(子线程)执行完成匿名方法内的任务,再执行 Main 结束这段代码。如果使用的是 winform 是不会卡界面的。

    这就是异步多线程,异步在于线程 1 并没有等待线程 3 执行完成任务,再执行线程 1 内的下一行,而是让线程 3 在不影响线程 1 执行任务的情况下执行,这就是异步。多线程在于我们启动了一个线程 3(子线程),在 Main 方法由线程1(子线程)与线程 3(主线程)一起完成 Main 方法内的代码,这就是多线程。
    在这里插入图片描述

    说到委托可会有小伙伴发出疑问,为啥不用 Action ?

    因为在这个版本还没有 Action、Func,这是在 .Net 3.0 时代的产物,Action、Func 的出现就是为了统一,也是为了解决此类问题。

    在 dotnet 框架,也建议最好使用 Action、Func,所以,在这使用 Action 是不可以的。如下

    Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    
    Action action = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
        // 做一些任务
    
    
        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    };
    
    ThreadStart threadStart = action;
    
    Thread thread = new Thread(threadStart);
    thread.Start();
    
    Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    
    Console.ReadLine();
    

    在这里插入图片描述

    Suspend、Resume

    Suspend 挂起、Resume 唤醒,这两个是一对相互对应的 API,使用时这两个容易产生死锁,其实在实际中也是不应该使用的,.NET 框架已经抛弃了,说的很清楚了。

    为什么会死锁呢?比如你开启了一个子线程 01,对 A 文件进行读写操作,此时你对子线程 01 进行了挂起。当你另外一个线程对 02 A 文件进行操作时,此时提示会 A 文件被占用,就行形成死锁。
    在这里插入图片描述在这里插入图片描述

    Abort、ResetAbort

    Abort 销毁,很多人在使用,这种是抛异常方式,使子线程销毁结束。这个功能也比较鸡肋,Abort 时子线程并不能立即停止,往往会有一些延迟,那这个销毁有时也不能达到我们可控的效果。

    比如,在一个方法内开了一个子线程进行数据计算,但执行的时间太长了,我们等待了 5000 ms,此时 Abort 子线程,是不能立马让子线程停止计算,而是可能要等一会才能结束子线程。

    比如,发出的动作,可能收不回来。查询数据库来说,当一个查库命令发送到数据库,我们在C# 执行了 Abort,但查库这个命令是收不回来的,因为他是在数据库层面,当数据库查询完成只是没有接收响应的线程罢了。
    在这里插入图片描述

    Abort 不建议使用,如果使用,一定要 try catch 一下。

    Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    
    ThreadStart threadStart = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
        // 做一些任务
    
        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    };
    Thread thread = new Thread(threadStart);
    
    thread.Start();
    
    try
    {
        thread.Abort(); // 销毁,方式是抛异常,不一定及时
    }
    catch (Exception ex)
    {
        //Thread.ResetAbort(); // 取消异常
    }
    
    Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    
    Console.ReadLine();
    

    在这里插入图片描述


    Suspend、Resume、Abort 这几个方法不建议使用,操作线程暂停、销毁或者其他操作都是不可控的,应为线程本身是操作系统的, CPU 分时分片会按照自己的规则进行运行,此时已经不是程序可以进行控的了。 既然设计了 Thread 不可能一无是处,接下来我们说些有用的


    Join

    线程等待 ,Join 可以一直等,也可以设置超时,超时就是等待一定时间,就不等了。等待的过程中主线程处于闲置状态等着子线程完成任务。如果是 winform 是会卡界面的,主线程等待也是一种工作。

    例如:threadStart 我们模拟任务耗时 5 秒,在 thread.Start() 任务开始后,使用 thread.Join() 等着子线程完成工作

    Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    ThreadStart threadStart = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
        // 做一些任务
        Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒
    
        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    };
    
    Thread thread = new Thread(threadStart);
    thread.Start();
    
    thread.Join();
    
    Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    Console.ReadLine();
    

    启动程序,可以看到是我们想要的结果(与同步执行一样),主线程 1 一直等着 子线程 3 完成执行的任务。如果是 winform 是会卡界面的,虽然 thread.Join() 主线程 1 会等着子线程 3 完成工作,但主线程 1 等着也是一种工作。
    在这里插入图片描述

    接着我们看下超时等待,Join 的重载方法

    例如:threadStart 我们模拟任务耗时 5 秒,在 thread.Start() 任务开始后,使用 thread.Join(3*1000) ,让主线程最多等子线程 3 秒,如果 3 秒子线程还未完成任务,就不等待了

    Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    ThreadStart threadStart = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
        // 做一些任务
        Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒
    
        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    };
    
    Thread thread = new Thread(threadStart);
    thread.Start();
    
    thread.Join(3 * 1000);
    
    Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    Console.ReadLine();
    

    启动程序,主线程 1 开始任务,子线程 3 也开始任务,当子线程执行 3 s 后(期间主线程 1 在等待),主线程 3 开始执行任务了。
    在这里插入图片描述

    注意:thread.Join(n * 1000) 并不是一定会等待那么长时间,而是最多等待,期间子线程任务执行完成后,就不等待了。

    例如:threadStart 任务方法模拟 5 s,thread.Join(7 * 1000) 主线程等待 7 s

    Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    ThreadStart threadStart = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
        // 做一些任务
        Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒
    
        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    };
    
    Thread thread = new Thread(threadStart);
    thread.Start();
    
    thread.Join(7 * 1000);
    
    Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    Console.ReadLine();
    

    在这里插入图片描述

    ThreadState

    线程状态,ThreadState 也可以做线程等待,等待的过程中主线程处于闲置状态等着子线程完成任务。如果是 winform 是会卡界面的,主线程等待也是一种工作。

    Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    ThreadStart threadStart = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
        // 做一些任务
        Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒
    
        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    };
    
    Thread thread = new Thread(threadStart);
    thread.Start();
    
    while (thread.ThreadState != ThreadState.Stopped)
    {
        Thread.Sleep(200); // 当前线程休息 200 毫秒
    }
    
    Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    Console.ReadLine();
    

    在这里插入图片描述

    Sleep

    线程暂停,Sleep 当前线程暂停。如果是 winform 是会卡界面的,当 Sleep 时,CPU 分片就交出去了,主线程并不在工作状态。

    Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒
    
    Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    Console.ReadLine();
    

    在这里插入图片描述

    IsBackground

    是否是后台线程,当实例 Thread 时,默认是前台线程(IsBackground == false )。前台线程一定要任务完成,才会让进程退出。后台线程(IsBackground == true)会随着进程的结束而结束,无论子线程任务是否完成。

    前台线程,意思也就是,当我们启动一个程序,当关闭程序时,如果还有子线程执行任务,当前进程是不会退出的,会等待着子进程将任务执行完成,也就是会阻止进程结束,反之亦然。

    例如:前台线程,启动控制台后,主线程执行完任务后,会等待子线程任务完成(5s)后,窗口才会被关闭

    static void Main(string[] args)
    {
        Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
        ThreadStart threadStart = () =>
        {
            Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
            // 做一些任务
            Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒
    
            Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
        };
    
        Thread thread = new Thread(threadStart);
        thread.Start();
    
        while (thread.ThreadState != ThreadState.Stopped)
        {
            Thread.Sleep(200); // 当前线程休息 200 毫秒
        }
    
        Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    }
    

    例如:后台线程,启动控制台后,主线程任务执行完毕后,窗口会立马被关闭

    static void Main(string[] args)
    {
        Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
        ThreadStart threadStart = () =>
        {
            Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
            // 做一些任务
            Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒
    
            Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
        };
    
        Thread thread = new Thread(threadStart);
        thread.IsBackground = true;
        thread.Start();
    
        Console.WriteLine($"thread IsBackground:{thread.IsBackground},DateTime:{DateTime.Now.ToLongTimeString()}");
    
        Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    }
    

    Priority

    线程可以设置优先级,当线程从高到低分配了优先级,在向 CPU 申请线程时会优先分配。但是这个功能也比较鸡肋,对于 CPU 而言,当他们同时过来,只是会为优先级高的先分进行分片,但优先级低的并不是不会分配,也不代表优先级高的就会先执行完成,这也取决执行的任务量。其实优先级也没什么用,多线程本来就是无序的。

    Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    ThreadStart threadStart = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
        // 做一些任务
        Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒
    
        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    };
    
    Thread thread = new Thread(threadStart);
    thread.Priority = ThreadPriority.Highest;// CPU 会先执行,不代表 Highest 就最优先
    thread.Start();
    
    Console.WriteLine($"thread IsBackground:{thread.IsBackground},DateTime:{DateTime.Now.ToLongTimeString()}");
    
    Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    

    总结

    其实现在来说 ,1.0 时代的 Thread 已经没有什么优势,现在 Thread 唯一有意义的就是 IsBackground = false,这个前线程(前台线程会阻碍进程的退出),后续的多线程设计都是后台线程,没有前台线程这个功能设计。

    展开全文
  • 手把手教你移植RT-Thread系统

    万次阅读 多人点赞 2020-12-10 17:00:42
    学习RT-Thread系统有一段时间了,感觉RT-Thread系统使用起来很简单,也很方便。但是在最开始移植的时候网上的教程很多,也很杂乱。比如可以使用官方的软件RT-Thread Studio 直接创建工程,创建好了之后系统就可以用...

            学习RT-Thread系统有一段时间了,感觉RT-Thread系统使用起来很简单,也很方便。但是在最开始移植的时候网上的教程很多,也很杂乱。比如可以使用官方的软件RT-Thread Studio 直接创建工程,创建好了之后系统就可以用了。也可以直接在 Keil MDK下载RT-Thread RTOS,下载完成后默认就会添加到工程中,系统直接也可以用了。还可以使用 CubeMX 移植 RT-Thread Nano,在创建工程的时候,直接将RT-Thread就添加进去了,工程创建完了之后,系统就直接可以使用。虽然会使用了,但是总感觉这种学习是知其然而不知其所以然。没有像学习其他系统时一步步拷贝文件,修改各种参数,那种深入的感觉。于是就查找各种文档,自己慢慢摸索着,从零开始如何通过拷贝文件和修改参数这种方法去真正自己动手移植系统。现在就将在STM32F103C8T6单片机上移植 RT-Thread Nano系统的方法分享出来。

    工程中用到的电路板是STM32F103C8T6最小系统,所使用的MDK为 keil 5.29版本,使用的是STM32的标准库文件。

    目录

    1.第一步 裸板工程模板

          要添加系统,首先得有一个完整的裸板工程,裸板工程的创建就不详细说了。这里用LED工程演示。

     2.第二步 下载RT-Thread Nano源码

            裸板工程准备好之后,下来需要准备RT-Thread的源码,去RT-Thread官网下载源码。

    下载地址:https://www.rt-thread.org/document/site/tutorial/nano/an0038-nano-introduction/

    准备好RT-Thread Nano的源码文件夹

     

    3.第三步 拷贝rtt 到裸机工程中

    在LED的裸机工程中新建一个rt-thread的文件夹,将刚才下载好的 rt-thread-3.1.3 文件夹拷贝到rt-thread文件夹中。

    4.第四步 删除rtt中用不到的文件

    rt-thread-3.1.3 文件夹中的内容比较多,在项目中只需要用到其中一部分文件,为了使代码看起来更清爽,需要将不需要的文件删除掉。打开工程中rt-thread--->rt-thread-3.1.3--->bsp文件夹。

    BSP文件夹中只留下board.c 和 rtconfig.h文件 其余全部删除。

    删除rt-thread--->rt-thread-3.1.3--->docs文件夹

    删除rtt中用不到的文件 rt-thread--->rt-thread-3.1.3--->libcpu--->arm,只留下工程中用到的cortex-m3文件夹,其余全部删除。

    5.第五步 修改工程目录结构

    • 打开工程
    • 打开工程项目管理 Manage Project Items
    • 在Groups中选择USER,在右边Files中添加\rt-thread\rt-thread-3.1.3\bsp文件中的board.c文件
    • 在Groups中选择USER,在右边Files中添加\rt-thread\rt-thread-3.1.3\bsp文件中的rtconfig.h文件
    • 在Groups中选择新建,新建rtt/source组,在右边Files中添加\rt-thread\rt-thread-3.1.3\src文件夹中的所有*.c文件 
    • 在Groups中选择新建,新建rtt/port组,在右边Files中添加rt-thread\rt-thread-3.1.3\libcpu\arm\cortex-m3文件夹下的context_rvds.S和cpuport.c文件
    • 给工程添加头文件路径,凡是文件夹中包含*.h文件,就需要将文件夹路径添加进去

                  ..\rt-thread\rt-thread-3.1.3\bsp

                  ..\rt-thread\rt-thread-3.1.3\components\finsh

                   ..\rt-thread\rt-thread-3.1.3\include\libc

                   ..\rt-thread\rt-thread-3.1.3\include

    6.第六步 编译工程并修改错误

       准备工作完成了,下来就可以开始编译工程了,点全部编译按钮。

    报了好多错误,基本都是不能打开文件 RTE_Components.h。头文件 RTE_Components.h是在 MDK中添加 RT-Thead Package 时由 MDK 自动生成的,目前我们没有使用 MDK 中自带的 RT-Thread 的 Package,所以这个头文件不存在,如果包含了该头文件,编译的时候会报错,需要修改 rtconfig.h 头文件,将相关代码注释掉。

    打开 rtconfig.h 头文件,将 #include "RTE_Components.h" 这行代码注释掉。

    注释之后再编译一次

    这时候错误就剩下了三个了,提示是有三个中断函数重复定义了。这时因为RT-Thread系统把这三个中断函数已经实现了,但是在stm32f10x_it.c这个文件中,也有这个三个中断函数,但是函数内是空的,没有实现。需要将 stm32f10x_it.c 文件中的这个三个中断函数注释掉。

    屏蔽掉这三个函数后,继续编译工程。

    现在说明操作系统已经移植成功了。

    7.第七步 编写第一个操作系统测试代码

           下来就可以先简单的编写一个测试程序,先测试测试RT-Thread系统。这里直接使用动态线程的创建方法,而系统默认的是静态创建方法,所以先要修改配置文件。

          删除rtconfig.h文件夹中第107行 前面的"//"符号,也就是将 #define RT_USING_HEAP 这行代码屏蔽去掉。

          这个宏的含义就是,使用动态堆栈。

    然后将main函数修改成下面这样

    #include "stm32f10x.h"
    #include "bsp_led.h"
    
    #include <rthw.h>
    #include "rtthread.h"
    
    /* 定义线程控制块 */
    static rt_thread_t led1_thread = RT_NULL;
    
    /*
    *************************************************************************
    *                             函数声明
    *************************************************************************
    */
    static void led1_thread_entry(void* parameter);
    
    
    int main( void )
    {
    
        /* LED 端口初始化 */
        LED_GPIO_Config();
    
       led1_thread =                          /* 线程控制块指针 */
        rt_thread_create( "led1",              /* 线程名字 */
                          led1_thread_entry,   /* 线程入口函数 */
                          RT_NULL,             /* 线程入口函数参数 */
                          512,                 /* 线程栈大小 */
                          3,                   /* 线程的优先级 */
                          20);                 /* 线程时间片 */
                       
        /* 启动线程,开启调度 */
       if (led1_thread != RT_NULL)
            rt_thread_startup(led1_thread);
        else
            return -1;
    
    }
    /*
    *************************************************************************
    *                             线程定义
    *************************************************************************
    */
    
    static void led1_thread_entry(void* parameter)
    {	
    	while (1)
    	{
    		LED1_ON;
    		rt_thread_delay(500);   /* 延时500个tick */
    	
    		LED1_OFF;     
    		rt_thread_delay(500);   /* 延时500个tick */		 		
    	
    	}
    }
    
    

    添加需要用到的RT-Thread系统头文件,定义线程控制块、声明线程入口函数。然后在主函数中动态创建一个led的线程。在led线程中闪烁LED灯。

    编译工程,将代码下载到开发板上,就可以看见LED灯在闪烁了。

    到这里RT-Thread实时操作系统移植已经全部完成了。

    8.第八步 修改board.c文件

           经过前面七步,RT-Thread实时系统已经可以运行了。但是为了方便工程管理,还需要对工程进行一点小改动。

           由于board.c和rtconfig.h文件是在每个工程中都需要修改的,为了方便管理,将rt-thread\rt-thread-3.1.3\bsp文件夹中的board.c和rtconfig.h文件拷贝到工程中User文件夹中。这样User文件夹下的所有文件,就代表是用户可以修改的。而rt-thread文件夹中的文件在以后的工程中就不需要做任何改动了。

          同时在User文件夹下新建 board.h 头文件,可以将工程用到的相关头文件,统一放到这个文件中。

    在board.h头文件中添加以下内容

    #ifndef __BOARD_H__
    #define __BOARD_H__
    
    /* STM32 固件库头文件 */
    #include "stm32f10x.h"
    
    /* RT-Thread相关头文件 */
    #include <rthw.h>
    #include <rtthread.h>
    
    /* 开发板硬件bsp头文件 */
    #include "bsp_led.h"
    
    
    #endif /* __BOARD_H__ */

          这个头文件中以后就可以存放在项目中所用到的所有相关头文件和函数声明。

          下面还需要修改board.c文件,将board.c包含的头文件全部删除,只添加"board.h" 头文件

             将时钟相关的宏定义和时钟配置函数全部屏蔽掉

           屏蔽掉rt_hw_board_init()函数中的  SystemCoreClockUpdate(); 和 _SysTick_Config( SystemCoreClock / RT_TICK_PER_SECOND ); 函数调用。

    重新添加时钟初始化语句 SysTick_Config( SystemCoreClock / RT_TICK_PER_SECOND );

    将LED初始化代码从主函数中剪切到时钟初始化语句下面。

             由于board.c和rtconfig.h文件位置发生了变化,所以要在工程管理中,将User文件夹下原来的board.c和rtconfig.h文件删除掉,然后重新从User文件夹里面添加这两个文件。

            由于这两个文件已经移动了位置,所以头文件中就不需要  ..\rt-thread\rt-thread-3.1.3\bsp这个路径了,将这个路径从头文件中删除。

          修改后的主函数如下

          下来就可以编译下载代码,如果LED灯正常闪烁,说明修改成功了。

    9.第九步 rt-thread移植完成

         最后就可以将上面步骤中屏蔽掉的那些代码删除了,这样以后在看代码时就会更清爽了,不会被那些无用的代码干扰了。删除stm32f10x_it.c中屏蔽的代码,删除board.c文件中屏蔽的代码。

             由于工程中 rt-thread 文件夹中存放的都是关于操作系统的代码,这些代码比较关键,不能够随便修改。 rt-thread 文件夹中以前的board.c和rtconfig.h文件已经复制到了User文件夹里面,所以rt-thread--->rt-thread-3.1.3--->bsp,这个BSP文件夹就可以删除掉了。

            下来就将工程目录下的 rt-thread 文件夹属性设置为只读,这样以后在编写代码过程中就不会因为误操作修改了系统文件而导致错误了。

            在工程目录下rt-thread 文件夹上单击鼠标右键,选择属性。

            打开属性对话框后,在只读前面点一下鼠标,让方框中打上对勾,然后点右下角 应用 按钮,在弹出的对话框中选择将更改应用于此文件夹、子文件夹和文件,然后点确定按钮,继续在属性对话框中点确定按钮。

    这时关闭掉工程,在重新打开一次。

           这时就会发现rtt相关文件前面都多了一个黄色的小钥匙图标,这就说明这些文件是只读的,不能修改。这样就不会因为误操作将操作系统的文件改动,而导致工程错误。

    到这一步,一个完美的RT-Thread系统工程模板就创建好了,以后的项目只需要在这个模板上修改就行了。

    10.第十步 rt-thread添加rt_printf支持

           在平时调试代码的时候,经常需要用到串口的打印功能。RT-Thread提供了一个专用的打印函数rt_kprintf(),这个函数的功能和printf()一样。要实现串口打印功能,首先需要给工程添加串口初始化相关代码,串口初始化和裸机中串口的使用是一模一样的。这里串口只需要使能接收功能,发送功能和发送中断可以不使用。

           在RT-Thread系统中rt_kprintf()函数是通过调用 rt_hw_console_output()函数来实现的。而rt_hw_console_output()函数系统默认是没有实现。需要用户自己实现。

           下面就自己实现rt_hw_console_output()函数

            在串口文件中添加rt_hw_console_output()函数实现代码,串口代码如下:

    #include "bsp_usart.h"
    #include "board.h"
    
    /**
     * @brief  USART GPIO 配置,工作参数配置
     * @param  无
     * @retval 无
     */
    void USART_Config( void )
    {
        GPIO_InitTypeDef GPIO_InitStructure;
        USART_InitTypeDef USART_InitStructure;
    
        // 打开串口GPIO的时钟
        DEBUG_USART_GPIO_APBxClkCmd( DEBUG_USART_GPIO_CLK, ENABLE );
    
        // 打开串口外设的时钟
        DEBUG_USART_APBxClkCmd( DEBUG_USART_CLK, ENABLE );
    
        // 将USART Tx的GPIO配置为推挽复用模式
        GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init( DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure );
    
        // 将USART Rx的GPIO配置为浮空输入模式
        GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
        GPIO_Init( DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure );
    
        // 配置串口的工作参数
        // 配置波特率
        USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
        // 配置 针数据字长
        USART_InitStructure.USART_WordLength = USART_WordLength_8b;
        // 配置停止位
        USART_InitStructure.USART_StopBits = USART_StopBits_1;
        // 配置校验位
        USART_InitStructure.USART_Parity = USART_Parity_No ;
        // 配置硬件流控制
        USART_InitStructure.USART_HardwareFlowControl =
            USART_HardwareFlowControl_None;
        // 配置工作模式,收发一起
        USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
        // 完成串口的初始化配置
        USART_Init( DEBUG_USARTx, &USART_InitStructure );
    
        // 使能串口
        USART_Cmd( DEBUG_USARTx, ENABLE );
    }
    //*************************************************************//
    // 系统rt_kprintf()函数是通过调用 rt_hw_console_output();来实现
    // 而rt_hw_console_output();函数系统默认是没有实现的,所以如果要
    // 使用rt_kprintf()函数,就需要用户自己实现rt_hw_console_output()函数
    // 实现rt_hw_console_output()函数的前提是,要初始化串口功能,串口初始化
    // 过程和裸机串口初始化过程完全一样
    //************************************************************//
    /**
      * @brief  重映射串口DEBUG_USARTx到rt_kprintf()函数
      *   Note:DEBUG_USARTx是在bsp_usart.h中定义的宏,默认使用串口1
      * @param  str:要输出到串口的字符串
      * @retval 无
      *
      * @attention
      *
      */
    void rt_hw_console_output( const char *str )
    {
        /* 进入临界段 */
        rt_enter_critical();
    
        /* 直到字符串结束 */
        while ( *str != '\0' )
        {
            /* 换行 */
            //RT-Thread 系统中已有的打印均以 \n 结尾,而并非 \r\n,所以在字符输出时,需要在输出 \n 之前输出 \r,完成回车与换行,否则系统打印出来的信息将只有换行
            if ( *str == '\n' )
            {
                USART_SendData( DEBUG_USARTx, '\r' );
                while ( USART_GetFlagStatus( DEBUG_USARTx, USART_FLAG_TXE ) == RESET );
            }
    
            USART_SendData( DEBUG_USARTx, *str++ );
            while ( USART_GetFlagStatus( DEBUG_USARTx, USART_FLAG_TXE ) == RESET );
        }
    
        /* 退出临界段 */
        rt_exit_critical();
    }
    
    
    

            下面就可以使用打印功能了,在LED线程中添加打印功能。

           编译下载程序后,就可以在串口助手中查看打印的内容了。

     可以看到在系统启动的时候,会先打印出系统相关信息,然后输出LED线程中的字符串。说明rt_kprintf()函数已经实现了。

    11.第十一步 rt-thread添加Finsh组件支持

            RT-Thread FinSH 是 RT-Thread 的命令行组件(shell),提供一套供用户在命令行调用的操作接口,主要用于调试或查看系统信息。

           要使用FinSH组件的话,首先要添加需要用到的文件。打开项目管理对话框 Manage Project Items ,在Group组中新建 rtt/finsh文件夹,然后在右边Files中添加rt-thread\rt-thread-3.1.3\components\finsh文件夹中的 cmd.c、msh.c、shell.c文件。

    下来添加头文件路径,添加头文件路径..\rt-thread\rt-thread-3.1.3\components\finsh

    下来需要在配置文件中开启FINSH组件的宏定义,rtconfig.h文件中添加 RT_USING_FINSH宏定义。

    由于在控制台中,需要键盘输入字符,需要调用finsh_getchar()函数,这个函数又是调用rt_hw_console_getchar()来实现的。

    这个rt_hw_console_getchar()函数需要用户自己实现。现在在串口函数中去实现这个字符读取函数。

    由于这个字符读取时使用查询方式实现的,所以使用Finsh组件的话,串口不能启用中断接收功能。

    串口中完整代码如下:

    #include "bsp_usart.h"
    #include "board.h"
    /**
     * @brief  USART GPIO 配置,工作参数配置
     * @param  无
     * @retval 无
     */
    void USART_Config( void )
    {
        GPIO_InitTypeDef GPIO_InitStructure;
        USART_InitTypeDef USART_InitStructure;
    
        // 打开串口GPIO的时钟
        DEBUG_USART_GPIO_APBxClkCmd( DEBUG_USART_GPIO_CLK, ENABLE );
    
        // 打开串口外设的时钟
        DEBUG_USART_APBxClkCmd( DEBUG_USART_CLK, ENABLE );
    
        // 将USART Tx的GPIO配置为推挽复用模式
        GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init( DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure );
    
        // 将USART Rx的GPIO配置为浮空输入模式
        GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
        GPIO_Init( DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure );
    
        // 配置串口的工作参数
        // 配置波特率
        USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
        // 配置 针数据字长
        USART_InitStructure.USART_WordLength = USART_WordLength_8b;
        // 配置停止位
        USART_InitStructure.USART_StopBits = USART_StopBits_1;
        // 配置校验位
        USART_InitStructure.USART_Parity = USART_Parity_No ;
        // 配置硬件流控制
        USART_InitStructure.USART_HardwareFlowControl =
            USART_HardwareFlowControl_None;
        // 配置工作模式,收发一起
        USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
        // 完成串口的初始化配置
        USART_Init( DEBUG_USARTx, &USART_InitStructure );
        // 使能串口
        USART_Cmd( DEBUG_USARTx, ENABLE );
    }
    //*************************************************************//
    // 系统rt_kprintf()函数是通过调用 rt_hw_console_output();来实现
    // 而rt_hw_console_output();函数系统默认是没有实现的,所以如果要
    // 使用rt_kprintf()函数,就需要用户自己实现rt_hw_console_output()函数
    // 实现rt_hw_console_output()函数的前提是,要初始化串口功能,串口初始化
    // 过程和裸机串口初始化过程完全一样
    //************************************************************//
    /**
      * @brief  重映射串口DEBUG_USARTx到rt_kprintf()函数
      *   Note:DEBUG_USARTx是在bsp_usart.h中定义的宏,默认使用串口1
      * @param  str:要输出到串口的字符串
      * @retval 无
      *
      * @attention
      *
      */
    void rt_hw_console_output( const char *str )
    {
        /* 进入临界段 */
        rt_enter_critical();
    
        /* 直到字符串结束 */
        while ( *str != '\0' )
        {
            /* 换行 */
            //RT-Thread 系统中已有的打印均以 \n 结尾,而并非 \r\n,所以在字符输出时,需要在输出 \n 之前输出 \r,完成回车与换行,否则系统打印出来的信息将只有换行
            if ( *str == '\n' )
            {
                USART_SendData( DEBUG_USARTx, '\r' );
                while ( USART_GetFlagStatus( DEBUG_USARTx, USART_FLAG_TXE ) == RESET );
            }
    
            USART_SendData( DEBUG_USARTx, *str++ );
            while ( USART_GetFlagStatus( DEBUG_USARTx, USART_FLAG_TXE ) == RESET );
        }
    
        /* 退出临界段 */
        rt_exit_critical();
    }
    //使用Finsh组件三步骤:
    //1.实现该函数及rt_hw_console_output函数;
    //2.rtconfig.h中开启RT_USING_FINSH宏;
    //3.添加Finsh组件(cmd.c、msh.c、shell.c);
    char rt_hw_console_getchar( void )
    {
        //查询方式实现,记得将Usart1初始化中的中断接收配置相关代码注释掉
        int ch = -1;
        /*等待串口1输入数据*/
        if( USART_GetFlagStatus( DEBUG_USARTx, USART_FLAG_RXNE ) != RESET )
        {
            ch = ( int )USART_ReceiveData( DEBUG_USARTx );
            USART_ClearFlag( DEBUG_USARTx, USART_FLAG_RXNE );
        }
        else
        {
            if( USART_GetFlagStatus( DEBUG_USARTx, USART_FLAG_ORE ) != RESET )
            {
                USART_ClearFlag( DEBUG_USARTx, USART_FLAG_ORE );
            }
            rt_thread_mdelay( 10 );
        }
        return ch;
    }

    为了方便观察,先把LED线程中rt_kprintf()函数屏蔽掉。

    Finsh组件这时候就可以使用了,编译下载文件。测试下Finsh组件功能。

    使用控制台的时候,需要键盘输入,而串口助手中不能输入字符,所以就不能使用串口助手测试了。在这里使用secureCRT软件进行测试。打开secureCRT软件,选择文件--->快速连接。

    在打开的对话框中协议选择 串口 Serial,端口选则电脑上所使用的的串口,波特率选择115200,数据位选8位,奇偶校验无,停止位1位,流控全部不选择。然后点连接。

    连接成功后左上角就会出现绿色的对勾

    这时候编译下载程序,控制台上就会输出信息

    这时候就可以在键盘上输入命令了,这里先输入help,然后按回车。

    控制台上就会打印当前所支持的所有命令,输入list_thread命令,然后按回车键。

    命令正常执行,说明Finsh组件功能已经完全正常了。

    到这里RT-Thread系统移植已经成功了,常用的rt_kprintf() 打印功能和Finsh命令行组件功能也实现了。下来就可以使用RT-Thread系统去开发项目了。

    上面步骤中所用到的工程已经打包上传了

    下载地址 点这里 https://download.csdn.net/download/qq_20222919/13623451

    手把手教你移植rt-thread

    展开全文
  • 从小白到起飞的 RT-Thread 开发指南

    万次阅读 多人点赞 2020-12-26 03:56:51
    Hello,大家好,我是 RT-Thread 宣传委员(自封的)???? 我经常给大家安利 RT-Thread,尤其是那些还没用上 RT-Thread、在纠结选择哪个 RTOS、要不要学习 RTOS 的朋友。 RT-Thread 简介 RT-Thread 是什么 据不完全...

    Hello,大家好,我是 RT-Thread 宣传委员(自封的)🙂

    我经常给大家安利 RT-Thread,尤其是那些还没用上 RT-Thread、在纠结选择哪个 RTOS、要不要学习 RTOS 的朋友。

    1. RT-Thread 简介

    1.1 RT-Thread 是什么

    据不完全统计,世界有成千上万个 RTOS(Real-time operating system,实时操作系统),RT-Thread 就是其中一个优秀的作品。

    RT-Thread 内核的第一个版本是熊谱翔先生在 2006 年年初发布的 0.1 版本。因为 RTOS 中的任务更类似于通用操作系统中的线程,并且这个系统支持基于优先级的抢占式任务调度算法,调度器的时间复杂度是 O(1),所以把它命名为 RT-Thread,即实时线程。名字起得真妙 (。・∀・)ノ

    经过14年的发展,RT-Thread 被广泛应用于智能家居、智慧城市、安防、工控、穿戴等众多行业领域,累计装机量超过6亿台,GitHub 的 Star 数量超过 5.3k,嵌入式开源社区活跃度行业第一。

    在这里插入图片描述

    1.2 RT-Thread 的作用

    随着 MCU 硬件性能的提升、外设的增加以及软件功能需求的增加,越来越多项目采用 RTOS 的开发方式。一方面裸机开发方式的软件框架不清晰,对后续扩展功能极其不利;另一方面,由于软件复杂性的增加,裸机开发对工程师的要求越来越严苛,过多使用中断等因素,都会增加系统的不可靠性。

    在这里插入图片描述

    和其他优秀的 RTOS 一样,RT-Thread 的首要目标也是解决这些问题。RT-Thread 内核的主要功能是向下管理所有硬件资源,向上为应用程序提供 API 接口和软件服务,所有任务在内核的管理、同步和调度下有序运行。

    简单来说,使用 RT-Thread 能够带来如下好处:

    • 支持任务并发处理
    • 容易扩展新的功能
    • 破解应用的复杂性
    • 增强代码可移植性
    • 节省软件开发时间

    您可能会说,其他 RTOS 也能提供这些好处呀,为什么你要推荐 RT-Thread?

    因为 RT-Thread 还有许多优点,包括它支持非常多的硬件平台,还拥有及其丰富的组件和软件包(包括文件系统、网络、IoT、AI、传感器等等),提供了便捷的开发环境和 IDE 工具,以及有众多技术文档、参考设计和活跃的开发者社区,这些都能帮助您快速入门和掌握 RT-Thread,开发出优秀的产品。

    当然啦,最重要的是,RT-Thread 是一群扎实做事的中国人写的开源操作系统,帮助您做出好产品的同时还能提高民族自信和国家竞争力。

    1.3 RT-Thread 的特点

    相比其他操作系统,RT-Thread 的主打特性是 “小而美的物联网操作系统”。

    ” 体现在 RT-Thread 的体积小,最小资源占用 1.2KB RAM 和 2.5KB flash。RT-Thread 可伸缩、易裁剪的特性,帮助用户在需要一个适用的操作系统的时候,轻松地进行裁减,适应到需要的场景,不占用过多的资源。

    ” 不单止代码质量和代码风格,还有 RT-Thread 的使用和开发体验,以及增加了小程序、SMP 多核调度、PSA 安全支持等多项实用的新功能,使得 RT-Thread 系统能实现灵活极简的应用开发,能扩展至众多高性能、高安全的应用领域。

    物联网” 则体现在 RT-Thread 针对物联网场景提供的众多组件和软件包,比如 AT组件、WiFi、蓝牙、LoRa、Sensor、AI 等等。以及针对安全和低功耗的优化,还有以 JS 为开发方式的柿饼 UI,都使其非常适用于 IoT 领域。

    1.4 开源的 RT-Thread

    您可能会担忧,RT-Thread 作为一个开源的操作系统,会不会污染企业私有代码?开发过程中会不会有很多坑?

    在这里插入图片描述

    其实不用担心,因为 RT-Thread 是一个“开源、社区化、中立”的系统。遵循 Apache 2.0 开源许可协议,意味着您可以放心地免费在商业产品中使用 RT-Thread 内核和组件,不需要公开私有代码,只需要在产品说明书上提及“基于 RT-Thread 系统”或“Powered by RT-Thread”即可。

    Tips:因为 RT-Thread 软件包平台是开放的,许多软件包都是来自社区开发者,不同软件包的许可协议可能会不一样,所以在商业产品中使用时需要稍加注意。

    同时,RT-Thread 非常注重在嵌入式开源社区的生态建设。经过多年的积累,除了有官方技术支持团队,还有活跃的开源社区和数十万开发者,开发过程中的遇到难题都能寻找官方团队和社区开发者的支持。

    2. RT-Thread 版本

    RT-Thread 针对不同应用场景,目前提供了三个主版本,分别是 RT-Thread 标准版、RT-Thread Nano 和 RT-Thread Smart。

    代码均可在 https://www.rt-thread.org/page/download.html 页面下载。

    2.1 RT-Thread 标准版

    最常用应该是 RT-Thread 标准版(也称为 RT-Thread 全功能版本),它由内核层、组件和服务层、IoT 框架层、文件系统、图形库、设备框架等组成。包含完整的中间件组件,具备低功耗、安全、通信协议支持和云端连接能力,是一个完整的 IoT OS。

    在这里插入图片描述

    可以看到,除了实时内核,RT-Thread 标准版还具备丰富的中间层组件和软件包生态,这也是 RT-Thread 与其他很多 RTOS 的主要区别之一。

    2.2 RT-Thread Nano

    RT-Thread Nano 是一个极简的硬实时内核,其内存资源占用极小,功能包括任务管理、软件定时器、信号量、邮箱和实时调度等相对完整的实时操作系统特性。对于系统资源紧张或是项目功能较为简单的,则可以选择基于 RT-Thread Nano 进行开发。比如家电、消费、医疗、工控领域的 32 位入门级 MCU。

    在这里插入图片描述

    Tips:RT-Thread Nano 已得到 ARM Keil 官方的认可和支持,在 Keil MDK 中以 pack 方式提供,方便用户使用。

    2.3 RT-Thread Smart

    RT-Thread Smart 是一款高性能混合微内核操作系统。其定位在于填补传统 RTOS 和大型操作系统 Linux 之间的空白,在实时性、成本、安全性、启动速度等方面取得最佳的平衡。适用于带 MMU 的中高端应用的芯片,例如 ARM Cortex-A 系列芯片、MIPS 芯片、带 MMU 的 RISC-V 芯片等。广泛应用于安防、车载、军工、消费电子等领域。

    在这里插入图片描述

    RT-Thread Smart 在 RT-Thread 标准版的基础上启用独立、完整的进程方式,同时以混合微内核模式执行。这里“混合”的意思是指在编译时决定整体系统采用什么样的运行模式。

    在这里插入图片描述

    2.4 更多

    除了以上三个主版本,RT-Thread 还有不同的演进版本,包括前几天开发者大会发布的专用于智能穿戴领域的 PersimUI OS(湃心OS),以及应用于智能表计领域的 MoM(Microkernel on MCU),还有一些许可授权的衍生OS。

    在这里插入图片描述

    3. RT-Thread 设计思想

    RT-Thread 有许多巧妙的设计思想,篇幅有限,这里先介绍几个,其他留给您慢慢探索。

    3.1 任务调度

    RT-Thread 支持多任务,允许多个任务同时运行,但并不是真正的同时运行(对于单核的 MCU),而是宏观上的并行,这就需要线程调度器(任务调度器)来完成任务的调度了。

    在这里插入图片描述

    RT-Thread 最大支持 256 级优先级(0~255),数值越小优先级越高。可以根据实际情况选择 8 或 32 级,对于 ARM Cortex-M 系列,通常采用 32 级优先级。

    调度器是操作系统的核心,其主要功能就是实现线程的切换。RT-Thread 通过管理就绪列表,当需要调度时可以直接找出就绪列表中优先级最高的线程,然后执行该线程,时间复杂度为 O(1)。

    /* ready thread list */
    rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];
    
    /* get highest ready priority thread */
    highest_priority_thread = rt_list_entry(rt_thread_priority_table[highest_ready_priority].next,
                              struct rt_thread,
                              tlist);
    

    同时,RT-Thread 还采用 round-robin 策略确保对具有相同优先级的所有线程进行同等调度。RT-Thread 的每个线程都有时间片参数,如果您希望控制相同优先级的多个线程的单次运行时长,可以分别给它们设置不同的时间片。

    3.2 启动流程

    RT-Thread 系统的初始化在 main() 函数之前,这意味着您不需要操心 RT-Thread 的初始化操作,可以专心编写应用程序。

    RT-Thread 还提供了自动初始化机制,初始化函数只需要在函数定义处通过宏定义的方式进行声明,就会在系统启动过程中自动执行,不需要在应用代码中显示调用,相当优雅。

    针对不同层级,RT-Thread 提供了不同的宏接口:

    初始化顺序宏接口描述
    1INIT_BOARD_EXPORT(fn)非常早期的初始化,此时调度器还未启动
    2INIT_PREV_EXPORT(fn)主要是用于纯软件的初始化、没有太多依赖的函数
    3INIT_DEVICE_EXPORT(fn)外设驱动初始化相关,比如网卡设备
    4INIT_COMPONENT_EXPORT(fn)组件初始化,比如文件系统或者 LWIP
    5INIT_ENV_EXPORT(fn)系统环境初始化,比如挂载文件系统
    6INIT_APP_EXPORT(fn)应用初始化,比如 GUI 应用

    3.3 内核对象

    RT-Thread 内核采用面向对象的设计思想进行设计,系统级的基础设施都是内核对象,比如线程、信号量、互斥量、事件、邮箱、消息队列、定时器、内存池、设备驱动等等。然后通过内核对象管理系统来访问/管理所有内核对象,例如当您创建一个对象时,内核对象管理系统就会将这个对象放到一个叫对象容器的地方。

    在这里插入图片描述

    这样做是为了方便管理内核资源,在后续的开发调试阶段可以很方便地获取各个内核对象的状态,并通过 FinSH 输出调试信息。

    3.4 FinSH 控制台

    FinSH 是 RT-Thread 最早的组件之一,提供了一套类似于 Linux Shell 的操作接口,您可以通过 串口/以太网/USB 等方式与 PC 机进行通信,通过命令行查看系统信息或用于调试。

    在这里插入图片描述

    RT-Thread 默认内置了一些 FinSH 命令,比如 list_threadps 用于查看线程信息,list_sem 用于查看系统信号量信息,free 用于查看系统内存使用情况等等。如果开启 DFS 组件,还可以使用 lscdcp 等命令操作文件系统。

    4. RT-Thread 快速上手

    为了提升开发体验,RT-Thread 为我们提供了几种开发环境,主要包括使用多年的 Env 工具和近年来主推的 RT-Thread Studio 集成开发环境。

    4.1 Env 工具

    Env 是 RT-Thread 推出的开发辅助工具,针对基于 RT-Thread 操作系统的项目工程,提供编译构建环境、图形化系统配置及软件包管理功能。其内置的 menuconfig 提供了简单易用的配置剪裁工具,可对内核、组件和软件包进行自由裁剪,使系统以搭积木的方式进行构建。

    在这里插入图片描述

    4.2 RT-Thread Studio

    RT-Thread Studio 是一个基于 Eclipse 的开发工具软件,主要包括工程创建和管理,代码编辑,SDK管理,RT-Thread配置,构建配置,调试配置,程序下载和调试等功能。

    • 优点:低门槛快速上手,一站式开发,图形化配置,社区版本永久免费。
    • 缺点:目前只支持 Windows 平台,配置、编译、运行速度较慢,支持的硬件平台较少。
    • 下载:https://www.rt-thread.org/page/studio.html

    在这里插入图片描述

    4.3 其他

    还有其他一些开发方式,比如在 Keil MDK 中以 pack 形式添加 RT-Thread 内核。

    5. 如何参与社区贡献

    最后,如果您也想参与社区建设,为 RT-Thread 的生态贡献一份力量,那么您可以结合自身情况选择轻度参与或重度参与。

    5.1 轻度参与

    • 在公司产品、个人项目或比赛中使用 RT-Thread;
    • RT-Thread 社区 中提问或回答问题;
    • GitHub 给 RT-Thread 提 Issue 反馈 Bug;
    • 参与社区活动和 B站 直播;

    5.2 重度参与

    • GitHub 给 RT-Thread 提 PR 修复 Bug 或增加新功能;
    • 制作软件包并提交到 官方软件包索引仓库
    • 领取 任务 并加入 社区工作小组
    • 组建社区兴趣小组一起学习;
    • 在当地组织或参与开发者沙龙;
    • 参与 RT-Thread 开发者能力认证考试;

    还有,别忘了参加 RT-Thread 年度开发者大会!

    6. 有哪些学习资料

    展开全文
  • 文章目录一、初识RT-Thread1.简介2.前景3.软件生态二、实验准备三、实验需求四、操作流程1.新建RT-Thread工程2.RT-Thread Studio界面介绍3.代码编写4.烧录5.串口监视五、代码演示六、原理讲解 一、初识RT-Thread 做...
  • 我们进行程序开发的时候,肯定避免不了要处理并发的情况。 一般并发的手段有采用多进程和多线程。 但线程比进程更轻量化,系统开销一般也更低,所以大家更倾向于用多线程的方式处理并发的情况。...thread...
  • Android ThreadthreadLoop方法

    千次阅读 2018-04-12 17:27:54
    Android Framework中的线程Thread及它的threadLoop方法 在Framework中的Thread普遍的特点就是有一个 threadLoop方法。它到底是怎么循环起来的。 Android中java世界的Thread 先来看看java是怎么创建一个线程的。...
  • RT-Thread快速入门-信号

    千次阅读 2022-03-24 11:26:46
    作为一款 RTOS,RT-Thread 提供了 Linux 才有的信号机制。其他常用的 RTOS 没有提供信号机制。 在 Linux 中,信号作为进程间通信的一种方式。而在 RT-Thread 中,仿照类似的原理,实现信号机制,用于线程(任务)间...
  • Thread线程停止的正确方式

    千次阅读 2019-11-23 17:47:37
    当我们使用Thread.stop()方法去停止线程时,这个方法会报错,抛出了UnsupportedOperationException异常,它在JDK中已经被声明“过期/作废”的方法,显然它在功能上有缺陷,不建议使用。 stop()方法的源码...
  • Android中线程(Thread)的创建及内存分配过程分析 本文详细分析Android中,线程的创建过程以及线程创建时,内存的分配。 Java层Thread的分析 我们想要创建一个线程,通常都是使用Thread类,创建一个Thread的实例...
  • C++ thread用法总结(整理)

    千次阅读 2021-03-15 08:40:56
    C++11的标准类std::thread对线程进行了封装,定义了C++11标准中的一些表示线程的类、用于互斥访问的类与方法等。应用C++11中的std::thread便于多线程程序的移值。 std::thread类成员函数: (1)、get_id:获取线程...
  • C++11中thread_local的使用

    千次阅读 2020-09-20 12:51:46
    C++11中的thread_local是C++存储期的一种,属于线程存储期。存储期定义C++程序中变量/函数的范围(可见性)和生命周期。C++程序中可用的存储期包括auto、register、static、extern、mutable和thread_local。这些说明符...
  • 彻底理解Runnable和Thread的区别

    万次阅读 多人点赞 2019-07-30 09:45:42
      在实际工作中,我们很可能习惯性地选择Runnable或Thread之一直接使用,根本没在意二者的区别,但在面试中很多自以为是的菜货面试官会经常而且非常严肃的问出:请你解释下Runnable或Thread的区别?尤其是新手就...
  • RT-Thread Smart 上手指南

    万次阅读 2020-11-26 18:55:04
    RT-Thread Smart(简称rt-smart)是基于RT-Thread操作系统衍生的新分支,面向带MMU,中高端应用的芯片,例如ARM Cortex-A系列芯片,MIPS芯片,带...
  • 准备工作 下载 RT-Thread Nano 源码:https://github.com/RT-Thread/rtthread-nano/archive/refs/heads/master.zip。 目前的最新版本是 3.15(写于2022-3-1) 新建 GD32F10x系列的 裸机Keil工程...实现RT-Thread的基本
  • 今天在研究ThreadPoolExecutor源码时,在runWorker(Worker w)的方法中有一个判断条件分别用到了Thread.interrupted()和Thread.currentThread().isInterrupted() 。一之间没有看懂。就写了一个demo研究了一下,这里...
  • 1.前言 RT-Thread这一个操作系统获得很多工程师的...主要要用的有两个一个是RT-Thread Studio,这个是IDE,另外一个是RT-Thread env 工具,用于图形化进行配置,然后还有一个RT-Thread本身,也就是MCU用的操作系统 ..
  • python 线程(thread)

    千次阅读 2020-12-24 10:36:51
    #coding:utf-8#多线程#Python的标准库提供了两个模块:thread和threading,thread是低级模块,threading是高级模块,对thread进行了封装#绝大数情况下,我们只需要使用threading这个高级模块#启动一个线程就是把一个函数...
  • std::thread使用

    千次阅读 2018-10-16 20:58:41
    c++ 11 之后有了标准的线程库:std::thread。通过c++11中的线程库创建线程,极为方便,且跨平台,是语言层面的。之前跨平台的多线程开发中,多使用boost 相关第三方库。现在推荐使用std::thread。 1.std::thread ...
  • RT-Thread Studio快速配置GPIO进行点灯

    万次阅读 2021-06-03 15:59:30
    本章通过RT-Thread Studio配置AB32VG1片上外设GPIO的引脚,控制RGB彩灯进行简单的颜色变换
  • Java Thread类源码详解

    千次阅读 2018-08-23 14:51:18
    Java所有多线程的实现,均通过封装Thread类实现,所以深入Thread类,对深入理解java多线程很有必要 构造函数: Thread的构造函数,采用缺省的方式实现: //传入Runnable接口实现 Thread...
  • Thread 类部分常用方法详解

    千次阅读 2020-09-29 23:25:21
    currentThread() 方法用来返回代码段正在被哪个线程调用,它是 Thread 类提供的一个 native 方法,返回一个 Thread 类的实例对象,源码如下: public static native Thread currentThread(); 废话就不多说了,直接...
  • thread dump分析

    千次阅读 2018-07-30 19:55:05
    一、thread dump信息获取 1、发送信号 * In Unix, use "kill -3 &lt;pid&gt;" where pid is the Process ID of the JVM.(kill 信号列表) * In Windows, press CTRL+BREAK on the window where ...
  • RT-Thread学习笔记——移植RT-Thread到STM32

    万次阅读 多人点赞 2019-01-22 11:08:27
    从本文开始,记录自己的RT-Thread学习笔记,基于STM32L475VET6讲解,相关开发板用RTT&amp;正点原子的潘多拉IoT Board开发板。本文先从Nano开始学起,个人觉得对于初学者,还是先学会Nano的移植,把内核部分向学...
  • 1 finally: lock.release() # 关闭锁 if __name__ == '__main__': # 定义一个线程锁 # 注意修改同一个变量的线程要使用同一个锁 lock = threading.Lock() # 开启多个线程 for i in range(10): t = threading.Thread...
  • C++ 多线程(3)std::thread 详解

    千次阅读 2020-05-30 21:18:55
    文章目录一、头文件二、std::thread 构造函数三、其他成员函数四、传递临时参数作为线程对象的注意事项4.1 **解决办法:**4.2 原因分析4.3 总结五、传递类对象、智能指针作为线程参数5.1 修改子线程中的对象,不会...
  • 多线程中Thread的join方法

    千次阅读 2021-09-21 09:46:30
    多线程中Thread的join方法 join简介 join方法是Thread类中的一个方法,该方法的定义是等待该线程执行直到终止。其实就说join方法将挂起调用线程的执行,直到被调用的对象完成它的执行。 join实例(以一道面试题为例...
  • RTThread(一) - 概念及简介

    万次阅读 多人点赞 2020-11-09 21:34:20
    文章目录前置说明RTThread框架线程管理及调度器线程间通信管理时钟管理内存分布内存管理动态内存堆管理静态内存池管理RTT启动过程自动初始化机制内核对象管理架构内核配置&剪裁 前置说明 一般单片机一般只有一个...
  • RT-Thread Studio联合STM32CubeMX进行开发

    千次阅读 多人点赞 2021-06-20 22:29:04
    使用RT-Thread Studio内置的STM32CubeMX进行外设配置步骤
  • 线程(Thread)(代码)

    千次阅读 2020-05-14 11:59:01
    在了解线程之前,我们先看看线程与进程的区别 进程:是正在运行的程序 是系统进行资源分配和调用的独立单位 每一个进程都有它自己的内存空间和系统资源 ...线程:是进程中的单个顺序控制流,是...继承Thread类 实.

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,313,084
精华内容 925,233
关键字:

thread

友情链接: lineless13.rar