Windows环境下的多线程编程(上)C++

1.为什么要用多线程

任务分解:

耗时的操作,任务分解,实时响应

数据分解:

充分利用多核CPU处理数据

数据流分解:

读写分流,解耦合设计

2.并发,进程,线程的基本概念

2.1 并发:两个或者两个以上的独立任务同时进行:一个程序同时执行多个独立的任务;

对于单核计算机来说,某一时刻只能执行一个任务:由操作系统调度,每秒钟进行多次的所谓的“任务切换”,对每个任务设置一个执行时间,到了时间之后切换到另一个任务,就这样来回切换,来实现并发,这种叫并发假象,也就是所谓的“上下文切换”。这种切换时间花销很大。

对于多核计算机来说,可真正实行并发任务,但一般情况下,多核处理器的也需要进行来回切换,因为任务一般可达到好几千个,而处理器一般就是2核,4核,6核,8核等。

2.2 进程:运行起来的可执行程序。(在任务管理器->用户中可以看到)

2.3 线程:用来执行代码的

  • 每个进程中只有一个主线程
  • 当执行一个可执行文件时,这个主线程就自动启动了,相当于是主线程来执行该代码,主线程与进程有你有我,无我无你。

除了主线程之外,还可以自己写代码来创建其他线程,及多线程(并发)每个线程都需要一个独立的堆栈空间(1M), 不宜太多。不然来回切换会耗费程序运行的时间。

3.并发的实现:

  1. 多进程实现
  2. 多线程实现(自己写代码实现)

 多进程并发:

word启动后就是进程,浏览器启动后就是进程。账号服务器启动就是一个进程,游戏逻辑服务器启动后又是一个进程,就叫多进程。进程与进程之间的通讯:在一台电脑上通讯手段有:管道,文件,消息队列,共享内存等;不在同一台电脑上的通信手段:socket通信技术。

多线程并发:

在单个进程中创建多个线程,线程可以看作是轻量级的进程,一个进程中的所有线程共享内存,使用多线程开销远远小于多进程;但共享内存一般会带来数据一致性问题,比如两个线程同时传递数据时会引起混乱,所以在写线程时要注意先后。传统情况下:主线程结束,子线程必须结束掉,不然就是不合格的程序,一般都应该是主线程等子线程,再自己执行。但在C++11新标准下不用,如detach()的使用。

4.创建线程 

 C++11新标准线程库

创建线程函数:windows下:CreateThread(),_beginthted(),_beginthredexe()

POSIX thread(pthread)库:配置后可跨平台使用;

C++11新标准支持跨平台使用。 

一般情况下也可用thread创建线程,但需要引入thread库

主线程从main()函数开始,那么自己写的线程应该也从一个函数开始运行(初始函数)

新标准下detach()函数:

主线程不再与子线程汇合,主线程可以先结束,不用等待子线程,但不建议经常使用。因为当主线程运行完后,子线程很有可能执行不完。

windows运行thread时,一定要配置好mingw.

官网地址:MinGW-w64 - for 32 and 64 bit Windows - Browse Files at SourceForge.nethttps://sourceforge.net/projects/mingw-w64/files/①点击File

 ②下载离线包,因为在线安装的话很慢很慢

 x86_64是64位的,想要运行多线程就必须选择posix,所以我选择的是第二个,直接下载解压即可

③配置环境变量

此电脑---属性---高级系统设置---环境变量---设置系统变量---双击Path将mingw64下的bin路径粘贴复制到Path处,点击确定即就配置好了。

范例演示:

#include <iostream>
#include <thread>
using namespace std;

void myPrint()
{
    cout << "我的线程开始运行" << endl;
    //-------------
    //-------------
    cout << "我的线程运行完毕" << endl;
}

int main()
{
    //(1)创建了线程,线程执行起点(myPrint)是myPrint;(2)执行线程
    thread myThread(myPrint);//thread标准库中的一个类,用来创建线程

    myThread.join();//阻塞主线程,让主线程等待子线程完成执行,然后子线程与主线程汇合,主线程继续
    
    if (myThread.joinable())
    {
        cout << "可以调用可以调用join()或者detach()" << endl;
    }
    else
    {
        cout << "不能调用可以调用join()或者detach()" << endl;
    }

    cout << "Hello World!" << endl;

    system("pause");
    return 0;
}

5.其他创建线程手法:

类对象范例演示:

#include <iostream>
#include <thread>
using namespace std;

class Ta
{
public:
    void operator()()
    {
        cout << "我的线程开始运行" << endl;
        //-------------
        //-------------
        cout << "我的线程运行完毕" << endl;
    }
};

int main()
{
    // main函数里的:
    Ta ta;
    thread myThread(ta);
    myThread.join();

    cout << "Hello World!" << endl;

    system("pause");
    return 0;
}

 用lambda表达式创建线程:

范例演示:

#include <iostream>
#include <thread>

using namespace std;

int main()
{
    auto lambdathread = []
    {
        cout << "线程开始" << endl;
        //...
        cout << "线程结束" << endl;
    };
    thread myThread(lambdathread);
    myThread.join();

    cout << "Hello World!" << endl;

    system("pause");
    return 0;
}

6.传递临时对象作线程参数

6.1 陷阱1范例展示:

#include <iostream>
#include <thread>
using namespace std;

void myPrint(const int &i, char* pmybuf)
{
	//如果线程从主线程detach了
	//i不是mvar真正的引用,实际上值传递,即使主线程运行完毕了,子线程用i仍然是安全的,但仍不推荐传递引用
	//推荐改为const int i
	cout << i << endl;
	//pmybuf还是指向原来的字符串,所以这么写是不安全的
	cout << pmybuf << endl;
}

int main()
{
	int mvar = 1;
	int& mvary = mvar;
	char mybuf[] = "this is a test";
	thread myThread(myPrint, mvar, mybuf);//第一个参数是函数名,后两个参数是函数的参数
	//myThread.join();
	myThread.detach();
	
	cout << "Hello World!" << endl;
}
//引用和指针慎用

6.2 陷阱2范例展示:

#include <iostream>
#include <thread>
#include <string>
using namespace std;

void myPrint(const int i, const string& pmybuf)
{
	cout << i << endl;
	cout << pmybuf << endl;
}

int main()
{
	int mvar = 1;
	int& mvary = mvar;
	char mybuf[] = "this is a test";
	//如果detach了,这样仍然是不安全的
	//因为存在主线程运行完了,mybuf被回收了,系统采用mybuf隐式类型转换成string
	//推荐先创建一个临时对象thread myThread(myPrint, mvar, string(mybuf));就绝对安全了。。。。
	thread myThread(myPrint, mvar, mybuf);
	//myThread.join();
	myThread.detach();

	cout << "Hello World!" << endl;
}

6.3 正确演示:

#include <iostream>
#include <thread>
using namespace std;

void myPrint(const int i, const string pmybuf)
{
    cout << i << endl;
    cout << pmybuf << endl;
}

int main()
{
    int mvar = 1; //用来测试对比
    int &mvary = mvar;
    char mybuf[] = "Bid farewell to the epidemic!";
    thread myThread(myPrint, mvar, string(mybuf)); //string(mybuf)临时对象,为避免隐式类型转换
    //myThread.join();
    myThread.detach();

    system("pause");
    return 0;
}
//类对象同理,创建临时对象就可以解决这种问题,须注意的地方是要传递引用

结论:

建议不使用detach(),只使用join(),这样就不存在局部变量失效导致线程对内存的非法引用问题。

 7. 临时对象作为线程参数深入:

线程ID:id是个数字,每个线程对应一个数字,每个线程对应的线程数字不同,可用c++标准库的函数来获取,this_thread::get_id()

#include <iostream>
#include <thread>

using namespace std;

class A
{
public:
    int m_i;
    A(int i) : m_i(i) { cout << "构造函数 " << std::this_thread::get_id() << endl; }
    A(const A &i) : m_i(i.m_i) { cout << "复制构造函数 " << std::this_thread::get_id() << endl; }
    ~A(int i) : m_i(i) { cout << "析构函数 " << std::this_thread::get_id() << endl; }
};

void myPrint(const A &pmybuf)
{
    pmybuf.m_i = 199;
    cout << "子线程myPrint的参数地址是" << &pmybuf << "thread = " << std::this_thread::get_id() << endl;
}

int main()
{
    int mm=10;
    // myPrint(const A& pmybuf)中引用不能去掉,如果去掉会多创建一个对象
    // const也不能去掉,去掉会出错
    //即使是传递的const引用,但在子线程中还是会调用拷贝构造函数构造一个新的对象,
    //所以在子线程中修改m_i的值不会影响到主线程
    //如果希望子线程中修改m_i的值影响到主线程,可以用thread myThread(myPrint, std::ref(myObj));
    //这样const就是真的引用了,myPrint定义中的const就可以去掉了,类A定义中的mutable也可以去掉了
    thread myThread(myPrint, A(mm));
    myThread.join();
    // myThread.detach();

    cout << "Hello World!" << endl;

    system("pause");
    return 0;
}
#include <iostream>
#include <thread>
#include <memory>
using namespace std;

//创建一个智能指针
void myPrint(unique_ptr<int> pp)
{
	cout << "thread = " << std::this_thread::get_id() << endl;
}

int main()
{
	unique_ptr<int> up(new int(10));
	//独占式指针只能通过std::move()才可以传递给另一个指针
	//传递后up就指向空,新的pp指向原来的内存
	//所以这时就不能用detach了,因为如果主线程先执行完,pp指向的对象就被释放了
	thread myThread(myPrint, std::move(up));
	myThread.join();
	//myThread.detach();

	return 0;
}

8. 多线程数据共享:

涉及互斥量,锁等概念

8.1 互斥量(mutex)的基本概念

  • 互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。
  • 互斥量使用要小心,保护数据不多也不少,少了达不到效果,多了影响效率。

8.2 互斥量的用法

  • 包含#include <mutex>头文件
  • 1. 成员函数:lock(),unlock()
  • 步骤:1.lock(),2.操作共享数据,3.unlock()。
  • lock()和unlock()要成对使用
  • 2. lock_guard类模板
  • lock_guard<mutex> sbguard(myMutex);可直接取代lock()和unlock()
  • lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock(),但不如lock()和unlock()灵活

8.3 死锁

A一直在北京死等B,B一直在深圳死等A,那么A,B永远见不了面,就叫死锁。
1 死锁演示
死锁至少有两个互斥量mutex1,mutex2。

  • a.线程A执行时,这个线程先锁mutex1,并且锁成功了,然后去锁mutex2的时候,出现了上下文切换。
  • b.上下文切换后线程B执行,这个线程先锁mutex2,因为mutex2没有被锁,即mutex2可以被锁成功,然后线程B要去锁mutex1.
  • c.此时,死锁产生了,A锁着mutex1,需要锁mutex2,B锁着mutex2,需要锁mutex1,两个线程没办法继续运行下去。

2 死锁的一般解决方案:
只要保证多个互斥量上锁的顺序一样就不会造成死锁。

3 std::lock()函数模板

std::lock(mutex1,mutex2……); 同时锁定多个互斥量(这种情况很少见),用于处理多个互斥量。如果互斥量中一个没锁住,它就卡着了,等所有互斥量都锁住,才能继续执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁),别忘了unlock()。


4 std::lock_guard的std::adopt_lock参数

std::lock_guardstd::mutex my_guard(my_mutex,std::adopt_lock);
加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock();
adopt_guard为结构体对象,起一个标记作用,表示这个互斥量已经lock(),不需要再lock()。

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;

class A
{
public:
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 100000; ++i)
        {
            cout << "11111111111111111111111111111" << i << endl;
            {
                // lock_guard<mutex> sbguard(myMutex1, adopt_lock);
                lock(myMutex1, myMutex2);
                // myMutex2.lock();
                // myMutex1.lock();
                msgRecvQueue.push_back(i);
                myMutex1.unlock();
                myMutex2.unlock();
            }
        }
    }
    bool outMsgLULProc()
    {
        myMutex1.lock();
        myMutex2.lock();
        if (!msgRecvQueue.empty())
        {
            cout << "000000000000000000000000000000" << msgRecvQueue.front() << endl;
            msgRecvQueue.pop_front();
            myMutex2.unlock();
            myMutex1.unlock();
            return true;
        }
        myMutex2.unlock();
        myMutex1.unlock();
        return false;
    }

    void outMsgRecvQueue()
    {
        for (int i = 0; i < 100000; ++i)
        {
            if (outMsgLULProc())
            {
            }
            else
            {
                cout << "nnnnnnnnnnnnnnnnnnnnnnnnnnn" << endl;
            }
        }
    }

private:
    list<int> msgRecvQueue;
    mutex myMutex1;
    mutex myMutex2;
};

int main()
{
    A myobja;
    mutex myMutex;
    thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
    thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
    myOutMsgObj.join();
    myInMsgObj.join();
    return 0;
}

 9. unique_lock()取代lock_guard()

unique_lock是个类模板,一般推荐使用lock_guard()。

unique_lock比lock_guard灵活很多(多出来很多用法),但效率差一点,占用内存多。
unique_lock<mutex> myUniLock(myMutex);

9.1.unique_lock的第二个参数
9.1.1 std::adopt_lock:

表示这个互斥量已经被lock(),即不需要在构造函数中lock这个互斥量了。
前提:必须提前lock;
lock_guard中也可以用这个参数


9.1.2 std::try_to_lock:

尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里;
使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方
前提:不能提前lock();
owns_locks()方法判断是否拿到锁,如拿到返回true。


9.1.3 std::defer_lock:

如果没有第二个参数就对mutex进行加锁,加上defer_lock是始化了一个没有加锁的mutex
不给它加锁的目的是以后可以调用unique_lock的一些方法
前提:不能提前lock


9.2.unique_lock的成员函数(前三个与std::defer_lock联合使用)
9.2.1 lock():加锁。不用自己unlock();

9.2.2 unlock():解锁。
因为一些非共享代码要处理,可以暂时先unlock(),用其他线程把它们处理了,处理完后再lock()。

9.2.3 try_lock():尝试给互斥量加锁
如果拿不到锁,返回false,否则返回true。

9.2.4 release():

  • unique_lock<mutex>

myUniLock(myMutex);相当于把myMutex和myUniLock绑定在了一起,release()就是解除绑定,返回它所管理的mutex对象的指针,并释放所有权

  • mutex* ptx =myUniLock.release();所有权由ptx接管,如果原来mutex对象处理加锁状态,就需要ptx在以后进行解锁了。

lock的代码段越少,执行越快,整个程序的运行效率越高。
a.锁住的代码少,叫做粒度细,执行效率高;
b.锁住的代码多,叫做粒度粗,执行效率低;

内容来源于bilibilic++11并发与多线程视频课程这个视频 

  • 7
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Windows C多线程编程Demo可以通过使用Windows API来实现。以下是一个简单的例子: ```c #include <stdio.h> #include <windows.h> DWORD WINAPI ThreadFunc(LPVOID lpParam) { int thread_num = *((int*)lpParam); printf("Thread %d is running\n", thread_num); Sleep(1000); // 模拟线程执行的一些操作 printf("Thread %d is done\n", thread_num); return 0; } int main() { const int num_threads = 3; HANDLE threads[num_threads]; int thread_nums[num_threads]; for (int i = 0; i < num_threads; ++i) { thread_nums[i] = i; threads[i] = CreateThread(NULL, 0, ThreadFunc, &thread_nums[i], 0, NULL); if (threads[i] == NULL) { fprintf(stderr, "Error creating thread\n"); return 1; } } WaitForMultipleObjects(num_threads, threads, TRUE, INFINITE); // 等待所有线程执行完毕 for (int i = 0; i < num_threads; ++i) { CloseHandle(threads[i]); // 关闭线程句柄 } return 0; } ``` 上述代码创建了3个线程,并使用CreateThread函数创建了每个线程。每个线程执行相同的ThreadFunc函数,该函数简单地输出线程号,并模拟一些操作。主线程使用WaitForMultipleObjects函数等待所有线程执行完毕。最后,必须关闭每个线程句柄来释放资源。 这只是一个简单的多线程编程示例,在实际应用,可能需要更复杂的线程同步和线程间通信机制,比如互斥量、信号量等。通过使用Windows API,我们可以实现更复杂和高效的多线程应用程序。 ### 回答2: Windows下的C语言多线程编程可以通过使用Windows API提供的相关函数来实现。常用的函数包括`CreateThread`创建线程、`WaitForSingleObject`等待线程运行结束、`CloseHandle`关闭线程句柄等。 以下是一个简单的Windows C多线程编程的示例程序: ```c #include <stdio.h> #include <windows.h> DWORD WINAPI MyThreadFunc(LPVOID lpParam) { int i; for (i = 0; i < 5; i++) { printf("Thread is running: %d\n", i); Sleep(1000); // 线程休眠1秒 } return 0; } int main() { HANDLE hThread; DWORD dwThread; // 创建线程 hThread = CreateThread(NULL, 0, MyThreadFunc, NULL, 0, &dwThread); if (hThread == NULL) { printf("Failed to create thread.\n"); return 1; } printf("Main thread is running.\n"); // 等待线程结束 WaitForSingleObject(hThread, INFINITE); printf("Main thread is exiting.\n"); // 关闭线程句柄 CloseHandle(hThread); return 0; } ``` 该示例程序创建了一个新线程,新线程运行`MyThreadFunc`函数,该函数会打印5次当前运行次数,并休眠1秒。主线程输出一条信息后,使用`WaitForSingleObject`等待新线程运行结束,然后输出退出信息。 以上就是一个简单的Windows C多线程编程示例。在实际开发,可以结合具体需求使用多线程来提升程序的并发性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值