Điều này thực sự là một nhận xét để trả lời câu trả lời của Mats Petersson, nhưng tôi muốn cung cấp các ví dụ về mã.
Sự cố là tranh chấp của một tài nguyên cụ thể và cũng là một đường dẫn trong bộ nhớ cache.
Alternative 1:
#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
static const uint64_t ITERATIONS = 10000000000ULL;
int main(int argc, const char** argv)
{
size_t numThreads = 1;
if (argc > 1) {
numThreads = strtoul(argv[1], NULL, 10);
if (numThreads == 0)
return -1;
}
std::vector<std::thread> threads;
uint64_t k = 0;
for (size_t t = 0; t < numThreads; ++t) {
threads.emplace_back([&k]() { // capture k by reference so we all use the same k.
while (k < ITERATIONS) {
k++;
}
});
}
for (size_t t = 0; t < numThreads; ++t) {
threads[t].join();
}
return 0;
}
Đây là chủ đề tranh cho một biến duy nhất, thực hiện cả hai đọc và viết mà buộc nó ping-pong gây tranh cãi và đưa ra luận cứ luồng duy nhất hiệu quả nhất.
#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
#include <atomic>
static const uint64_t ITERATIONS = 10000000000ULL;
int main(int argc, const char** argv)
{
size_t numThreads = 1;
if (argc > 1) {
numThreads = strtoul(argv[1], NULL, 10);
if (numThreads == 0)
return -1;
}
std::vector<std::thread> threads;
std::atomic<uint64_t> k = 0;
for (size_t t = 0; t < numThreads; ++t) {
threads.emplace_back([&]() {
// Imperfect division of labor, we'll fall short in some cases.
for (size_t i = 0; i < ITERATIONS/numThreads; ++i) {
k++;
}
});
}
for (size_t t = 0; t < numThreads; ++t) {
threads[t].join();
}
return 0;
}
Ở đây chúng tôi phân chia lao động theo định thức (chúng tôi thuộc trường hợp numThread không phải là ước số của ITERATIONS nhưng đủ gần để trình diễn). Thật không may, chúng tôi vẫn gặp phải tranh chấp để truy cập vào phần tử được chia sẻ trong bộ nhớ.
#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
#include <atomic>
static const uint64_t ITERATIONS = 10000000000ULL;
int main(int argc, const char** argv)
{
size_t numThreads = 1;
if (argc > 1) {
numThreads = strtoul(argv[1], NULL, 10);
if (numThreads == 0)
return -1;
}
std::vector<std::thread> threads;
std::vector<uint64_t> ks;
for (size_t t = 0; t < numThreads; ++t) {
threads.emplace_back([=, &ks]() {
auto& k = ks[t];
// Imperfect division of labor, we'll fall short in some cases.
for (size_t i = 0; i < ITERATIONS/numThreads; ++i) {
k++;
}
});
}
uint64_t k = 0;
for (size_t t = 0; t < numThreads; ++t) {
threads[t].join();
k += ks[t];
}
return 0;
}
Một lần nữa điều này là xác định về phân phối khối lượng công việc và chúng tôi dành một chút nỗ lực để kết hợp kết quả. Tuy nhiên chúng tôi đã không làm gì để đảm bảo việc phân phối các quầy ủng hộ phân phối CPU lành mạnh. Đối với điều đó:
#include <cstdint>
#include <thread>
#include <vector>
#include <stdlib.h>
static const uint64_t ITERATIONS = 10000000000ULL;
#define CACHE_LINE_SIZE 128
int main(int argc, const char** argv)
{
size_t numThreads = 1;
if (argc > 1) {
numThreads = strtoul(argv[1], NULL, 10);
if (numThreads == 0)
return -1;
}
std::vector<std::thread> threads;
std::mutex kMutex;
uint64_t k = 0;
for (size_t t = 0; t < numThreads; ++t) {
threads.emplace_back([=, &k]() {
alignas(CACHE_LINE_SIZE) uint64_t myK = 0;
// Imperfect division of labor, we'll fall short in some cases.
for (uint64_t i = 0; i < ITERATIONS/numThreads; ++i) {
myK++;
}
kMutex.lock();
k += myK;
kMutex.unlock();
});
}
for (size_t t = 0; t < numThreads; ++t) {
threads[t].join();
}
return 0;
}
Ở đây chúng tôi tránh tranh chấp giữa các luồng xuống cấp độ bộ nhớ cache, ngoại trừ trường hợp chúng tôi sử dụng mutex để kiểm soát đồng bộ hóa. Đối với khối lượng công việc tầm thường này, mutex sẽ có một địa ngục có giá tương đối. Ngoài ra, bạn có thể sử dụng alignas để cung cấp mỗi thread với bộ nhớ riêng của nó ở phạm vi bên ngoài và tóm tắt kết quả sau khi kết nối, loại bỏ sự cần thiết cho mutex. Tôi để nó như một bài tập cho người đọc.
Tất cả các chủ đề đang làm là bashing đầu cố gắng để đọc từ và ghi vào k. Và khi lần đầu tiên kết thúc, nó vẫn phải đợi đến lần thứ hai. – chris
@chris: 'k' không phải là' biến động', do đó, các chuỗi không cạnh tranh, bởi vì chúng không chia sẻ hiệu quả 'k'. –
@MooingDuck: 'volatile' đảm bảo ghi vào bộ nhớ, nhưng không có' biến động' không ngăn cản chúng. Câu hỏi cụ thể nói "... không có tối ưu hóa ..." và nó là điển hình cho các bản dựng không được tối ưu hóa để làm theo các hướng dẫn chương trình rất nghĩa đen và không phải để "cache" giá trị trong sổ đăng ký. Trên phần cứng Intel/AMD gần đây, có khả năng xả tự động giữa các bộ đệm lõi truy cập cùng một địa chỉ bộ nhớ sẽ làm chậm quá trình này. –