- 错误展示
- 错误原因
- 解决办法
- 1. 在本问题情况下:使用printf函数替代cout:
- 2. 使用互斥锁使 cout函数线程保持原子状态
- 什么是原子操作?
错误展示
最近学习多线程的时候,创建了一堆线程,然后每个线程都运行这个方法:
void *start_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
int cnt = 10;
while (cnt)
{
cout << "cnt :" << cnt <<" "<< "&cnt:" << &cnt << endl;
cnt--;
sleep(1);
}
// delete td;
return nullptr;
}
出现了下面这种情况,使用cout函数打印输出在显示器上面的内容很混乱。
我自己认为是cout
函数输出内容到stdout文件
时,在这个线程还未输出完整的情况下又被别的线程调度然后又继续cout
导致了最后刷新缓冲区就成了这样子。
上述出现错误的完整代码:
#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
class ThreadData
{
public:
int number;
pthread_t tid;
char namebuffer[64];
};
void *start_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
int cnt = 10;
while (cnt)
{
cout << "cnt :" << cnt <<" "<< "&cnt:" << &cnt << endl;
cnt--;
sleep(1);
}
return nullptr;
}
int main()
{
//创建一批线程
vector<ThreadData *> threads;
#define NUM 10
for (int i = 0; i < NUM; i++)
{
ThreadData *td = new ThreadData();
td->number = i + 1;
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i + 1);
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);
}
for (auto &iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;
}
while(true)
{
sleep(2);
cout<<"mainthred"<<endl;
}
return 0;
}
错误原因
错误原因:如果多个线程同时调用cout
输出数据,由于cout
不是线程安全的,不是原子操作(下面会说什么是原子操作),所以可能会出现数据交错或混乱的情况。这是因为多个线程可能会同时访问和修改cout的输出缓冲区,导致数据交错或丢失。
std::cout的输出操作通常涉及到多个步骤,例如格式化数据、写入缓冲区、输出到终端等,这些步骤可能会在不同的线程中交错执行,从而导致输出混乱。
cout函数的缓冲区策略:在默认情况下,cout函数使用行缓冲区(line-buffered)策略,即每当输出一个换行符时,缓冲区就会被刷新。此外,如果缓冲区已满,也会自动刷新缓冲区。但是,如果程序异常退出或者使用endl或flush等函数强制刷新缓冲区,也会导致缓冲区被刷新。
多线程环境下,cout输出语句可能出现的问题,对于下面的输出语句:
std::cout << "hello" << cnt << std::endl;
多个线程同时执行这个输出语句,那么可能会出现以下情况:
- 多个线程同时输出"hello"和cnt的值到输出缓冲区中,导致输出内容被交错输出。
- 多个线程同时输出换行符到输出缓冲区中,导致换行符被重复输出或者被覆盖。
- 多个线程同时刷新输出缓冲区,导致输出内容被交错输出或者被覆盖。
解决办法
1. 在本问题情况下:使用printf函数替代cout:
printf("cnt : %d &cnt:%p\n",cnt,&cnt);
printf
函数通常不是原子操作。这意味着两个或多个线程可能会同时尝试打印到控制台,并且输出可能会交错或损坏。但是,printf 函数在用于将输出打印到单个流时是原子操作。这是因为 printf 函数是作为单个系统调用实现的,这意味着在任何其他线程可以访问流之前,它保证由一个线程完整执行。
printf内部会使用同步机制来确保输出的数据不会被其他线程中断。具体来说,printf通常会使用文件锁或互斥锁来保证同一时间只有一个线程在输出数据,从而避免输出混乱或数据交错的问题。
例如:
- 如果有两个线程都在尝试打印到标准输出流,则 printf 函数将确保一次只能打印一个线程。这将防止输出交错或损坏。
- 如果有两个线程尝试打印到不同的流,则 printf 函数不会阻止输出交错或损坏。这是因为 printf 函数不会锁定流本身。通常,最好使用互斥锁来保护由多个线程访问的任何共享资源。这将有助于防止争用条件并确保程序的输出正确。
文件锁定是一种允许进程独占访问文件的机制。这意味着一次只有一个进程可以访问该文件。当进程锁定文件时,将阻止其他进程访问该文件,直到释放锁定为止。
互斥锁是一种同步原语,它允许线程锁定资源,以便一次只有一个线程可以访问它。当线程锁定互斥锁时,将阻止其他线程访问资源,直到释放锁。
2. 使用互斥锁使 cout函数线程保持原子状态
使用方法:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex mtx;
void print_message(string message) {
// Lock the mutex
mtx.lock();
// Print the message
cout << message << endl;
// Unlock the mutex
mtx.unlock();
}
int main() {
// Create two threads
thread t1(print_message, "Hello, world!");
thread t2(print_message, "Goodbye, world!");
// Wait for the threads to finish
t1.join();
t2.join();
return 0;
}
代码工作原理的说明:
- 对象在代码顶部声明。
mutex
- 该函数将字符串作为参数,并将消息打印到控制台。该函数还会在打印消息之前锁定互斥锁,并在打印消息后解锁互斥锁。
print_message()
main()
函数创建两个线程。每个线程使用不同的消息调用函数。print_message()
- 函数等待线程完成。
join()
确保一次只有一个线程可以访问该对象。这可以防止输出交错或损坏。
什么是原子操作?
不会被线程调度机制打断的操作
原子操作是指在计算机中执行的一种操作,它要么完全成功完成,要么完全不执行,没有中间状态。原子操作通常是一个单个的、不可分割的操作,可以被看作是一种基本操作。原子操作的目的是确保多个并发的执行线程或进程能够正确地协作,而不会产生竞争条件或死锁问题。
原子操作通常用于对共享资源进行访问和修改的情况,例如在多线程编程中,多个线程可能同时访问和修改同一个变量,这时就需要使用原子操作来确保操作的正确性。常见的原子操作包括读取和写入一个共享变量、递增或递减一个计数器、测试和设置一个标志等。
如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀